NimBlock Войти

Грамматика плагинов

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 принимает новые имена команд только при запуске. А вот содержимое правила обновляется на лету.

  • name text Имя команды, без слэша
  • description text необязательно Описание, отображаемое в справке
  • permission text необязательно Требуемое право, пусто для всех

предоставляет: {player} Игрок, который ввёл команду, {world} Его мир, {args} Всё, что идёт после команды, как есть

schedule.repeat Каждые N секунд

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

  • seconds integer [1..86400] Интервал в секундах

Условия

Что фильтрует. От нуля до 20 на правило, объединяются через match.

player.has_permission У игрока есть право требует игрока
  • permission text Право
player.is_op Игрок — оператор требует игрока
player.in_world Игрок находится в мире требует игрока
  • world text Имя мира
player.health_below У игрока меньше X сердец требует игрока

В очках здоровья: 20 очков равны 10 сердцам.

  • value number [0..1024] Очки здоровья
text.contains Текст содержит
  • text text Проверяемый текст
  • search text Что ищем в нём
  • ignoreCase boolean необязательно, по умолчанию: true Игнорировать регистр
text.equals Текст равен
  • text text Проверяемый текст
  • value text Ожидаемое значение
  • ignoreCase boolean необязательно, по умолчанию: true Игнорировать регистр
number.compare Сравнить два числа

Обе части — это шаблоны: {y} сравнивается с 62 без написания кода.

  • left text Левая часть
  • operator < | <= | == | != | >= | > Сравнение
  • right text Правая часть
chance Случайно, в X % случаев
  • percent number [0..100] Процент

Действия

Что происходит. От одного до 50 на правило, выполняются по порядку.

player.send_message Отправить сообщение игроку требует игрока
  • message text Сообщение
player.send_actionbar Показать сообщение над панелью действий требует игрока
  • message text Сообщение
player.send_title Показать крупный заголовок требует игрока
  • title text Заголовок
  • subtitle text необязательно Подзаголовок
  • fadeIn number [0..60] необязательно, по умолчанию: 0.5 Появление, в секундах
  • stay number [0..600] необязательно, по умолчанию: 3 Длительность в секундах
  • fadeOut number [0..60] необязательно, по умолчанию: 0.5 Исчезновение, в секундах
player.play_sound Проиграть звук игроку требует игрока
  • sound identifier Звук, ключ Minecraft (entity.player.levelup)
  • volume number [0..10] необязательно, по умолчанию: 1 Громкость
  • pitch number [0.5..2] необязательно, по умолчанию: 1 Высота тона
player.give_item Дать предмет игроку требует игрока

То, что не помещается в инвентарь, падает на землю у его ног.

  • material identifier Предмет (DIAMOND_SWORD)
  • amount integer [1..2304] необязательно, по умолчанию: 1 Количество
player.give_effect Применить эффект к игроку требует игрока
  • effect identifier Эффект, ключ Minecraft (night_vision)
  • seconds integer [1..86400] необязательно, по умолчанию: 10 Длительность в секундах
  • amplifier integer [0..255] необязательно, по умолчанию: 0 Уровень, начиная с 0
player.heal Вылечить игрока требует игрока

Здоровье максимальное, голод утолён.

player.teleport Телепортировать игрока требует игрока
  • x number [-30000000..30000000] Координата X
  • y number [-512..1024] Высота
  • z number [-30000000..30000000] Координата Z
  • world text необязательно Мир, пусто для мира самого игрока
player.kick Выгнать игрока требует игрока
  • reason text Отображаемая причина
broadcast Объявить всему серверу
  • message text Сообщение
server.run_command Выполнить команду от имени консоли

Аварийный выход из грамматики: всё, что умеет консоль. Это вся власть консоли над этим сервером, так что пиши это осознанно.

  • command text Команда, без слэша
cancel_event Отменить то, что только что произошло требует отменяемый триггер

Существует только у отменяемых событий: сообщение не отправляется, блок не ломается.

log Записать в консоль сервера
  • message text Сообщение
  • level info | warn необязательно, по умолчанию: info Уровень
delay Подождать

Приостанавливает выполнение остальных действий правила. Правило, которое ждёт, больше ничего не может отменить: «Отменить» должно идти раньше.

  • seconds number [0.05..3600] Секунды

Публикуемые файлы

JSON Schema формируется из каталога при каждом развёртывании и служит машиночитаемым эталоном: редактор, который её учитывает, автоматически дополняет идентификаторы и отклоняет неизвестные параметры.

  • plugin-1.schema.json : схема, в формате JSON Schema draft 2020-12.
  • catalogue-1.json : сам каталог, источник, из которого выводится всё остальное (валидатор, схема, блоки редактора и движок на Java).

Каталог содержит подписи на французском языке, который является исходным. Сами идентификаторы не переводятся: именно их выполняет движок.

Что происходит при изменении грамматики

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

Адреса выше несут этот номер: plugin-1.schema.json всегда будет обозначать именно эту грамматику, даже в день, когда появится другая.