De plugin-grammatica
specVersion 1
Een NimBlock-plugin is geen code: het is een JSON-bestand dat regels beschrijft, gelezen en uitgevoerd door een Paper-engine die één keer voor iedereen is geschreven. Deze pagina zegt precies wat je erin mag schrijven, en niets anders is mogelijk.
Eén regel, en niets dan een regel
De hele grammatica past in één zin: wanneer dit gebeurt, als deze voorwaarden gelden, doe dan dat. Een plugin is een lijst van regels, een regel is één trigger, nul tot 20 voorwaarden en één tot 50 acties.
Er wordt nergens Java gegenereerd, en er wordt niets gecompileerd: de server laadt een engine die de spec leest. Dat is de eigenschap die er later toe doet, want een nieuwe Minecraft-versie wordt maar één keer betaald, in die engine, en niet in elke plugin die er ooit mee is geschreven.
De vorm van een spec
{
"specVersion": 1,
"name": "Welkom",
"rules": [
{
"id": "welkom",
"name": "Welkomstbericht",
"match": "all",
"trigger": { "type": "player.join" },
"conditions": [
{ "type": "player.has_permission", "params": { "permission": "nimblock.vip" } }
],
"actions": [
{ "type": "player.send_message", "params": { "message": "&6Welkom {player}!" } }
]
}
]
}
match is all (standaard) of any, en een
voorwaarde kan "not": true dragen. enabled: false houdt
een regel in het bestand zonder hem te activeren.
De grammatica, in EBNF
De onderstaande lijsten met terminalen worden gegenereerd vanuit de catalogus: het zijn letterlijk de identifiers die de engine kan uitvoeren, geen met de hand gemaakte overschrijving.
<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"
Twee dingen zijn niet in deze productieregels te lezen, omdat ze over samenhang
gaan en niet over vorm. Een voorwaarde of actie die een speler vereist, kan alleen
worden geschreven onder een trigger die er een levert: "genees de speler" heeft
geen betekenis onder "elke N seconden". En cancel_event kan alleen
worden geschreven onder een annuleerbare trigger. Beide worden geweigerd bij het
schrijven, niet bij het uitvoeren.
Sjablonen
Parameters van het type text zijn sjablonen:
{player} wordt daarin vervangen door wat de trigger
heeft geleverd, {{ geeft een letterlijk accolade-teken,
en de kleurcodes &a &l worden vertaald.
Een variabele die de trigger niet kent, is een fout, geen tekst. Elke trigger geeft aan wat hij levert (zie de lijst hieronder): buiten die lijst is er niets te lezen, en een typefout wordt geweigerd bij het schrijven in plaats van in de chat van de spelers terecht te komen. De parameters van een trigger zelf zijn geen sjablonen: ze worden gelezen bij het opstarten van de server, voordat er iets te vervangen valt.
Minecraft-namen worden op twee manieren geschreven, afhankelijk van waar ze naar
verwijzen. Een voorwerp wordt in beide schrijfwijzen genoemd
(DIAMOND_SWORD of diamond_sword). Een geluid
of een effect wordt genoemd via de Minecraft-sleutel
(entity.player.levelup, speed) en niet via de
Bukkit-constante: de sleutel gaat ongewijzigd naar de client en overleeft
versiewisselingen.
Wat de grammatica niet toestaat
Ze is gesloten, en dat wordt eerst gezegd. Wat in de catalogus staat, is mogelijk, de rest is onmogelijk en blijft dat: er is geen enkele manier om willekeurige Java uit te voeren. Dat is wat de studio direct maakt (niets te compileren) en een spec onschadelijk om te installeren, ongeacht de herkomst.
Voorwaarden worden niet genest: dat is een bewuste beperking van
versie 1, een booleaanse boom laat zich slecht tekenen in blokken, en
all / any / not zijn bijna altijd genoeg. De
enige uitweg is de actie "voer een commando uit als console", die op die server
precies de macht van de console heeft, niet meer en niet minder.
Triggers
Wat een regel start, en de variabelen die elke trigger levert voor de sjablonen.
player.join
Een speler verbindt
Op het moment dat de speler de server binnenkomt, nadat de wereld geladen is.
levert:
{player} De speler die verbindt,
{world} De wereld waarin hij aankomt
player.quit
Een speler vertrekt
Op het moment dat de speler de server verlaat. Hij is dan nog bereikbaar, maar elk verzonden bericht gaat verloren.
levert:
{player} De speler die vertrekt,
{world} De wereld die hij verlaat
player.chat
Een speler schrijft in de chat
annuleerbaar
Voordat het bericht aan de andere spelers wordt afgeleverd. Annuleerbaar: het bericht wordt dan door niemand gezien.
levert:
{player} Wie het bericht schreef,
{world} Zijn wereld,
{message} Het geschreven bericht
player.death
Een speler sterft
Bij het overlijden van de speler, vóór het herrijzingsscherm.
levert:
{player} De overleden speler,
{world} Zijn wereld,
{killer} De verantwoordelijke speler, leeg als die er niet is
player.respawn
Een speler herrijst
Wanneer de speler na het overlijden terugkeert in het spel.
levert:
{player} De speler die herrijst,
{world} De herrijzingswereld
block.break
Een speler breekt een blok
annuleerbaar
Voordat het blok verdwijnt. Annuleerbaar: het blok blijft staan.
levert:
{player} De speler die breekt,
{world} Zijn wereld,
{block} Het bloktype (DIAMOND_ORE),
{x} X-coördinaat van het blok,
{y} Hoogte van het blok,
{z} Z-coördinaat van het blok
block.place
Een speler plaatst een blok
annuleerbaar
Voordat het blok geplaatst wordt. Annuleerbaar: het blok wordt niet geplaatst.
levert:
{player} De speler die plaatst,
{world} Zijn wereld,
{block} Het geplaatste bloktype,
{x} X-coördinaat van het blok,
{y} Hoogte van het blok,
{z} Z-coördinaat van het blok
command
Een speler typt een commando
Maakt een nieuw commando aan op de server. ⚠️ Een commando waarvan de naam nog niet bestond, verschijnt pas na een herstart van de server: Paper accepteert nieuwe commandonamen alleen bij het opstarten. De inhoud van de regel wordt daarentegen wél direct herladen.
-
nametext Naam van het commando, zonder de schuine streep -
descriptiontext optioneel Beschrijving die in de help wordt getoond -
permissiontext optioneel Vereiste permissie, leeg voor iedereen
levert:
{player} De speler die het commando typte,
{world} Zijn wereld,
{args} Wat er na het commando volgt, precies zoals getypt
schedule.repeat
Elke N seconden
Herhaalt de regel zolang de server draait. Er is geen speler bij betrokken: alleen acties die er geen nodig hebben, kunnen gebruikt worden.
-
secondsinteger [1..86400] Interval in seconden
Voorwaarden
Wat filtert. Nul tot 20 per regel, gecombineerd via match.
player.has_permission
De speler heeft de permissie
heeft een speler nodig
-
permissiontext Permissie
player.is_op
De speler is een operator
heeft een speler nodig
player.in_world
De speler is in de wereld
heeft een speler nodig
-
worldtext Naam van de wereld
player.health_below
De speler heeft minder dan X harten
heeft een speler nodig
In levenspunten: 20 punten zijn 10 harten.
-
valuenumber [0..1024] Levenspunten
text.contains
Een tekst bevat
-
texttext Onderzochte tekst -
searchtext Wat je erin zoekt -
ignoreCaseboolean optioneel, standaard:trueHoofdletters negeren
text.equals
Een tekst is gelijk aan
-
texttext Onderzochte tekst -
valuetext Verwachte waarde -
ignoreCaseboolean optioneel, standaard:trueHoofdletters negeren
number.compare
Vergelijk twee getallen
Beide kanten zijn sjablonen: {y} wordt vergeleken met 62 zonder code te schrijven.
-
lefttext Linkerkant -
operator< | <= | == | != | >= | > Vergelijking -
righttext Rechterkant
chance
Willekeurig, X % van de tijd
-
percentnumber [0..100] Percentage
Acties
Wat er gebeurt. Eén tot 50 per regel, uitgevoerd in volgorde.
player.send_message
Stuur een bericht naar de speler
heeft een speler nodig
-
messagetext Bericht
player.send_actionbar
Toon een bericht boven de actiebalk
heeft een speler nodig
-
messagetext Bericht
player.send_title
Toon een titel in grote letters
heeft een speler nodig
-
titletext Titel -
subtitletext optioneel Ondertitel -
fadeInnumber [0..60] optioneel, standaard:0.5Verschijnen in seconden -
staynumber [0..600] optioneel, standaard:3Duur in seconden -
fadeOutnumber [0..60] optioneel, standaard:0.5Verdwijnen in seconden
player.play_sound
Speel een geluid af voor de speler
heeft een speler nodig
-
soundidentifier Geluid, als Minecraft-sleutel (entity.player.levelup) -
volumenumber [0..10] optioneel, standaard:1Volume -
pitchnumber [0.5..2] optioneel, standaard:1Toonhoogte
player.give_item
Geef een voorwerp aan de speler
heeft een speler nodig
Wat niet in de inventaris past, valt op de grond aan zijn voeten.
-
materialidentifier Voorwerp (DIAMOND_SWORD) -
amountinteger [1..2304] optioneel, standaard:1Aantal
player.give_effect
Pas een effect toe op de speler
heeft een speler nodig
-
effectidentifier Effect, als Minecraft-sleutel (night_vision) -
secondsinteger [1..86400] optioneel, standaard:10Duur in seconden -
amplifierinteger [0..255] optioneel, standaard:0Niveau, vanaf 0
player.heal
Genees de speler
heeft een speler nodig
Leven op maximum, honger gestild.
player.teleport
Teleporteer de speler
heeft een speler nodig
-
xnumber [-30000000..30000000] X-coördinaat -
ynumber [-512..1024] Hoogte -
znumber [-30000000..30000000] Z-coördinaat -
worldtext optioneel Wereld, leeg voor die van de speler zelf
player.kick
Kick de speler
heeft een speler nodig
-
reasontext Getoonde reden
broadcast
Kondig aan op de hele server
-
messagetext Bericht
server.run_command
Voer een commando uit als de console
De ontsnappingsroute van de grammatica: alles wat de console kan doen. Dat is de volledige macht van de console over deze server, schrijf dit dus alleen met kennis van zaken.
-
commandtext Commando, zonder de schuine streep
cancel_event
Annuleer wat er net gebeurde
heeft een annuleerbare trigger nodig
Bestaat alleen bij annuleerbare triggers: het bericht wordt niet afgeleverd, het blok wordt niet gebroken.
log
Schrijf naar de serverconsole
-
messagetext Bericht -
levelinfo | warn optioneel, standaard:infoNiveau
delay
Wacht
Pauzeert de rest van de acties in de regel. Een regel die wacht, kan niets meer annuleren: "Annuleren" moet daarvoor komen.
-
secondsnumber [0.05..3600] Seconden
De gepubliceerde bestanden
Het JSON Schema wordt bij elke deploy gegenereerd vanuit de catalogus, en het is de machineleesbare referentie: een teksteditor die het volgt, vult identifiers automatisch aan en weigert onbekende parameters.
- plugin-1.schema.json : het schema, in JSON Schema draft 2020-12.
- catalogue-1.json : de catalogus zelf, de bron waar al de rest van afgeleid is (de validator, het schema, de blokken van de editor en de Java-engine).
De catalogus draagt de labels in het Frans, de brontaal. De identifiers zelf worden niet vertaald: zij zijn het wat de engine uitvoert.
Wat er gebeurt als de grammatica verandert
Een trigger, voorwaarde of actie toevoegen breekt niets: een eerder geschreven spec
blijft gewoon draaien. Een breaking change daarentegen verhoogt
specVersion, en de engine weigert dan wat hij niet begrijpt in plaats
van het verkeerd uit te voeren, en meldt dat in de serverconsole.
De URL's hierboven dragen dat nummer: plugin-1.schema.json
zal altijd naar deze grammatica verwijzen, ook op de dag dat er een andere bestaat.