NimBlock Se connecter

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.

  • name text Nom de la commande, sans le slash
  • description text facultatif Description affichée dans l'aide
  • permission text 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.

  • seconds integer [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
  • permission text Permission
player.is_op Le joueur est opérateur exige un joueur
player.in_world Le joueur est dans le monde exige un joueur
  • world text 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.

  • value number [0..1024] Points de vie
text.contains Un texte contient
  • text text Texte examiné
  • search text Ce qu'on y cherche
  • ignoreCase boolean facultatif, défaut : true Ignorer la casse
text.equals Un texte est égal à
  • text text Texte examiné
  • value text Valeur attendue
  • ignoreCase boolean facultatif, défaut : true Ignorer la casse
number.compare Comparer deux nombres

Les deux membres sont des gabarits : {y} se compare à 62 sans écrire de code.

  • left text Membre de gauche
  • operator < | <= | == | != | >= | > Comparaison
  • right text Membre de droite
chance Au hasard, X % du temps
  • percent number [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
  • message text Message
player.send_actionbar Afficher un message au-dessus de la barre d'action exige un joueur
  • message text Message
player.send_title Afficher un titre en grand exige un joueur
  • title text Titre
  • subtitle text facultatif Sous-titre
  • fadeIn number [0..60] facultatif, défaut : 0.5 Apparition en secondes
  • stay number [0..600] facultatif, défaut : 3 Durée en secondes
  • fadeOut number [0..60] facultatif, défaut : 0.5 Disparition en secondes
player.play_sound Jouer un son pour le joueur exige un joueur
  • sound identifier Son, en clé Minecraft (entity.player.levelup)
  • volume number [0..10] facultatif, défaut : 1 Volume
  • pitch number [0.5..2] facultatif, défaut : 1 Hauteur
player.give_item Donner un objet au joueur exige un joueur

Ce qui ne tient pas dans l'inventaire tombe au sol à ses pieds.

  • material identifier Objet (DIAMOND_SWORD)
  • amount integer [1..2304] facultatif, défaut : 1 Quantité
player.give_effect Appliquer un effet au joueur exige un joueur
  • effect identifier Effet, en clé Minecraft (night_vision)
  • seconds integer [1..86400] facultatif, défaut : 10 Durée en secondes
  • amplifier integer [0..255] facultatif, défaut : 0 Niveau, à 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
  • x number [-30000000..30000000] Abscisse
  • y number [-512..1024] Hauteur
  • z number [-30000000..30000000] Ordonnée
  • world text facultatif Monde, vide pour celui du joueur
player.kick Expulser le joueur exige un joueur
  • reason text Motif affiché
broadcast Annoncer à tout le serveur
  • message text 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.

  • command text 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
  • message text Message
  • level info | warn facultatif, défaut : info Niveau
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.

  • seconds number [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.