Как обратиться к Velocity из Bukkit?

Взаимодействие между Velocity и Bukkit. Как сделать отправку команд на все сервера разом? Как перезагрузить сервер прямо из Velocity? Сейчас расскажу!

Как обратиться к Velocity из Bukkit?
Photo by Christopher Gower / Unsplash

Предисловие

Для тех, кому нужна только основная часть статьи - Нажми сюда

Ситуация, которая побудила меня написать эту статью следующая: Поступил ко мне заказ на плагин для Bukkit, который будет реализовывать механику анимированной перезагрузки сервера. Что я имею ввиду под "анимированной перезагрузкой"? Согласен, звучит странно, однако это видео должно дать понимание того, что от меня требовалось:

Перезагрузка сервера с анимацией тумана

С этим туманом там вообще отдельная история, но она у меня заготовлена под другой пост. Как мы видим, игрок вводит команду и сервер перезагружается. Но перед тем, как это происходит игрок видит красивую анимацию экспансии мира вокруг него.

Дело в том, что такая анимация может быть отправлена игроку только с Прокси-сервера, который, собственно Velocity. С ним, да и вообще с прокси-серверами я толком ранее не сталкивался, как разработчик. Соответственно, как там всё устроено знал поверхностно. Разобрался, кстати, довольно быстро, API там удобное и практичное, но несколько отличается от привычного ведро-крана. Итого, план работы плагина у меня получился следующий:

Отправить игроку туман
Отправить игроку Title
Отправить игроку звук
Выполнить нужные команды на сервере
По окончании таймера кинуть игрока в лобби
Стопнуть сервер

С туманом, тайтлом - всё понятно. Но как игроку отправить звук из велосити? (На момент написания статьи этот метод есть в Velocity, но он пока ничего не делает, о чем нам говорит предупреждение от разработчиков)

Вот тут мы уже перейдем к основной части статьи.

Как заставить Bukkit и Velocity обмениваться данным между собой?

В первую очередь, стоит сказать, что часть информации, которая будет рассказана далее была взята с официальной документации Paper.

ВАЖНО: В предоставленном коде используется библиотека lombok.

Кейс 1: Взаимодействие Velocity -> Bukkit

Для начала разберём Velocity часть, а после уже перейдем к Bukkit.
Первым делом, мы зарегистрируем наш канал общения. Сделать это можно прямо в главном классе нашего плагина. Канал будет носить ключ "plugin:main".

// Создаём переменную с идентификатором нашего канала
public final MinecraftChannelIdentifier IDENTIFIER = MinecraftChannelIdentifier.from("plugin:main");

@Subscribe
public void onProxyInitialization(ProxyInitializeEvent event) {
    // Регистрируем канал
    proxyServer.getChannelRegistrar().register(IDENTIFIER);
}

Далее мы создадим Enum (перечисление) всех сообщений, которые мы будем отправлять. Для хранения всех енумов, я создал пакет enums.

import lombok.Getter;

@Getter
public enum MessageType {
    EXECUTE_COMMAND("execute_command"), // Выполнить команду
    SHUTDOWN("shutdown"), // Выключить сервер
    PLAY_SOUND("play_sound"); // Проиграть звук всем на сервере

    private final String type;

    MessageType(String type) {
        this.type = type;
    }

}

Разбирать мы будем именно 3 этих сообщения - Выполнение какой-либо команды, выключение сервера и проигрывание звука всем игрокам на сервере.

Следующим нашим шагом будет объект отправщика сообщений, в который мы передадим главный класс плагина, идентификатор канала общения и логгер (необязательно)

import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import lombok.SneakyThrows;
import org.slf4j.Logger;
import ru.er1one.messaginglesson.MessagingLesson; // Импорт главного класса
import ru.er1one.messaginglesson.enums.MessageType; // Импорт нашего енума

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.util.Optional;

public class MessageSender {

    private final MessagingLesson plugin; // Главный класс

    private final MinecraftChannelIdentifier channel; // Канал общения

    private final Logger logger; // Логгер

    public MessageSender(MessagingLesson plugin, MinecraftChannelIdentifier channel) {
        this.plugin = plugin;
        this.channel = channel;
        this.logger = plugin.getLogger();
    }

    @SneakyThrows
    public void sendMessage(MessageType messageType, String serverName,
    String... args) {
        ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
        DataOutputStream out = new DataOutputStream(byteOut);

        out.writeUTF(messageType.getType());

        for (String arg : args) {
            out.writeUTF(arg);
        }

        Optional<RegisteredServer> server = plugin.getProxy().getServer(serverName);

        if (server.isEmpty()) {
            logger.warn("Отправка сообщения {}: Сервер {} не найден!",
            messageType.name(), serverName);
            return;
        }

        server.get().sendPluginMessage(channel, byteOut.toByteArray());
    }
}

Аннотация @SneakyThrows у метода sendMessage автоматически сгенерирует проверенные исключения, что облегчит читаемость кода.
Теперь построчно разберём метод sendMessage

ByteArrayOutputStream byteOut =
new ByteArrayOutputStream()
- Создаём байтовый массив, для хранения данных.
DataOutputStream out = new DataOutputStream(byteOut) - Создаём поток для записи данных в байтовый массив.
out.writeUTF(messageType
.getType())
- Первым делом записываем в поток данных тип нашего сообщения.
Далее у нас идёт цикл, где мы перебираем каждый переданный аргумент и также записываем его в поток данных.
Optional<RegisteredServer> server =
plugin.getProxy().
getServer(serverName)
- Опционально получаем бэкэнд сервер, на который будем отправлять наше сообщения по его названию из конфига Velocity.
if (server.isEmpty()) { ... } - Проверяем получили ли мы сервер или его не существует и логируем в консоль предупреждение о том, что сервер не был найден.
server.get()
.sendPluginMessage(channel,
byteOut.toByteArray())
- Отправляем сообщение серверу по заданному каналу, преобразовывая наш поток данных в массив байтов.

Следующим шагом мы объявим объект нашего отправщика сообщения в главном классе private MessageSender messageSender и создадим его, при запуске сервер this.messageSender =
new MessageSender(this, CHANNEL)

Теперь, всё что нам требуется для того, чтобы отправить какое-либо сообщение на бэкэнд, это вызвать метод sendMessage:

messageSender.sendMessage(MessageType.EXECUTE_COMMAND, "test_server", "/broadcast Привет!");
messageSender.sendMessage(MessageType.SHUTDOWN, "test_server");
messageSender.sendMessage(MessageType.PLAY_SOUND, "test_server", "ui.button.click");

Однако, наш бэкенд сервер пока что не умеет обрабатывать такие сообщения. Теперь мы займемся Bukkit частью.

TO BE CONTINUED...