Написание Paper/Bukkit плагина LiteSMT #1 — Основа понятий и окружение

от автора

Перед началом…

Всем привет! Это серия постов будет иметь много различных тем, но и я так-же не забыл о написании ядра на Rust, скоро будут продолжения).

Немного понятий

Я думаю, что стоит начать с некоторой основной информацией по созданию плагинов, а именно:

  1. Все плагины основываются на Bukkit — API для плагинов, которая может немного отличаться в зависимости от версий.

  2. Плагины пишутся в основном на Java или иногда на Kotlin.

  3. Некоторая информация плагина исключительно для ядра храниться в plugin.yml, где можно найти версию плагина, главный класс плагина и многое другое.

  4. В стандартных случаях плагинам хватает обычного, но существуют моменты, где могут понадобится NMS — net.minecraft.server или же API к самому Minecraft.

  5. Плагины без труда могут использовать API других плагинов, если имеется такая возможность, например PlaceholderAPI и Vault.

  6. Почти всегда, когда ядро обращается к плагину — выполняется в синхронном потоке сервера.

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

Подготавливаем наш тестовый сервер

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

Для начала нам стоит выбрать основную версию и ядро, я выбрал 1.16.5 версию так-как сейчас она как основа для версий выше.

Статистика bStats
Статистика bStats

Из статистики выше видно, что большинство серверов на 1.16.5 и выше, а ядро Paper и следовательно как изначально планировалось было выбрано ядро Paper. Скачать его версии 1.16.5 можно по этой ссылке.

Скачанный файл по ссылке выше я помещаю в отдельную директорию рядом со своим Start.sh файлом:

Содержимое Start.sh

java -Xms512M -Xmx1G -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=200 -XX:+UnlockExperimentalVMOptions -XX:+DisableExplicitGC -XX:+AlwaysPreTouch -XX:G1HeapWastePercent=5 -XX:G1MixedGCCountTarget=4 -XX:G1MixedGCLiveThresholdPercent=90 -XX:G1RSetUpdatingPauseTimePercent=5 -XX:SurvivorRatio=32 -XX:+PerfDisableSharedMem -XX:MaxTenuringThreshold=1 -XX:G1NewSizePercent=30 -XX:G1MaxNewSizePercent=40 -XX:G1HeapRegionSize=8M -XX:G1ReservePercent=20 -XX:InitiatingHeapOccupancyPercent=15 -Dusing.aikars.flags=https://mcflags.emc.gs -Daikars.new.flags=true -jar paper-1.16.5-794.jar nogui

После первого запуска будет скачан кеш и создан eula.txt( в котором надо будет установить true вместо false ):

Что будет показано после первого запуска
Что будет показано после первого запуска

После изменения eula.txt будет такой вывод с полным запуском:

Сервер полностью создаст все нужные файлы
Сервер полностью создаст все нужные файлы
Как я настроил некоторые файлы при разработке
bukkit.yml
settings:   allow-end: false   warn-on-overload: true   permissions-file: permissions.yml   update-folder: update   plugin-profiling: false   connection-throttle: 0   query-plugins: false   deprecated-verbose: default   shutdown-message: Server closed   minimum-api: none spawn-limits:   monsters: 10   animals: 10   water-animals: 5   water-ambient: 10   ambient: 5 chunk-gc:   period-in-ticks: 900 ticks-per:   animal-spawns: 1   monster-spawns: 1   water-spawns: 1   water-ambient-spawns: 1   ambient-spawns: 1   autosave: 12000 aliases: now-in-commands.yml
enable-jmx-monitoring=false rcon.port=25575 level-seed= gamemode=survival enable-command-block=false enable-query=false generator-settings= level-name=world motd=Test Server query.port=25565 pvp=true generate-structures=false difficulty=easy network-compression-threshold=256 max-tick-time=60000 max-players=20 use-native-transport=true online-mode=false enable-status=true allow-flight=false broadcast-rcon-to-ops=true view-distance=6 max-build-height=256 server-ip= allow-nether=false server-port=25565 enable-rcon=false sync-chunk-writes=true op-permission-level=4 prevent-proxy-connections=false resource-pack= entity-broadcast-range-percentage=100 rcon.password= player-idle-timeout=0 debug=false force-gamemode=false rate-limit=0 hardcore=false white-list=false broadcast-console-to-ops=true spawn-npcs=true spawn-animals=true snooper-enabled=true function-permission-level=2 level-type=flat text-filtering-config= spawn-monsters=true enforce-whitelist=false resource-pack-sha1= spawn-protection=16 max-world-size=500

