Единый код для Paper и Velocity: архитектура мультилоадер-плагинов в 2026 году

Прокомментировать Просмотры: 6

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

Вполне возможно. Данная архитектурная методология именуется multiloader: общая функциональность инкапсулируется в единственном модуле, в то время как под конкретные окружения создаются легковесные адаптеры. Давайте детально рассмотрим внутреннее устройство такого решения, актуальный стек технологий текущего года, а также подводные камни, с которыми я столкнулся в процессе проектирования рабочего шаблона.

Для начала уточним терминологию во избежание путаницы

В майнкрафт-сообществе под понятием multiloader подразумевают две совершенно разные концепции. Первое — это кросс-платформенные модификации для Forge, Fabric и NeoForge в рамках единого репозитория, ярким примером чему служит MultiLoader-Template от Jared. Второе — предмет нашей текущей беседы: плагины, чей костяк успешно функционирует как на Paper-сервере, так и на прокси Velocity.

Эти направления кардинально отличаются друг от друга. В модах общий код напрямую зависит от маппингов и специфичного для загрузчика процесса инициализации игры. В случае с плагинами Paper и Velocity мы имеем дело с двумя независимыми API поверх единого игрового движка, причем их интеграция значительно проще: обе системы используют Adventure, поддерживают команды на базе Brigadier и базируются на Java. Данный материал посвящен исключительно второму сценарию.

Детальный осмотр обеих платформ

Для тех, кто никогда не сталкивался с одновременной разработкой под обе системы, вкратце поясню их ключевые различия. Оба продукта, к слову, развиваются под эгидой команды PaperMC:

PaperMC
PaperMC

Paper представляет собой полноценный игровой сервер. Игровые персонажи, измерения, тики, инвентарь — все это его епархия. Точкой входа выступает класс, наследующий JavaPlugin с методами onEnable/onDisable, дополненный фазами ранней инициализации вроде PluginBootstrap (зарегистрированные там команды доступны даже в датапаках) и PluginLoader (предварительная загрузка библиотек в classpath до старта). Метаданные описываются в файле paper-plugin.yml. Актуальная ветка релизов — 26.x, требующая наличия Java 25.

Velocity функционирует как прокси-сервер. За ним может быть закреплено множество бэкендов, игроков он воспринимает исключительно как сетевые подключения, а концепция игровых миров в нем отсутствует как таковая. Привычной точки входа здесь нет — вместо этого используется аннотация @Plugin совместно с внедрением зависимостей через Guice: экземпляры ProxyServer, системный логгер и рабочая директория передаются в конструктор автоматически. Жизненный цикл построен на событиях: ProxyInitializeEvent, PostLoginEvent, ProxyShutdownEvent. Манифест velocity-plugin.json генерируется процессором аннотаций автоматически, поэтому вручную писать его не требуется. На фронте версий наблюдается переходный этап: ветка 3.5.x продолжает работать на Java 21 и обладает более широкой совместимостью со сторонними расширениями, в то время как четвертая версия жестко требует минимум Java 25 (что явно зафиксировано в официальной документации).

Общих черт у платформ не так много, однако они фундаментальны: текстовый движок Adventure, встроенный Brigadier для обработки команд и экосистема Java. Этого более чем достаточно, чтобы вынести всю бизнес-логику на верхний уровень, оставив на откуп адаптерам лишь узкоспециализированные задачи.

Архитектурное строение: квартет модулей и один незыблемый закон

Типовая структура проекта выглядит следующим образом:

  • common — сосредоточение всей логики: конфигурационные файлы, текстовые сообщения, команды, функционал. Полное отсутствие импортов из API Paper и Velocity.

  • api — общедоступный SPI для сторонних разработчиков. Также полностью независим от платформ.

  • paper — адаптерный модуль: Bootstrap, Loader, JavaPlugin, обработчики событий, paper-plugin.yml.

  • velocity — адаптерный модуль: класс с @Plugin, листенеры, автогенерация velocity-plugin.json.

Направление зависимостей строго одностороннее, что наглядно отражает схема:

paper/ ──\
           +--> common/ --> api/
velocity/ ──/

Ядро ничего не ведает о конкретных платформах, тогда как платформы осведомлены обо всем. Модуль api не имеет никаких внешних зависимостей, за исключением библиотеки Adventure.

