Перед началом...
Всем привет! Это серия постов будет иметь много различных тем, но и я так-же не забыл о написании ядра на Rust, скоро будут продолжения).
Немного понятий
Я думаю, что стоит начать с некоторой основной информацией по созданию плагинов, а именно:
Все плагины основываются на Bukkit - API для плагинов, которая может немного отличаться в зависимости от версий.
Плагины пишутся в основном на Java или иногда на Kotlin.
Некоторая информация плагина исключительно для ядра храниться в plugin.yml, где можно найти версию плагина, главный класс плагина и многое другое.
В стандартных случаях плагинам хватает обычного, но существуют моменты, где могут понадобится NMS - net.minecraft.server или же API к самому Minecraft.
Плагины без труда могут использовать API других плагинов, если имеется такая возможность, например PlaceholderAPI и Vault.
Почти всегда, когда ядро обращается к плагину - выполняется в синхронном потоке сервера.
Думаю этой малой информации может хватить для того, чтобы мы могли подготовить основу нашего плагина и небольшое окружение для работы со своим плагином.
Подготавливаем наш тестовый сервер
Всё-таки я думаю нам сначала стоит именно тестовый сервер так-как тема конкретно написания плагина уже гораздо больше.
Для начала нам стоит выбрать основную версию и ядро, я выбрал 1.16.5 версию так-как сейчас она как основа для версий выше.
Из статистики выше видно, что большинство серверов на 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 проект:
После создания проекта я в первую очередь удалил папку 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 создаётся в два клика:
В нашем новом пакете создадим класс с именем MainChatListener, который по началу имеет только публикацию класса и пакет. И после его имени мы должны прописать 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());
}
И после сборки этого кода, то при заходе будет данное сообщение:
Итог
Вот и была сделана небольшая основа для плагина, которая дальше будет дополняться всё большим функционалом в следующих частях!
Готовый сходный код доступен по этой ссылке.