После изменений конфигурации я удалил все миры, чтобы был создан только world с плоской генерацией. И теперь консоль после запуска выглядит так:

Теперь для начала нам надо загрузить важны плагины:

  • PlaceholderAPI — API для плейсхолдеров.

  • PlugManX — форк оригинального PlugMan для работы с плагинами(перезагрузка, загрузка, выгрузка и тд).

  • Auto Reload — автоматическая перезагрузка плагинов в случае изменений.

После загрузки можно перезагрузить частично сервер используя reload confirm. И потом плагины загрузятся и некоторые создадут конфигурации.

Изменённые конфигурации
plugins/PlugManX/config.yml
ignored-plugins: [PlugManX] notify-on-broken-command-removal: true auto-load:   enabled: true   check-every-seconds: 2 auto-unload:   enabled: true   check-every-seconds: 2 auto-reload:   enabled: false   check-every-seconds: 2

plugins/PlaceholderAPI/config.yml
# PlaceholderAPI # Version: 2.11.1 # Created by: extended_clip # Contributors: https://github.com/PlaceholderAPI/PlaceholderAPI/graphs/contributors # Issues: https://github.com/PlaceholderAPI/PlaceholderAPI/issues # Expansions: https://api.extendedclip.com/all/ # Wiki: https://github.com/PlaceholderAPI/PlaceholderAPI/wiki # Discord: https://helpch.at/discord # No placeholders are provided with this plugin by default. # Download placeholders: /papi ecloud check_updates: true cloud_enabled: true cloud_sorting: "name" cloud_allow_unverified_expansions: true boolean:   'true': 'yes'   'false': 'no' date_format: MM/dd/yy HH:mm:ss debug: true

plugins/bStats/config.yml
enabled: false serverUuid: 00000-00000 logFailedRequests: false

После сделанных изменений надо снова перезагрузить сервер, а после полной перезагрузки мы можем спокойно зайти на наш тестовый сервер с любого клиента, который поддерживает версию 1.16.5, так-же стоит выдать себе все права используя op <ваш ник>.

Подготовка нашего плагина

Так-как плагин будет написан на стандартном Java, то нам надо подумать о сборщике нашего плагина, есть несколько вариантов: IDE компилятор, Maven и Gradle. Давайте посмотрим основные плюсы и минусы, которые я выделил как основные:

  • Компилятор IDE(IDEA, Eclipse и др)
    Плюсы:
    1) Все зависимости и компиляция настроены прямо в настройках редактора.
    2) Может быть быстрым но зависит от настроек компиляции.
    Минусы:
    1) Для редактирования может потребоваться конкретный редактор или его поддержка в другом редакторе.
    2) Часто ограничен возможностями самого редактора.
    3) Компиляция часто может идти тяжелее нежели на Maven или Gradle.

  • Maven
    Плюсы:
    1) Возможно настроить и форматирование файлов, все зависимости и репозитори, а так-же плагины компиляции.
    2) Не зависит от конкретного редактора.
    3) Быстро компилирует, но хуже Gradle.
    Минусы:
    1) Для компиляции каждый раз запускается новый процесс, который каждый раз заново читает и собирает информацию.

  • Gradle
    Плюсы:
    1) Есть настройки форматирования файлов, зависимостей, репозиторий и различных дополнений(например Lombok).
    2) Запускает компиляцию в фоновом процессе, который считывает конфиги и другое после изменений или при другой нужде.
    3) Не зависит от редактора как и Maven.
    4) Можно писать конфигурацию компиляции на языке Groovy или Kotlin DSL
    5) Запуск компиляции проходит с помощью запуска фонового процесса(если не запущен) или обращение к нему.
    Минусы:
    1) Фоновый процесс постоянно требует некоторое количество ОЗУ.

