Gramatyka pluginów
specVersion 1
Plugin NimBlock to nie kod: to plik JSON opisujący reguły, odczytywany i wykonywany przez silnik Paper napisany raz dla wszystkich. Ta strona mówi dokładnie, co wolno w nim zapisać, i nic poza tym nie jest możliwe.
Jedna reguła i nic poza regułą
Cała gramatyka mieści się w jednym zdaniu: gdy to się wydarzy, jeśli te warunki są spełnione, wtedy zrób to. Plugin to lista reguł, reguła to jeden wyzwalacz, od zera do 20 warunków i od jednej do 50 akcji.
Nigdzie nie powstaje żaden kod Java i nic nie jest kompilowane: serwer wczytuje silnik, który odczytuje specyfikację. To ta właściwość liczy się na przyszłość, bo koszt nowej wersji Minecrafta płaci się raz, w tym silniku, a nie w każdym pluginie, który za jego pomocą powstał.
Jak wygląda specyfikacja
{
"specVersion": 1,
"name": "Witaj",
"rules": [
{
"id": "powitanie",
"name": "Wiadomość powitalna",
"match": "all",
"trigger": { "type": "player.join" },
"conditions": [
{ "type": "player.has_permission", "params": { "permission": "nimblock.vip" } }
],
"actions": [
{ "type": "player.send_message", "params": { "message": "&6Witaj {player}!" } }
]
}
]
}
match przyjmuje wartość all (domyślnie) lub
any, a warunek może mieć ustawione "not": true.
enabled: false zachowuje regułę w pliku, nie włączając jej.
Gramatyka w zapisie EBNF
Poniższe listy terminali są generowane z katalogu: to dosłownie identyfikatory, które silnik potrafi wykonać, a nie transkrypcja zrobiona ręcznie.
<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"
Dwóch rzeczy nie widać w tych regułach produkcji, bo dotyczą spójności, a nie
formy. Warunek lub akcja wymagające gracza dają się zapisać tylko pod
wyzwalaczem, który go dostarcza: „ulecz gracza” nie ma sensu pod „co N
sekund”. A cancel_event da się zapisać tylko pod wyzwalaczem,
który można anulować. Oba przypadki są odrzucane już przy zapisie, nie dopiero
przy wykonaniu.
Szablony
Parametry typu text to szablony:
{player} zostaje w nich zastąpiony tym, co
dostarczył wyzwalacz, {{ daje dosłowny nawias
klamrowy, a kody kolorów &a &l są
tłumaczone.
Zmienna nieznana wyzwalaczowi to błąd, a nie tekst. Każdy wyzwalacz deklaruje, co dostarcza (zobacz listę poniżej): poza tą listą nie ma nic do odczytania, a literówka jest odrzucana już przy zapisie, zamiast trafić na czat graczy. Parametry samego wyzwalacza nie są natomiast szablonami: są odczytywane przy starcie serwera, zanim będzie cokolwiek do podstawienia.
Nazwy z Minecrafta zapisuje się na dwa sposoby, zależnie od tego, co
oznaczają. Przedmiot nazywa się w obu zapisach
(DIAMOND_SWORD lub diamond_sword).
Dźwięk lub efekt nazywa się swoim kluczem
Minecrafta (entity.player.levelup, speed), a nie
stałą Bukkit: klucz trafia do klienta w niezmienionej postaci i przetrwa
zmiany wersji.
Czego gramatyka nie pozwala
Jest zamknięta i to trzeba powiedzieć jako pierwsze. To, co znajduje się w katalogu, jest możliwe do zrobienia, reszta jest niemożliwa i taka pozostanie: nie ma żadnego sposobu na wykonanie dowolnego kodu Java. Dzięki temu studio działa natychmiast (nie ma nic do skompilowania), a specyfikacja jest bezpieczna do zainstalowania, bez względu na to, skąd pochodzi.
Warunki nie zagnieżdżają się: to świadome ograniczenie
wersji 1, drzewo logiczne źle się rysuje w klockach, a all /
any / not niemal zawsze wystarczają. Jedynym
wyjściem jest akcja „wykonaj polecenie jako konsola”, która na danym serwerze
ma dokładnie taką moc, jak konsola, ani więcej, ani mniej.
Wyzwalacze
To, co uruchamia regułę, oraz zmienne, które każdy z nich dostarcza szablonom.
player.join
Gracz dołącza
W momencie, gdy gracz wchodzi na serwer, po wczytaniu jego świata.
udostępnia:
{player} Gracz, który dołącza,
{world} Świat, do którego trafia
player.quit
Gracz wychodzi
W momencie, gdy gracz opuszcza serwer. Jest jeszcze osiągalny, ale każda wysłana wiadomość zostaje utracona.
udostępnia:
{player} Gracz, który wychodzi,
{world} Świat, który opuszcza
player.chat
Gracz pisze na czacie
można anulować
Zanim wiadomość zostanie rozesłana do innych graczy. Można anulować: wtedy wiadomości nie zobaczy nikt.
udostępnia:
{player} Autor wiadomości,
{world} Jego świat,
{message} Napisana wiadomość
player.death
Gracz ginie
W chwili śmierci gracza, przed ekranem odrodzenia.
udostępnia:
{player} Zmarły gracz,
{world} Jego świat,
{killer} Odpowiedzialny gracz, puste, jeśli go nie ma
player.respawn
Gracz się odradza
Gdy gracz wraca do gry po śmierci.
udostępnia:
{player} Gracz, który się odradza,
{world} Świat odrodzenia
block.break
Gracz niszczy blok
można anulować
Zanim blok zniknie. Można anulować: blok zostaje na miejscu.
udostępnia:
{player} Gracz, który niszczy,
{world} Jego świat,
{block} Typ bloku (DIAMOND_ORE),
{x} Współrzędna X bloku,
{y} Wysokość bloku,
{z} Współrzędna Z bloku
block.place
Gracz stawia blok
można anulować
Zanim blok zostanie postawiony. Można anulować: blok nie zostaje postawiony.
udostępnia:
{player} Gracz, który stawia,
{world} Jego świat,
{block} Typ postawionego bloku,
{x} Współrzędna X bloku,
{y} Wysokość bloku,
{z} Współrzędna Z bloku
command
Gracz wpisuje polecenie
Tworzy nowe polecenie na serwerze. ⚠️ Polecenie o nazwie, która wcześniej nie istniała, pojawia się dopiero po ponownym uruchomieniu serwera: Paper przyjmuje nowe nazwy poleceń tylko przy starcie. Treść reguły natomiast wczytuje się na bieżąco, bez restartu.
-
nametext Nazwa polecenia, bez ukośnika -
descriptiontext opcjonalne Opis wyświetlany w pomocy -
permissiontext opcjonalne Wymagane uprawnienie, puste dla wszystkich
udostępnia:
{player} Gracz, który wpisał polecenie,
{world} Jego świat,
{args} To, co następuje po poleceniu, w niezmienionej postaci
schedule.repeat
Co N sekund
Powtarza regułę, dopóki serwer działa. Nie dotyczy żadnego gracza: da się użyć tylko akcji, które go nie wymagają.
-
secondsinteger [1..86400] Odstęp w sekundach
Warunki
To, co filtruje. Od zera do 20 na regułę, łączone przez match.
player.has_permission
Gracz ma uprawnienie
wymaga gracza
-
permissiontext Uprawnienie
player.is_op
Gracz jest operatorem
wymaga gracza
player.in_world
Gracz jest w świecie
wymaga gracza
-
worldtext Nazwa świata
player.health_below
Gracz ma mniej niż X serc
wymaga gracza
W punktach zdrowia: 20 punktów to 10 serc.
-
valuenumber [0..1024] Punkty zdrowia
text.contains
Tekst zawiera
-
texttext Sprawdzany tekst -
searchtext Czego szukamy -
ignoreCaseboolean opcjonalne, domyślnie:trueIgnoruj wielkość liter
text.equals
Tekst jest równy
-
texttext Sprawdzany tekst -
valuetext Oczekiwana wartość -
ignoreCaseboolean opcjonalne, domyślnie:trueIgnoruj wielkość liter
number.compare
Porównaj dwie liczby
Obie strony to szablony: {y} porównuje się z 62 bez pisania kodu.
-
lefttext Lewa strona -
operator< | <= | == | != | >= | > Porównanie -
righttext Prawa strona
chance
Losowo, przez X % czasu
-
percentnumber [0..100] Procent
Akcje
To, co się dzieje. Od jednej do 50 na regułę, wykonywane po kolei.
player.send_message
Wyślij wiadomość do gracza
wymaga gracza
-
messagetext Wiadomość
player.send_actionbar
Pokaż wiadomość nad paskiem akcji
wymaga gracza
-
messagetext Wiadomość
player.send_title
Pokaż duży tytuł
wymaga gracza
-
titletext Tytuł -
subtitletext opcjonalne Podtytuł -
fadeInnumber [0..60] opcjonalne, domyślnie:0.5Pojawianie się w sekundach -
staynumber [0..600] opcjonalne, domyślnie:3Czas trwania w sekundach -
fadeOutnumber [0..60] opcjonalne, domyślnie:0.5Znikanie w sekundach
player.play_sound
Odtwórz dźwięk dla gracza
wymaga gracza
-
soundidentifier Dźwięk, jako klucz Minecrafta (entity.player.levelup) -
volumenumber [0..10] opcjonalne, domyślnie:1Głośność -
pitchnumber [0.5..2] opcjonalne, domyślnie:1Wysokość dźwięku
player.give_item
Daj graczowi przedmiot
wymaga gracza
To, co nie mieści się w ekwipunku, spada na ziemię pod jego nogami.
-
materialidentifier Przedmiot (DIAMOND_SWORD) -
amountinteger [1..2304] opcjonalne, domyślnie:1Ilość
player.give_effect
Nałóż efekt na gracza
wymaga gracza
-
effectidentifier Efekt, jako klucz Minecrafta (night_vision) -
secondsinteger [1..86400] opcjonalne, domyślnie:10Czas trwania w sekundach -
amplifierinteger [0..255] opcjonalne, domyślnie:0Poziom, od 0
player.heal
Ulecz gracza
wymaga gracza
Pełne zdrowie, najedzenie do syta.
player.teleport
Teleportuj gracza
wymaga gracza
-
xnumber [-30000000..30000000] Współrzędna X -
ynumber [-512..1024] Wysokość -
znumber [-30000000..30000000] Współrzędna Z -
worldtext opcjonalne Świat, puste dla świata gracza
player.kick
Wyrzuć gracza
wymaga gracza
-
reasontext Wyświetlany powód
broadcast
Ogłoś na cały serwer
-
messagetext Wiadomość
server.run_command
Wykonaj polecenie jako konsola
Furtka wyjściowa gramatyki: wszystko, co potrafi konsola. To pełna władza konsoli nad tym serwerem, więc pisz to świadomie.
-
commandtext Polecenie, bez ukośnika
cancel_event
Anuluj to, co właśnie się stało
wymaga wyzwalacza, który można anulować
Istnieje tylko przy anulowalnych wyzwalaczach: wiadomość nie zostaje rozesłana, blok nie zostaje zniszczony.
log
Zapisz w konsoli serwera
-
messagetext Wiadomość -
levelinfo | warn opcjonalne, domyślnie:infoPoziom
delay
Poczekaj
Wstrzymuje kolejne akcje reguły. Reguła, która czeka, nie może już niczego anulować: „Anuluj” musi wystąpić wcześniej.
-
secondsnumber [0.05..3600] Sekundy
Opublikowane pliki
JSON Schema jest generowany z katalogu przy każdym wdrożeniu i stanowi referencję czytelną dla maszyny: edytor tekstu, który się nim posługuje, podpowiada identyfikatory i odrzuca nieznane parametry.
- plugin-1.schema.json : schemat, w formacie JSON Schema draft 2020-12.
- catalogue-1.json : sam katalog, źródło, z którego wywodzi się wszystko inne (walidator, schemat, klocki edytora i silnik Java).
Katalog zawiera etykiety w języku francuskim, który jest językiem źródłowym. Same identyfikatory nie są tłumaczone: to one są wykonywane przez silnik.
Co się dzieje, gdy gramatyka się zmienia
Dodanie wyzwalacza, warunku lub akcji niczego nie psuje: specyfikacja
napisana wcześniej dalej działa. Zmiana łamiąca kompatybilność
zwiększa natomiast specVersion, a silnik odmawia wtedy wykonania
tego, czego nie rozumie, zamiast wykonać to błędnie, informując o tym w
konsoli serwera.
Powyższe adresy URL noszą ten numer:
plugin-1.schema.json zawsze będzie oznaczać tę
właśnie gramatykę, nawet gdy pewnego dnia powstanie kolejna.