La grammaire des plugins
specVersion 1
Un plugin NimBlock n'est pas du code : c'est un fichier JSON qui décrit des règles, lu et exécuté par un moteur Paper écrit une fois pour tout le monde. Cette page dit ce qu'on a le droit d'y écrire, exactement, et rien d'autre n'est possible.
Une règle, et rien qu'une règle
Toute la grammaire tient dans une phrase : quand ceci arrive, si ces conditions tiennent, alors faire cela. Un plugin est une liste de règles, une règle est un déclencheur, zéro à 20 conditions et une à 50 actions.
Il n'y a pas de Java engendré quelque part, ni de compilation : le serveur charge un moteur qui lit la spec. C'est la propriété qui compte pour la suite, parce qu'une nouvelle version de Minecraft se paie une fois dans ce moteur, et pas dans chacun des plugins écrits avec.
La forme d'une spec
{
"specVersion": 1,
"name": "Bienvenue",
"rules": [
{
"id": "accueil",
"name": "Message d'accueil",
"match": "all",
"trigger": { "type": "player.join" },
"conditions": [
{ "type": "player.has_permission", "params": { "permission": "nimblock.vip" } }
],
"actions": [
{ "type": "player.send_message", "params": { "message": "&6Bienvenue {player} !" } }
]
}
]
}
match vaut all (défaut) ou any, et une
condition peut porter "not": true. enabled: false garde
une règle dans le fichier sans l'activer.
La grammaire, en EBNF
Les listes de terminaux ci-dessous sont engendrées depuis le catalogue : ce sont littéralement les identifiants que le moteur sait exécuter, pas une transcription faite à la main.
<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"
Deux choses ne se lisent pas dans ces règles de production, parce qu'elles portent
sur la cohérence et non sur la forme. Une condition ou une action qui exige un
joueur ne s'écrit que sous un déclencheur qui en pose un : « soigner le joueur »
n'a pas de sens sous « toutes les N secondes ». Et
cancel_event ne s'écrit que sous un déclencheur annulable. Les deux
sont refusés à l'écriture, pas à l'exécution.
Les gabarits
Les paramètres de type text sont des gabarits :
{player} y est remplacé par ce que le déclencheur a
posé, {{ donne une accolade littérale, et les codes
couleur &a &l sont traduits.
Une variable inconnue du déclencheur est une erreur, pas un texte. Chaque déclencheur déclare ce qu'il pose (voir la liste plus bas) : hors de cette liste, il n'y a rien à lire, et une faute de frappe est refusée à l'écriture plutôt que d'atterrir dans le chat des joueurs. Les paramètres d'un déclencheur, eux, ne sont pas des gabarits : ils sont lus au démarrage du serveur, avant qu'il n'y ait quoi que ce soit à remplacer.
Les noms Minecraft s'écrivent de deux façons selon ce qu'ils désignent. Un
objet se nomme dans les deux graphies
(DIAMOND_SWORD ou diamond_sword). Un son
ou un effet se nomme par sa clé Minecraft
(entity.player.levelup, speed) et pas par la constante
Bukkit : la clé part telle quelle au client, elle survit aux versions.
Ce que la grammaire ne permet pas
Elle est fermée, et ça se dit avant le reste. Ce qui figure dans le catalogue est faisable, le reste est impossible, et le restera : il n'y a aucun moyen d'exécuter du Java arbitraire. C'est ce qui rend le studio instantané (rien à compiler) et une spec inoffensive à installer, quelle que soit sa provenance.
Les conditions ne s'imbriquent pas : c'est une limite assumée de
la version 1, un arbre booléen se dessine mal en briques, et
all / any / not suffisent presque toujours.
La seule porte de sortie est l'action « exécuter une commande en tant que
console », qui a sur ce serveur-là le pouvoir de la console, ni plus ni moins.
Les déclencheurs
Ce qui déclenche une règle, et les variables que chacun pose pour les gabarits.
player.join
Un joueur se connecte
Au moment où le joueur entre sur le serveur, après le chargement de son monde.
pose :
{player} Le joueur qui se connecte,
{world} Le monde où il arrive
player.quit
Un joueur se déconnecte
Au moment où le joueur quitte le serveur. Il est encore joignable, mais tout envoi de message est perdu.
pose :
{player} Le joueur qui part,
{world} Le monde qu'il quitte
player.chat
Un joueur écrit dans le chat
annulable
Avant que le message ne soit distribué aux autres joueurs. Annulable : le message n'est alors vu de personne.
pose :
{player} L'auteur du message,
{world} Son monde,
{message} Le message écrit
player.death
Un joueur meurt
À la mort du joueur, avant l'écran de réapparition.
pose :
{player} Le joueur mort,
{world} Son monde,
{killer} Le joueur responsable, vide s'il n'y en a pas
player.respawn
Un joueur réapparaît
Quand le joueur revient en jeu après sa mort.
pose :
{player} Le joueur qui réapparaît,
{world} Le monde de réapparition
block.break
Un joueur casse un bloc
annulable
Avant que le bloc ne disparaisse. Annulable : le bloc reste en place.
pose :
{player} Le joueur qui casse,
{world} Son monde,
{block} Le type de bloc (DIAMOND_ORE),
{x} Abscisse du bloc,
{y} Hauteur du bloc,
{z} Ordonnée du bloc
block.place
Un joueur pose un bloc
annulable
Avant que le bloc ne soit posé. Annulable : le bloc n'est pas posé.
pose :
{player} Le joueur qui pose,
{world} Son monde,
{block} Le type de bloc posé,
{x} Abscisse du bloc,
{y} Hauteur du bloc,
{z} Ordonnée du bloc
command
Un joueur tape une commande
Crée une nouvelle commande sur le serveur. ⚠️ Une commande dont le nom n'existait pas encore n'apparaît qu'au redémarrage du serveur : Paper n'accepte de nouveaux noms de commandes qu'à son démarrage. Le contenu de la règle, lui, se recharge à chaud.
-
nametext Nom de la commande, sans le slash -
descriptiontext facultatif Description affichée dans l'aide -
permissiontext facultatif Permission exigée, vide pour tout le monde
pose :
{player} Le joueur qui a tapé la commande,
{world} Son monde,
{args} Ce qui suit la commande, tel quel
schedule.repeat
Toutes les N secondes
Répète la règle tant que le serveur tourne. Aucun joueur n'est concerné : seules les actions qui n'en demandent pas sont utilisables.
-
secondsinteger [1..86400] Intervalle en secondes
Les conditions
Ce qui filtre. Zéro à 20 par règle, combinées par match.
player.has_permission
Le joueur a la permission
exige un joueur
-
permissiontext Permission
player.is_op
Le joueur est opérateur
exige un joueur
player.in_world
Le joueur est dans le monde
exige un joueur
-
worldtext Nom du monde
player.health_below
Le joueur a moins de X cœurs
exige un joueur
En points de vie : 20 points valent 10 cœurs.
-
valuenumber [0..1024] Points de vie
text.contains
Un texte contient
-
texttext Texte examiné -
searchtext Ce qu'on y cherche -
ignoreCaseboolean facultatif, défaut :trueIgnorer la casse
text.equals
Un texte est égal à
-
texttext Texte examiné -
valuetext Valeur attendue -
ignoreCaseboolean facultatif, défaut :trueIgnorer la casse
number.compare
Comparer deux nombres
Les deux membres sont des gabarits : {y} se compare à 62 sans écrire de code.
-
lefttext Membre de gauche -
operator< | <= | == | != | >= | > Comparaison -
righttext Membre de droite
chance
Au hasard, X % du temps
-
percentnumber [0..100] Pourcentage
Les actions
Ce qui se passe. Une à 50 par règle, exécutées dans l'ordre.
player.send_message
Envoyer un message au joueur
exige un joueur
-
messagetext Message
player.send_actionbar
Afficher un message au-dessus de la barre d'action
exige un joueur
-
messagetext Message
player.send_title
Afficher un titre en grand
exige un joueur
-
titletext Titre -
subtitletext facultatif Sous-titre -
fadeInnumber [0..60] facultatif, défaut :0.5Apparition en secondes -
staynumber [0..600] facultatif, défaut :3Durée en secondes -
fadeOutnumber [0..60] facultatif, défaut :0.5Disparition en secondes
player.play_sound
Jouer un son pour le joueur
exige un joueur
-
soundidentifier Son, en clé Minecraft (entity.player.levelup) -
volumenumber [0..10] facultatif, défaut :1Volume -
pitchnumber [0.5..2] facultatif, défaut :1Hauteur
player.give_item
Donner un objet au joueur
exige un joueur
Ce qui ne tient pas dans l'inventaire tombe au sol à ses pieds.
-
materialidentifier Objet (DIAMOND_SWORD) -
amountinteger [1..2304] facultatif, défaut :1Quantité
player.give_effect
Appliquer un effet au joueur
exige un joueur
-
effectidentifier Effet, en clé Minecraft (night_vision) -
secondsinteger [1..86400] facultatif, défaut :10Durée en secondes -
amplifierinteger [0..255] facultatif, défaut :0Niveau, à partir de 0
player.heal
Soigner le joueur
exige un joueur
Vie au maximum, faim rassasiée.
player.teleport
Téléporter le joueur
exige un joueur
-
xnumber [-30000000..30000000] Abscisse -
ynumber [-512..1024] Hauteur -
znumber [-30000000..30000000] Ordonnée -
worldtext facultatif Monde, vide pour celui du joueur
player.kick
Expulser le joueur
exige un joueur
-
reasontext Motif affiché
broadcast
Annoncer à tout le serveur
-
messagetext Message
server.run_command
Exécuter une commande en tant que console
La porte de sortie de la grammaire : tout ce que la console sait faire. Le pouvoir de la console sur ce serveur, donc à n'écrire qu'en connaissance de cause.
-
commandtext Commande, sans le slash
cancel_event
Annuler ce qui vient de se passer
exige un déclencheur annulable
N'existe que sur les déclencheurs annulables : le message n'est pas distribué, le bloc n'est pas cassé.
log
Écrire dans la console du serveur
-
messagetext Message -
levelinfo | warn facultatif, défaut :infoNiveau
delay
Attendre
Met en pause la suite des actions de la règle. Une règle qui attend ne peut plus rien annuler : « Annuler » doit venir avant.
-
secondsnumber [0.05..3600] Secondes
Les fichiers publiés
Le JSON Schema est engendré depuis le catalogue, à chaque déploiement, et c'est la référence lisible par une machine : un éditeur de texte qui le suit complète les identifiants et refuse les paramètres inconnus.
- plugin-1.schema.json : le schéma, en JSON Schema draft 2020-12.
- catalogue-1.json : le catalogue lui-même, la source dont tout le reste dérive (le validateur, le schéma, les briques de l'éditeur et le moteur Java).
Le catalogue porte les libellés en français, qui est la langue source. Les identifiants, eux, ne se traduisent pas : ce sont eux que le moteur exécute.
Ce qui se passe quand la grammaire change
Ajouter un déclencheur, une condition ou une action ne casse rien : une spec
écrite avant continue de tourner. Une rupture, elle, incrémente
specVersion, et le moteur refuse alors ce qu'il ne comprend pas
plutôt que de l'exécuter de travers, en le disant dans la console du serveur.
Les URL ci-dessus portent ce numéro : plugin-1.schema.json
désignera toujours cette grammaire-ci, même le jour où il en existe une autre.