Вы конечно можете сами выбрать сборщик под себя, но я выбрал Gradle и из-за чего дальше будет информация связанная с ним. Так-же выбрал редактор Intellij IDEA от JetBrains так-как он очень замечательно работает с Java.

Первым делом нужно инициализировать Gradle проект:

Создание Gradle проекта в IDEA
Создание Gradle проекта в IDEA

После создания проекта я в первую очередь удалил папку test и изменил build.gradle под проект:

Содержимое build.grade
plugins {     id 'java' }  group 'xyz.distemi' version '1.0-SNAPSHOT'  repositories {     mavenCentral()     maven {         name = "PaperMC"         url = "https://repo.papermc.io/repository/maven-public/"     }     maven {         name = "PlaceholderAPI"         url = 'https://repo.extendedclip.com/content/repositories/placeholderapi/'     } }  dependencies {     compileOnly 'com.destroystokyo.paper:paper-api:1.16.5-R0.1-SNAPSHOT' // PaperMC     compileOnly 'me.clip:placeholderapi:2.11.1' // API плагина PlaceholderAPI      compileOnly 'org.projectlombok:lombok:1.18.24' // Lombok API     annotationProcessor 'org.projectlombok:lombok:1.18.24' // Lombok процессор }

И следом я нажал на кнопку синхронизации в своей IDEA.

Теперь нам надо сделать главный класс плагина, который у меня будет xyz.distemi.litesmt.LiteSMT:

Базовый код главного класса плагина LiteSMT
package xyz.distemi.litesmt; // Объявляем наш пакет  // Импортируем Getter из ломбок, абстрактный класс JavaPlugin и // интерфейс логгера. import lombok.Getter;  import org.bukkit.plugin.java.JavaPlugin;  import java.util.logging.Logger;  public class LiteSMT extends JavaPlugin {   // Создаём две статичные переменные:     @Getter     private static LiteSMT instance; // Класс плагина.     @Getter     private static Logger jlogger; // Логгер.     @Override     public void onEnable() { // Устанавливаем наши статичные переменные:         instance = this;          jlogger = super.getLogger();       // Выводим в консоль сообщение Hello from LiteSMT       // от имени плагина.       jlogger.info("Hello from LiteSMT!");     } }
Для людей, которые не знают Java или хотящие подробное объяснение

В классе выше мы создали файл в папке src/main/xyz/distemi/litesmt файл LiteSMT.java с содержанием выше.

Импорты передают компилятору информацию о использованных классах и другого, без импорта ему неизвестно какой именно класс/интерфейс или другое вы используете.

public class позволяет нам показать класс как главный в этом файле, а это значит, что никто не мешает создать рядом просто class без public.

private является областью видимости поля, которая может быть применена как на метод, так и на переменную.

static — модификатор, говорящий, что данное поле может быть использовано, инициализировано и тд без конструирования самого класса( new Class() ).

Аннотация @Override позволяет нам перезаписать метод из класса-предка(у нас это абстрактный класс JavaPlugin)
Аннотация @Getter из Lombok указывает, что должен будет сгенерироваться дополнительный код, используя процессор аннотаций Lombok-а.

Метод onEnable работает как конструктор, но только вот он исключительно для нашего плагина. Метод возвращает тип void, а именно ничего.

Внутри перезаписанного метода onEnable мы устанавливаем глобальные переменные instance и jlogger, но если с первым случаем ясно, что ссылаемся на сконструированный класс, то во втором случае используем super уже для обращению к «предку», а именно JavaPlugin.
Следом выводим в консоль сообщение из аргумента.

Чтож, главный класс у нас имеется, но для работы плагина этого недостаточно!
Серверу нужно знать какой же класс главный и другую информацию, для этого нам нужно создать файл plugin.yml, но уже не в качестве кода, а файл-ресурса, в Gradle такие файлы можно создать в директории проекта src/main/resources/, где файлы внутри не могут быть скомпилированы, а копируются в наш jar «сырыми», но есть например возможность некоторого форматирования, однако пока думаю можно будет обойтись и без него.

Создание plugin.yml

В папке ресурсов я создаю файл plugin.yml, который по умолчанию не имеет ничего я записываю содержимое ниже:

name: LiteSMT main: xyz.distemi.litesmt.LiteSMT version: 1.0 author: Distemi prefix: LSMT depend:   - PlaceholderAPI

Как вы можете заметить, то мы устанавливаем всего шесть значений, однако некоторые необязательные.

name (Обязательно) — даёт знать ядру о «имени» плагина, которое может использоваться как в некоторых командах сервера, так и других плагинах по типу того же PlugManX.

main (Обязательно) — указывает класс в нашем jar, который становиться главным в работе плагина, 1 jar = 1 плагин.

version (Обязательно) — атрибут, означающий версию нашего плагина, может быть как 1.0 так и 1.0.0.

author — указывает автора плагина.

prefix — префикс в логе вместо названия плагина.

depend — обязательные зависимости для плагина, если каких-то нету, то плагин не будет загружен, в списке указываются «имена» плагинов.

Если интересно почитать о других атрибутах и тд для plugin.yml, то можете почитать по этой ссылке.

Всё почти готово, однако теперь нам нужно собрать плагин и перекинуть в папку с сервером, можно конечно это делать руками, но я больше предпочитаю делать это автоматически, используя свои задачи в Gradle, для чего нам нужно в build.gradle добавить следующее:

task copyToDevEnv_1_16_5() {     doLast {         copy {             from "build/libs/LiteSMT-1.0-SNAPSHOT.jar"             into "../test-server1.16.5/plugins/"         }     } }  build.finalizedBy copyToDevEnv_1_16_5

Тут мы объявляем задачу, которая копирует готовый jar плагина в папку указанную из into, а build.finalizedBy означает, что задачу build мы всегда заканчиваем с copyToDevEnv_1_16_5. Давайте теперь мы впишем ./gradlew build —offline -x test, где мы запускаем компиляцию без доступа к интернету(—offline) и исключаем задачу(-x) тестов(test). Теперь смотрим в нашу папку с плагинами и видим:

Ура! Наш плагин успешно собрался и сам был помещён в директорию с плагинами. Теперь пробуем запустить наш сервер и видим…

Да! Плагин наш был успешно запущен и при запуске вывел в консоль наше сообщение, однако для проверки автоматической перезагрузки плагина в случае изменений я могу чуть изменить сообщение в коде и заново собрать плагин прошлой командой в консоль/терминал и увидеть уже в консоли сервера:

Всё-таки я добавил «Changed from me!» в строку вывода и после сборки плагин AutoReload сам увидел изменение и перезагрузил плагин, ну не удобство ли, когда надо бывает частенько и главный класс изменить?)

