La grammatica dei plugin
specVersion 1
Un plugin NimBlock non è codice: è un file JSON che descrive delle regole, letto ed eseguito da un motore Paper scritto una volta per tutti. Questa pagina dice esattamente cosa puoi scriverci, e nient'altro è possibile.
Una regola, e nient'altro che una regola
Tutta la grammatica sta in una frase: quando succede questo, se queste condizioni sono vere, allora fai questo. Un plugin è un elenco di regole, una regola è un trigger, da zero a 20 condizioni e da una a 50 azioni.
Non c'è Java generato da nessuna parte, né alcuna compilazione: il server carica un motore che legge la spec. È questa la proprietà che conta per il seguito, perché una nuova versione di Minecraft si paga una volta sola in questo motore, e non in ognuno dei plugin scritti con esso.
La forma di una spec
{
"specVersion": 1,
"name": "Benvenuto",
"rules": [
{
"id": "benvenuto",
"name": "Messaggio di benvenuto",
"match": "all",
"trigger": { "type": "player.join" },
"conditions": [
{ "type": "player.has_permission", "params": { "permission": "nimblock.vip" } }
],
"actions": [
{ "type": "player.send_message", "params": { "message": "&6Benvenuto {player}!" } }
]
}
]
}
match vale all (predefinito) oppure any, e
una condizione può avere "not": true. enabled: false
mantiene una regola nel file senza attivarla.
La grammatica, in EBNF
Gli elenchi di terminali qui sotto sono generati dal catalogo: sono letteralmente gli identificatori che il motore sa eseguire, non una trascrizione fatta a mano.
<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"
Due cose non si leggono in queste regole di produzione, perché riguardano la
coerenza e non la forma. Una condizione o un'azione che richiede un giocatore si
scrive solo sotto un trigger che ne fornisce uno: «curare il giocatore» non ha
senso sotto «ogni N secondi». E
cancel_event si scrive solo sotto un trigger annullabile. Entrambe
sono rifiutate in fase di scrittura, non di esecuzione.
I template
I parametri di tipo text sono template:
{player} viene sostituito con ciò che il trigger ha
fornito, {{ dà una graffa letterale, e i codici
colore &a &l vengono tradotti.
Una variabile sconosciuta al trigger è un errore, non un testo. Ogni trigger dichiara cosa fornisce (vedi l'elenco più sotto): fuori da questo elenco non c'è nulla da leggere, e un errore di battitura viene rifiutato in fase di scrittura invece di finire nella chat dei giocatori. I parametri di un trigger, invece, non sono template: vengono letti all'avvio del server, prima che ci sia qualcosa da sostituire.
I nomi Minecraft si scrivono in due modi a seconda di cosa designano. Un
oggetto si scrive in entrambe le grafie
(DIAMOND_SWORD oppure diamond_sword). Un
suono o un effetto si scrive con la sua chiave
Minecraft (entity.player.levelup, speed) e non con la
costante Bukkit: la chiave parte così com'è verso il client, e sopravvive ai
cambi di versione.
Ciò che la grammatica non permette
È chiusa, e va detto prima di tutto il resto. Ciò che compare nel catalogo è fattibile, il resto è impossibile e lo resterà: non c'è alcun modo di eseguire Java arbitrario. È questo che rende lo studio istantaneo (niente da compilare) e una spec innocua da installare, qualunque sia la sua provenienza.
Le condizioni non si annidano: è un limite assunto
consapevolmente della versione 1, un albero booleano si disegna male a blocchi, e
all / any / not bastano quasi sempre.
L'unica via d'uscita è l'azione «esegui un comando come console», che su quel
server ha esattamente il potere della console, né più né meno.
I trigger
Cosa fa scattare una regola, e le variabili che ciascuno fornisce ai template.
player.join
Un giocatore si connette
Nel momento in cui il giocatore entra nel server, dopo il caricamento del suo mondo.
fornisce:
{player} Il giocatore che si connette,
{world} Il mondo in cui arriva
player.quit
Un giocatore si disconnette
Nel momento in cui il giocatore lascia il server. È ancora raggiungibile, ma ogni messaggio inviato va perso.
fornisce:
{player} Il giocatore che se ne va,
{world} Il mondo che lascia
player.chat
Un giocatore scrive in chat
annullabile
Prima che il messaggio venga distribuito agli altri giocatori. Annullabile: il messaggio non viene quindi visto da nessuno.
fornisce:
{player} L'autore del messaggio,
{world} Il suo mondo,
{message} Il messaggio scritto
player.death
Un giocatore muore
Alla morte del giocatore, prima della schermata di ricomparsa.
fornisce:
{player} Il giocatore morto,
{world} Il suo mondo,
{killer} Il giocatore responsabile, vuoto se non ce n'è uno
player.respawn
Un giocatore ricompare
Quando il giocatore torna in gioco dopo la sua morte.
fornisce:
{player} Il giocatore che ricompare,
{world} Il mondo di ricomparsa
block.break
Un giocatore distrugge un blocco
annullabile
Prima che il blocco scompaia. Annullabile: il blocco resta al suo posto.
fornisce:
{player} Il giocatore che distrugge,
{world} Il suo mondo,
{block} Il tipo di blocco (DIAMOND_ORE),
{x} Coordinata X del blocco,
{y} Altezza del blocco,
{z} Coordinata Z del blocco
block.place
Un giocatore piazza un blocco
annullabile
Prima che il blocco venga piazzato. Annullabile: il blocco non viene piazzato.
fornisce:
{player} Il giocatore che piazza,
{world} Il suo mondo,
{block} Il tipo di blocco piazzato,
{x} Coordinata X del blocco,
{y} Altezza del blocco,
{z} Coordinata Z del blocco
command
Un giocatore digita un comando
Crea un nuovo comando sul server. ⚠️ Un comando il cui nome non esisteva ancora appare solo al riavvio del server: Paper accetta nuovi nomi di comandi solo all'avvio. Il contenuto della regola, invece, si ricarica a caldo.
-
nametext Nome del comando, senza la barra -
descriptiontext facoltativo Descrizione mostrata nell'aiuto -
permissiontext facoltativo Permesso richiesto, vuoto per tutti
fornisce:
{player} Il giocatore che ha digitato il comando,
{world} Il suo mondo,
{args} Ciò che segue il comando, così com'è
schedule.repeat
Ogni N secondi
Ripete la regola finché il server è in funzione. Nessun giocatore è coinvolto: sono utilizzabili solo le azioni che non ne richiedono uno.
-
secondsinteger [1..86400] Intervallo in secondi
Le condizioni
Ciò che filtra. Da zero a 20 per regola, combinate tramite match.
player.has_permission
Il giocatore ha il permesso
richiede un giocatore
-
permissiontext Permesso
player.is_op
Il giocatore è operatore
richiede un giocatore
player.in_world
Il giocatore è nel mondo
richiede un giocatore
-
worldtext Nome del mondo
player.health_below
Il giocatore ha meno di X cuori
richiede un giocatore
In punti vita: 20 punti equivalgono a 10 cuori.
-
valuenumber [0..1024] Punti vita
text.contains
Un testo contiene
-
texttext Testo esaminato -
searchtext Cosa cercare -
ignoreCaseboolean facoltativo, predefinito:trueIgnora maiuscole e minuscole
text.equals
Un testo è uguale a
-
texttext Testo esaminato -
valuetext Valore atteso -
ignoreCaseboolean facoltativo, predefinito:trueIgnora maiuscole e minuscole
number.compare
Confronta due numeri
Entrambi i membri sono modelli: {y} si confronta con 62 senza scrivere codice.
-
lefttext Membro sinistro -
operator< | <= | == | != | >= | > Confronto -
righttext Membro destro
chance
A caso, X % delle volte
-
percentnumber [0..100] Percentuale
Le azioni
Ciò che accade. Da una a 50 per regola, eseguite in ordine.
player.send_message
Invia un messaggio al giocatore
richiede un giocatore
-
messagetext Messaggio
player.send_actionbar
Mostra un messaggio sopra la barra delle azioni
richiede un giocatore
-
messagetext Messaggio
player.send_title
Mostra un titolo in grande
richiede un giocatore
-
titletext Titolo -
subtitletext facoltativo Sottotitolo -
fadeInnumber [0..60] facoltativo, predefinito:0.5Comparsa in secondi -
staynumber [0..600] facoltativo, predefinito:3Durata in secondi -
fadeOutnumber [0..60] facoltativo, predefinito:0.5Scomparsa in secondi
player.play_sound
Riproduci un suono per il giocatore
richiede un giocatore
-
soundidentifier Suono, come chiave Minecraft (entity.player.levelup) -
volumenumber [0..10] facoltativo, predefinito:1Volume -
pitchnumber [0.5..2] facoltativo, predefinito:1Tono
player.give_item
Dai un oggetto al giocatore
richiede un giocatore
Quello che non entra nell'inventario cade a terra ai suoi piedi.
-
materialidentifier Oggetto (DIAMOND_SWORD) -
amountinteger [1..2304] facoltativo, predefinito:1Quantità
player.give_effect
Applica un effetto al giocatore
richiede un giocatore
-
effectidentifier Effetto, come chiave Minecraft (night_vision) -
secondsinteger [1..86400] facoltativo, predefinito:10Durata in secondi -
amplifierinteger [0..255] facoltativo, predefinito:0Livello, a partire da 0
player.heal
Cura il giocatore
richiede un giocatore
Vita al massimo, fame saziata.
player.teleport
Teletrasporta il giocatore
richiede un giocatore
-
xnumber [-30000000..30000000] Coordinata X -
ynumber [-512..1024] Altezza -
znumber [-30000000..30000000] Coordinata Z -
worldtext facoltativo Mondo, vuoto per quello del giocatore
player.kick
Espelli il giocatore
richiede un giocatore
-
reasontext Motivo mostrato
broadcast
Annuncia a tutto il server
-
messagetext Messaggio
server.run_command
Esegui un comando come console
La via d'uscita della grammatica: tutto ciò che la console sa fare. È il pieno potere della console su questo server, quindi scrivilo sapendo cosa stai facendo.
-
commandtext Comando, senza la barra
cancel_event
Annulla ciò che è appena successo
richiede un trigger annullabile
Esiste solo sui trigger annullabili: il messaggio non viene distribuito, il blocco non viene distrutto.
log
Scrivi nella console del server
-
messagetext Messaggio -
levelinfo | warn facoltativo, predefinito:infoLivello
delay
Attendi
Mette in pausa il resto delle azioni della regola. Una regola che attende non può più annullare nulla: "Annulla" deve venire prima.
-
secondsnumber [0.05..3600] Secondi
I file pubblicati
Il JSON Schema viene generato dal catalogo a ogni deploy, ed è il riferimento leggibile da una macchina: un editor di testo che lo segue completa gli identificatori e rifiuta i parametri sconosciuti.
- plugin-1.schema.json : lo schema, in JSON Schema draft 2020-12.
- catalogue-1.json : il catalogo stesso, la fonte da cui deriva tutto il resto (il validatore, lo schema, i blocchi dell'editor e il motore Java).
Il catalogo porta le etichette in francese, che è la lingua sorgente. Gli identificatori, invece, non si traducono: sono loro che il motore esegue.
Cosa succede quando la grammatica cambia
Aggiungere un trigger, una condizione o un'azione non rompe nulla: una spec
scritta prima continua a funzionare. Una modifica incompatibile,
invece, incrementa specVersion, e il motore rifiuta allora ciò che
non capisce invece di eseguirlo in modo sbagliato, dicendolo nella console del
server.
Gli URL qui sopra portano questo numero: plugin-1.schema.json
designerà sempre questa grammatica, anche il giorno in cui ne esisterà un'altra.