Грамматика плагинов
specVersion 1
Плагин NimBlock не является кодом: это JSON-файл, описывающий правила, которые читает и выполняет движок Paper, написанный один раз для всех. На этой странице точно сказано, что в нём можно написать, и ничего другого сделать нельзя.
Одно правило, и только правило
Вся грамматика умещается в одном предложении: когда происходит это, если выполняются эти условия, то сделать то-то. Плагин представляет собой список правил, а правило состоит из одного триггера, от нуля до 20 условий и от одного до 50 действий.
Java нигде не генерируется, и ничего не компилируется: сервер загружает движок, который читает спецификацию. Именно это свойство важно дальше, потому что новая версия Minecraft оплачивается один раз в этом движке, а не в каждом плагине, написанном с его помощью.
Как выглядит спецификация
{
"specVersion": 1,
"name": "Добро пожаловать",
"rules": [
{
"id": "приветствие",
"name": "Приветственное сообщение",
"match": "all",
"trigger": { "type": "player.join" },
"conditions": [
{ "type": "player.has_permission", "params": { "permission": "nimblock.vip" } }
],
"actions": [
{ "type": "player.send_message", "params": { "message": "&6Добро пожаловать, {player}!" } }
]
}
]
}
match может быть all (по умолчанию) или
any, а условие может нести "not": true.
enabled: false оставляет правило в файле, не включая его.
Грамматика в форме EBNF
Списки терминалов ниже формируются из каталога: это буквально идентификаторы, которые умеет выполнять движок, а не сделанная вручную запись.
<plugin> ::= "{" "specVersion" ":" 1 ","
[ "id" ":" <slug64> "," ]
"name" ":" <label> ","
[ "description" ":" <text500> "," ]
"rules" ":" "[" <rule> { "," <rule> } "]" "}" (* 1..200 *)
<rule> ::= "{" [ "id" ":" <slug32> "," ]
"name" ":" <label> ","
[ "enabled" ":" <boolean> "," ] (* = true *)
[ "match" ":" ( "all" | "any" ) "," ] (* = "all" *)
"trigger" ":" <trigger> ","
[ "conditions" ":" "[" [ <condition>
{ "," <condition> } ] "]" "," ] (* 0..20 *)
"actions" ":" "[" <action>
{ "," <action> } "]" "}" (* 1..50 *)
<trigger> ::= "{" "type" ":" <trigger-id> [ "," "params" ":" <params> ] "}"
<condition> ::= "{" "type" ":" <condition-id> [ "," "not" ":" <boolean> ]
[ "," "params" ":" <params> ] "}"
<action> ::= "{" "type" ":" <action-id> [ "," "params" ":" <params> ] "}"
<params> ::= "{" [ <param-name> ":" <param-value>
{ "," <param-name> ":" <param-value> } ] "}"
<param-value> ::= <template> | <identifier> | <integer> | <number>
| <boolean> | <choice>
<template> ::= { <literal> | <variable> | <colour> } (* 0..2000 *)
<literal> ::= /[^{}]/ | "{{" | "}}"
<variable> ::= "{" /[a-zA-Z][a-zA-Z0-9_]*/ "}"
<colour> ::= "&" ( "0".."9" | "a".."f" | "k".."o" | "r" )
<identifier> ::= /[A-Za-z][A-Za-z0-9_:.\/-]{0,63}/
<slug32> ::= /[A-Za-z0-9_-]{1,32}/
<slug64> ::= /.{0,64}/
<label> ::= /.{1,64}/
<text500> ::= /.{0,500}/
<trigger-id> ::= "player.join"
| "player.quit"
| "player.chat"
| "player.death"
| "player.respawn"
| "block.break"
| "block.place"
| "command"
| "schedule.repeat"
<condition-id> ::= "player.has_permission"
| "player.is_op"
| "player.in_world"
| "player.health_below"
| "text.contains"
| "text.equals"
| "number.compare"
| "chance"
<action-id> ::= "player.send_message"
| "player.send_actionbar"
| "player.send_title"
| "player.play_sound"
| "player.give_item"
| "player.give_effect"
| "player.heal"
| "player.teleport"
| "player.kick"
| "broadcast"
| "server.run_command"
| "cancel_event"
| "log"
| "delay"
Две вещи не видны в этих правилах вывода, потому что они касаются
согласованности, а не формы. Условие или действие, требующее игрока,
можно написать только под триггером, который его предоставляет:
«вылечить игрока» не имеет смысла под «каждые N секунд». А
cancel_event можно написать только под отменяемым триггером.
Оба случая отклоняются на этапе записи, а не выполнения.
Шаблоны
Параметры типа text являются шаблонами:
{player} в них заменяется тем, что предоставил
триггер, {{ даёт буквальную фигурную скобку, а
цветовые коды &a &l переводятся.
Переменная, неизвестная триггеру, считается ошибкой, а не текстом. Каждый триггер объявляет, что он предоставляет (см. список ниже): вне этого списка читать нечего, и опечатка отклоняется на этапе записи, а не попадает в чат игроков. А вот собственные параметры триггера шаблонами не являются: они читаются при запуске сервера, ещё до того, как появится что подставлять.
Названия Minecraft пишутся по-разному в зависимости от того, что они
обозначают. Предмет называется в обоих написаниях
(DIAMOND_SWORD или diamond_sword). Звук
или эффект называется своим ключом Minecraft
(entity.player.levelup, speed), а не константой
Bukkit: ключ уходит клиенту как есть и переживает смену версий.
Чего грамматика не позволяет
Она закрытая, и это стоит сказать раньше всего остального. То, что есть в каталоге, выполнимо, всё остальное невозможно, и таким и останется: выполнить произвольный Java-код нельзя никак. Именно это делает студию мгновенной (компилировать нечего), а спецификацию безопасной для установки, откуда бы она ни пришла.
Условия не вкладываются друг в друга: это осознанное
ограничение версии 1, булево дерево плохо рисуется блоками, и
all / any / not почти всегда
достаточно. Единственной лазейкой служит действие «выполнить команду от
имени консоли», которое на этом сервере имеет ровно ту же силу, что и
консоль, не больше и не меньше.
Триггеры
Что запускает правило, и какие переменные каждый из них предоставляет шаблонам.
player.join
Игрок подключается
В момент, когда игрок заходит на сервер, после загрузки его мира.
предоставляет:
{player} Игрок, который подключается,
{world} Мир, в который он попадает
player.quit
Игрок отключается
В момент, когда игрок покидает сервер. Он ещё доступен, но любое отправленное сообщение теряется.
предоставляет:
{player} Игрок, который уходит,
{world} Мир, который он покидает
player.chat
Игрок пишет в чат
отменяемый
До того, как сообщение будет отправлено остальным игрокам. Можно отменить: тогда сообщение не увидит никто.
предоставляет:
{player} Автор сообщения,
{world} Его мир,
{message} Написанное сообщение
player.death
Игрок умирает
В момент смерти игрока, до экрана возрождения.
предоставляет:
{player} Погибший игрок,
{world} Его мир,
{killer} Игрок-виновник, пусто, если такого нет
player.respawn
Игрок возрождается
Когда игрок возвращается в игру после смерти.
предоставляет:
{player} Возрождающийся игрок,
{world} Мир возрождения
block.break
Игрок ломает блок
отменяемый
До того, как блок исчезнет. Можно отменить: тогда блок останется на месте.
предоставляет:
{player} Игрок, который ломает,
{world} Его мир,
{block} Тип блока (DIAMOND_ORE),
{x} Координата X блока,
{y} Высота блока,
{z} Координата Z блока
block.place
Игрок ставит блок
отменяемый
До того, как блок будет установлен. Можно отменить: тогда блок не будет установлен.
предоставляет:
{player} Игрок, который ставит,
{world} Его мир,
{block} Тип устанавливаемого блока,
{x} Координата X блока,
{y} Высота блока,
{z} Координата Z блока
command
Игрок вводит команду
Создаёт новую команду на сервере. ⚠️ Команда с ранее не существовавшим именем появится только после перезапуска сервера: Paper принимает новые имена команд только при запуске. А вот содержимое правила обновляется на лету.
-
nametext Имя команды, без слэша -
descriptiontext необязательно Описание, отображаемое в справке -
permissiontext необязательно Требуемое право, пусто для всех
предоставляет:
{player} Игрок, который ввёл команду,
{world} Его мир,
{args} Всё, что идёт после команды, как есть
schedule.repeat
Каждые N секунд
Повторяет правило, пока сервер работает. Игрок здесь ни при чём: можно использовать только действия, которые не требуют игрока.
-
secondsinteger [1..86400] Интервал в секундах
Условия
Что фильтрует. От нуля до 20 на правило, объединяются через match.
player.has_permission
У игрока есть право
требует игрока
-
permissiontext Право
player.is_op
Игрок — оператор
требует игрока
player.in_world
Игрок находится в мире
требует игрока
-
worldtext Имя мира
player.health_below
У игрока меньше X сердец
требует игрока
В очках здоровья: 20 очков равны 10 сердцам.
-
valuenumber [0..1024] Очки здоровья
text.contains
Текст содержит
-
texttext Проверяемый текст -
searchtext Что ищем в нём -
ignoreCaseboolean необязательно, по умолчанию:trueИгнорировать регистр
text.equals
Текст равен
-
texttext Проверяемый текст -
valuetext Ожидаемое значение -
ignoreCaseboolean необязательно, по умолчанию:trueИгнорировать регистр
number.compare
Сравнить два числа
Обе части — это шаблоны: {y} сравнивается с 62 без написания кода.
-
lefttext Левая часть -
operator< | <= | == | != | >= | > Сравнение -
righttext Правая часть
chance
Случайно, в X % случаев
-
percentnumber [0..100] Процент
Действия
Что происходит. От одного до 50 на правило, выполняются по порядку.
player.send_message
Отправить сообщение игроку
требует игрока
-
messagetext Сообщение
player.send_actionbar
Показать сообщение над панелью действий
требует игрока
-
messagetext Сообщение
player.send_title
Показать крупный заголовок
требует игрока
-
titletext Заголовок -
subtitletext необязательно Подзаголовок -
fadeInnumber [0..60] необязательно, по умолчанию:0.5Появление, в секундах -
staynumber [0..600] необязательно, по умолчанию:3Длительность в секундах -
fadeOutnumber [0..60] необязательно, по умолчанию:0.5Исчезновение, в секундах
player.play_sound
Проиграть звук игроку
требует игрока
-
soundidentifier Звук, ключ Minecraft (entity.player.levelup) -
volumenumber [0..10] необязательно, по умолчанию:1Громкость -
pitchnumber [0.5..2] необязательно, по умолчанию:1Высота тона
player.give_item
Дать предмет игроку
требует игрока
То, что не помещается в инвентарь, падает на землю у его ног.
-
materialidentifier Предмет (DIAMOND_SWORD) -
amountinteger [1..2304] необязательно, по умолчанию:1Количество
player.give_effect
Применить эффект к игроку
требует игрока
-
effectidentifier Эффект, ключ Minecraft (night_vision) -
secondsinteger [1..86400] необязательно, по умолчанию:10Длительность в секундах -
amplifierinteger [0..255] необязательно, по умолчанию:0Уровень, начиная с 0
player.heal
Вылечить игрока
требует игрока
Здоровье максимальное, голод утолён.
player.teleport
Телепортировать игрока
требует игрока
-
xnumber [-30000000..30000000] Координата X -
ynumber [-512..1024] Высота -
znumber [-30000000..30000000] Координата Z -
worldtext необязательно Мир, пусто для мира самого игрока
player.kick
Выгнать игрока
требует игрока
-
reasontext Отображаемая причина
broadcast
Объявить всему серверу
-
messagetext Сообщение
server.run_command
Выполнить команду от имени консоли
Аварийный выход из грамматики: всё, что умеет консоль. Это вся власть консоли над этим сервером, так что пиши это осознанно.
-
commandtext Команда, без слэша
cancel_event
Отменить то, что только что произошло
требует отменяемый триггер
Существует только у отменяемых событий: сообщение не отправляется, блок не ломается.
log
Записать в консоль сервера
-
messagetext Сообщение -
levelinfo | warn необязательно, по умолчанию:infoУровень
delay
Подождать
Приостанавливает выполнение остальных действий правила. Правило, которое ждёт, больше ничего не может отменить: «Отменить» должно идти раньше.
-
secondsnumber [0.05..3600] Секунды
Публикуемые файлы
JSON Schema формируется из каталога при каждом развёртывании и служит машиночитаемым эталоном: редактор, который её учитывает, автоматически дополняет идентификаторы и отклоняет неизвестные параметры.
- plugin-1.schema.json : схема, в формате JSON Schema draft 2020-12.
- catalogue-1.json : сам каталог, источник, из которого выводится всё остальное (валидатор, схема, блоки редактора и движок на Java).
Каталог содержит подписи на французском языке, который является исходным. Сами идентификаторы не переводятся: именно их выполняет движок.
Что происходит при изменении грамматики
Добавление триггера, условия или действия ничего не ломает: спецификация,
написанная раньше, продолжает работать. А вот ломающее изменение
увеличивает specVersion, и тогда движок отказывается выполнять то,
чего не понимает, вместо того чтобы выполнить это неправильно, и сообщает об
этом в консоли сервера.
Адреса выше несут этот номер: plugin-1.schema.json
всегда будет обозначать именно эту грамматику, даже в день, когда появится
другая.