Так-же как вы могли бы заметить, то наш префикс из plugin.yml тоже показывает результат так-как без того атрибута у нас выводился бы LiteSMT.

В плагине мы можем так-же допустим реализовать работу с конфигурацией, событиями и другим, но я в этой части покажу лишь работу с событиями так-как на конфигурацию у меня есть много интересного содержания)

Обработка событий в плагине или как можно приукрасить чат.

Сейчас мы будем работать лишь с тремя из большого количества событий, а именно вход игрока, выход и написание сообщений в чат. Наверное многим не нравиться стандартный формат чата из и игры из-за чего скачивают и устанавливают плагины по типу Chatty и другого, но сейчас мы сделаем некую свою мини альтернативу, правда не всё, но хоть что-то)

Для событий в плагинах используют отдельные классы, которые просто помимо своего существования должны наследовать один из интерфейсов от Bukkit и быть зарегистрированы в событиях Bukkit-а.

Для событий чата будет у меня отдельный пакет в моём jar, а именно xyz.distemi.litesmt.listeners.chat, который в IDEA создаётся в два клика:

нужный пакет -> ПКМ -> New -> Package» title=»нужный пакет -> ПКМ -> New -> Package» width=»1920″ height=»1007″ data-src=»https://habrastorage.org/getpro/habr/upload_files/349/4fd/ad3/3494fdad339187772d9173af9b996cf7.png»/><figcaption>нужный пакет -> ПКМ -> New -> Package</figcaption></figure>
<p>В нашем новом пакете создадим класс с именем <strong>MainChatListener</strong>, который по началу имеет только публикацию класса и <abbr title=пакет. И после его имени мы должны прописать implements Listener, однако как я говорил раньше, то нужно и импортировать классы, чтобы сборщик мог знать какой класс нам нужен и JVM тоже, поэтому IDEA предлагает выбрать Listener из нескольких пакетов, но нам нужен именно с org.bukkit.event и следом появиться импорт org.bukkit.event.Listener. Теперь данный класс для ядра считается неким слушателем событий, однако он не зарегистрирован и пусть, а это значит, что от него нету толка, для этого в главном классе, в onEnable мы прописываем:

Bukkit.getPluginManager().registerEvents(new MainChatListener(), this);

И теперь для bukkit наш слушатель зарегистрирован на все типы событий, а в качестве второго аргумента мы передаём плагин, это означает, что этот слушатель принадлежит нашему плагин.

— Тогда получается возможно регистрировать слушатели и из других плагинов?
— Верно! Однако не стоит так делать.

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

import io.papermc.paper.event.player.AsyncChatEvent; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.TextColor; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener;  public class MainChatListener implements Listener {     @EventHandler     public void onChat(AsyncChatEvent event) {         event.renderer((source, sourceDisplayName, message, viewer) ->                  Component.text()                   .append(sourceDisplayName.color(TextColor.fromHexString("#a8a432")))                   .append(Component.text(" : "))                   .append(message)                   .build());     } }
Объяснение кода

Все методы событий обязательно должны иметь аннотацию EventHandler от org.bukkit, чтобы было ясно, что этот метод точно является слушателем событий, а именно AsyncChatEvent так-как именно он указан в аргументе. Далее для форматирования не используется устаревший метод formatter, а новый — renderer, который имеет больше возможностей. Для нашего renderer мы используем так называемый функциональные интерфейсы, в данном случае ChatRenderer и спасибо Java, что тут получилось так сокращённо, ведь иначе вышло бы на несколько строк больше.

Для форматирования мы создаём наш новый чат-компонент Component, но для всего связанного с чатом сейчас используется пакет net.kyori.adventure.text. Component.text() создаёт нам конструктор, который мы используем для связки трёх других компонентов: ник, разделитель для сообщения( : ) и самого сообщения.

Метод TextColor.fromHexString даёт возможность получить нам цвет для чат-компонента из HEX строки c нужным цветом, в моём случае #a8a432. Этот цвет я применяю на переменную компонента sourceDisplayName и добавляю получившейся компонент в конструктор.

Далее я добавляю в конструктор разделитель в сообщении » : «, который можно получить используя Component.text(» : «).

Последнее добавление в конструктор — само сообщение игрока.

Заканчивается создание форматированного компонента методом build.

Теперь если мы попробуем набрать в консоль любое сообщение, то увидим:

Как выглядит итог форматирования.
Как выглядит итог форматирования.

Далее я бы убрал сообщение выхода с сервера таким кодом:

@EventHandler public void onQuit(PlayerQuitEvent event) { event.quitMessage(null); }

Теперь при повторной сборке и следующей проверки выхода мы не увидим сообщения в консоли о выходе игрока, кроме того, что является от самого ядра.

Можно и приукрасить сообщение о входе:

@EventHandler public void onJoin(PlayerJoinEvent event) { event.joinMessage(Component.text() .append(Component.text("["))     .append(Component.text("+", TextColor.fromHexString("#28ff03")))     .append(Component.text("] "))     .append(event.getPlayer().displayName())     .build()); }

И после сборки этого кода, то при заходе будет данное сообщение:

Итог

Вот и была сделана небольшая основа для плагина, которая дальше будет дополняться всё большим функционалом в следующих частях!

Готовый сходный код доступен по этой ссылке.


ссылка на оригинал статьи https://habr.com/ru/post/668836/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *