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

Всем привет! Это серия постов будет иметь много различных тем, но и я так-же не забыл о написании ядра на 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
нужный пакет -> ПКМ -> New -> Package

В нашем новом пакете создадим класс с именем 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());
}

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

Итог

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

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

Комментарии (0)