Существует главное правило, поддерживающее целостность всей конструкции: в каталоге common категорически запрещено наличие любых платформенных импортов. Как только туда проникает пакет вроде org.bukkit или com.velocitypowered, концепция multiloader разрушается, превращая проект в те же две кодовые базы, просто сгруппированные в пределах одного каталога. Подобное упущение легко выявляется элементарным поиском по импортам за пару секунд, и этот контроль следует держать в голове при каждом новом коммите.

Взаимодействие платформ с ядром реализовано через компактные интерфейсы-прослойки. В качестве примера из моего шаблона можно привести отправителя команды. Библиотека Cloud на платформе Paper возвращает CommandSourceStack, на Velocity — CommandSource, однако самой логике команд эти объекты не нужны. Следовательно, создается универсальный контракт:

public interface TemplateSender {
    String name();
    boolean hasPermission(String node);
    void sendMessage(Component message);
}

Буферный адаптер для Paper аккуратно инкапсулирует stack.getSender() (буквально три строки кода, объединяющие CommandSender, Audience и проверку прав), а Velocity-адаптер выступает практически прозрачной оберткой над CommandSource. В результате команда /template ping реализуется в единственном экземпляре:

public static  void register(
        CommandManager manager,
        Function senders,
        TemplateCore core) {
    var root = manager.commandBuilder("template");
    manager.command(root.literal("ping")
            .permission("template.command.ping")
            .handler(ctx -> {
                var sender = senders.apply(ctx.sender());
                // ...
            }));
}

Универсальный дженерик-параметр C обозначает нативный тип отправителя конкретной среды. Модуль Paper передает PaperSender::new, Velocity делегирует VelocitySender::new, тогда как общий код абстрагирован от этих деталей.

Актуальный технологический стек для multiloader-разработки

Ниже приведен набор инструментов, используемый в моей сборке и успешно проходящий проверки в CI. Версии указаны строго фиксированные, поскольку подход «использовать самые свежие сборки» для multiloader чреват проблемами: платформы обновляются в собственном ритме, игнорируя репозиторий Maven Central.

Gradle с каталогом версий (version catalog). Четыре независимых модуля без централизованного управления зависимостями гарантированно приводят к рассинхронизации уже через месяц: подсистема Paper тянет одну редакцию библиотек, Velocity — совершенно другую. Единый файл libs.versions.toml является не просто элементом эстетики, а суровой необходимостью. К тому же к нему напрямую обращается инструмент Dependabot.

Adventure в связке с MiniMessage. Оформление текстовых сообщений вручную посредством устаревших параграфов в текущих реалиях неприемлемо. Строки MiniMessage активно задействуются в конфигурациях, а метод Placeholder.unparsed("player", name) закрывает потребность в динамической подстановке. Отдельное внимание стоит уделить безопасности, поскольку речь идет о реальном векторе уязвимостей, а не о теоретических измышлениях: никнейм пользователя, внедренный в строку MiniMessage простой конкатенацией, открывает путь к инъекции разметки. Игрок с ником вроде администратор способен нарушить отображение чужих реплик. Единственно верный путь — применение плейсхолдеров и метода unparsed для любых входящих от пользователя данных. В моем репозитории даже предусмотрен специализированный тест, симуляцией передающий враждебный ник в приветственный модуль и верифицирующий, что теги остались обычным текстом.

Стоит отметить, что библиотека Adventure перешла под крыло проекта PaperMC, а весной состоялся релиз мажорного пятого поколения: повышены требования к версии Java, удалены все устаревшие (deprecated) методы, упразднен механизм Examination, а аннотации мигрированы на JSpecify. Для кодовой базы без устаревших вызовов переход проходит незаметно, в противном случае выручает официальное руководство по миграции. Лично я пока остаюсь на четвертой версии, поскольку ее предоставляет сама среда выполнения платформ, а мажорные релизы запрещено подключать в качестве provided-зависимостей (причины будут описаны далее при разборе работы dependabot).

Configurate. Признанный стандарт конфигурационных систем для экосистемы Paper, а именно связка configurate-yaml версии 4.2.0 — наиболее зрелое и стабильное решение на сегодняшний день. Я заложил в менеджер конфигураций два незыблемых принципа: плагин никогда не должен падать из-за синтаксических ошибок в конфиге (поврежденный файл порождает лишь предупреждение в логе, после чего система продолжает функционировать на базовых значениях), а конфигурация обязана самовосстанавливаться (пропущенные параметры автоматически дополняются дефолтными значениями). Пользовательские файлы config.yml переживают любые апдейты без риска полной очистки.

Incendo Cloud v2. Унифицированный фреймворк команд: ядро cloud-core подключается в common-модуль, а платформоспецифичные cloud-paper и cloud-velocity отправляются в соответствующие адаптеры. Здесь кроется неочевидный нюанс, с которым мне довелось столкнуться: ядро и платформенные адаптеры имеют независимую нумерацию версий. В моем проекте используется cloud-core версии 2.1.0 в паре с адаптерами версии 2.0.1, поэтому попытка указать «единую версию для всего» в каталоге обречена на провал — подобного артефакта просто не существует на Maven Central. В среде Paper задействуется современный менеджер через конструкцию PaperCommandManager.builder()...buildBootstrapped(context), благодаря чему команды, зарегистрированные на этапе bootstrap, становятся доступны датапакам, а поддержка Brigadier активируется автоматически. На стороне Velocity менеджер инициализируется вручную с вызовом SenderMapper.identity() поверх нативного интерфейса CommandSource, избавляя от необходимости задействовать модуль Guice.

Версии Java: 21 в основании, 25 на вершине. Общая логика активно задействует records для описания конфигов и контекстных данных, конструкцию Optional вместо возврата null-значений, сопоставление с образцом (pattern matching), а также виртуальные потоки для легких фоновых задач (вызов newThreadPerTaskExecutor оказывается значительно эффективнее классических пулов потоков для мелких операций, в то время как долгоживущие или периодические задачи традиционно передаются в распоряжение планировщика платформы). При этом модуль paper компилируется строго с использованием тулчейна Java 25, и это не прихоть автора: библиотека paper-api версии 26.x скомпилирована под байт-код 25-й версии, поэтому компилятор более ранней ревизии физически не способен прочитать ее классы. Таким образом, популярный лозунг «везде используем Java 21» разбивается о суровую действительность: общие модули и Velocity работают на 21-й версии, а Paper требует 25-ю.

Сборка проекта: что упаковывается в теневой архив, а что предоставляется средой

Логика разделения зависимостей на две категории предельно проста. Если компонент поставляется самой платформой — мы исключаем его из итогового jar-файла: библиотеки Adventure и slf4j уже присутствуют и в Paper, и в Velocity, поэтому в конфигурации shadowJar они явно отсекаются. Все остальное подлежит обязательной сборке: библиотеки Configurate и Cloud не входят в поставку платформ, следовательно, они внедряются внутрь каждого скомпилированного jar с предварительной релокацией пакетов, исключая возможные конфликты с иными плагинами, использующими собственные инстансы того же Cloud. Дополнительно задействуется директива mergeServiceFiles(), без которой процесс слияния файлов META-INF/services приведет к потере SPI-провайдеров сторонних библиотек. Каждый из этих пунктов продиктован практикой: пренебрежение ими ведет либо к раздуванию архива лишними компонентами, либо к аварийному завершению работы в рантайме.

Метки версий внедряются в дескрипторы силами системы сборки, исключая ручной труд: в файл paper-plugin.yml данные транслируются посредством фильтра expand() (причем исключительно номер версии, и этому есть критически важное объяснение, приведенное ниже), тогда как для Velocity перед началом компиляции генерируется класс BuildConstants из заготовленного шаблона, дабы аннотация @Plugin имела доступ к актуальной константе.

Кому не требуется multiloader-архитектура?

Будем откровенны: подобный подход нужен далеко не всем. Создание компактного плагина узкой специализации исключительно под Paper не оправдывает организацию громоздкой структуры из четырех gradle-модулей — проще разместить весь код в едином пространстве. Архитектура multiloader начинает окупать себя в том случае, когда кодовая база востребована на двух платформах сразу: когда требуются идентичные команды, общая конфигурация, единые текстовые шаблоны или синхронизация данных между прокси-слоем и игровыми серверами. Если появление второй платформы даже не планируется — подобное решение создаст избыточный технический долг. Однако если интеграция прокси уже маячит на горизонте — гораздо дешевле сразу заложить правильную структуру, чем впоследствии болезненно рефакторить монолит.

Где ознакомиться с готовым решением

Все описанные выше принципы представляют собой выжимку из реального рабочего шаблона, доступного в репозитории: multiloader-template на GitHub.

 

Источник

Поделиться:

Похожие статьи

Поиск по играм, новостям и статьям…

Введите не менее двух символов

Введите не менее двух символов