NimBlock Iniciar sesión

La gramática de los plugins

specVersion 1

Un plugin de NimBlock no es código: es un archivo JSON que describe reglas, leído y ejecutado por un motor de Paper escrito una sola vez para todos. Esta página dice exactamente qué se puede escribir en él, y nada más es posible.

Una regla, y solo una regla

Toda la gramática cabe en una frase: cuando esto ocurre, si estas condiciones se cumplen, entonces hacer esto. Un plugin es una lista de reglas, una regla es un disparador, de cero a 20 condiciones y de una a 50 acciones.

No hay Java generado en ningún sitio, ni compilación: el servidor carga un motor que lee la spec. Es la propiedad que importa para lo que sigue, porque una nueva versión de Minecraft se paga una vez en ese motor, y no en cada uno de los plugins escritos con él.

La forma de una spec

{
    "specVersion": 1,
    "name": "Bienvenida",
    "rules": [
        {
            "id": "bienvenida",
            "name": "Mensaje de bienvenida",
            "match": "all",
            "trigger": { "type": "player.join" },
            "conditions": [
                { "type": "player.has_permission", "params": { "permission": "nimblock.vip" } }
            ],
            "actions": [
                { "type": "player.send_message", "params": { "message": "&6¡Bienvenido {player}!" } }
            ]
        }
    ]
}

match vale all (por defecto) o any, y una condición puede llevar "not": true. enabled: false mantiene una regla en el archivo sin activarla.

La gramática, en EBNF

Las listas de terminales de abajo se generan a partir del catálogo: son literalmente los identificadores que el motor sabe ejecutar, no una transcripción hecha 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"

Dos cosas no se leen en estas reglas de producción, porque tienen que ver con la coherencia y no con la forma. Una condición o una acción que requiere un jugador solo se puede escribir bajo un disparador que proporcione uno: «curar al jugador» no tiene sentido bajo «cada N segundos». Y cancel_event solo se escribe bajo un disparador cancelable. Ambas se rechazan al escribir, no al ejecutar.

Las plantillas

Los parámetros de tipo text son plantillas: {player} se sustituye por lo que puso el disparador, {{ da una llave literal, y los códigos de color &a &l se traducen.

Una variable que el disparador desconoce es un error, no un texto. Cada disparador declara lo que proporciona (ver la lista más abajo): fuera de esa lista no hay nada que leer, y un error de tecleo se rechaza al escribir en vez de terminar en el chat de los jugadores. Los parámetros de un disparador, en cambio, no son plantillas: se leen al arrancar el servidor, antes de que haya algo que sustituir.

Los nombres de Minecraft se escriben de dos formas según lo que designan. Un objeto se nombra con cualquiera de las dos grafías (DIAMOND_SWORD o diamond_sword). Un sonido o un efecto se nombran por su clave de Minecraft (entity.player.levelup, speed) y no por la constante de Bukkit: la clave llega tal cual al cliente y sobrevive a los cambios de versión.

Lo que la gramática no permite

Es cerrada, y eso hay que decirlo antes que lo demás. Lo que figura en el catálogo es posible, el resto es imposible y lo seguirá siendo: no hay ninguna forma de ejecutar Java arbitrario. Eso es lo que hace que el estudio sea instantáneo (nada que compilar) y que una spec sea inofensiva de instalar, venga de donde venga.

Las condiciones no se anidan: es un límite asumido de la versión 1, un árbol booleano se dibuja mal con bloques, y all / any / not casi siempre bastan. La única salida es la acción «ejecutar un comando como consola», que en ese servidor tiene exactamente el poder de la consola, ni más ni menos.

Los disparadores

Lo que dispara una regla, y las variables que cada uno proporciona a las plantillas.

player.join Un jugador se conecta

En el momento en que el jugador entra al servidor, después de cargar su mundo.

aporta: {player} El jugador que se conecta, {world} El mundo al que llega

player.quit Un jugador se desconecta

En el momento en que el jugador abandona el servidor. Todavía se le puede contactar, pero cualquier mensaje enviado se pierde.

aporta: {player} El jugador que se va, {world} El mundo que abandona

player.chat Un jugador escribe en el chat cancelable

Antes de que el mensaje se envíe a los demás jugadores. Cancelable: si se cancela, nadie ve el mensaje.

aporta: {player} El autor del mensaje, {world} Su mundo, {message} El mensaje escrito

player.death Un jugador muere

En el momento en que muere el jugador, antes de la pantalla de reaparición.

aporta: {player} El jugador que murió, {world} Su mundo, {killer} El jugador responsable, vacío si no hay ninguno

player.respawn Un jugador reaparece

Cuando el jugador vuelve al juego después de morir.

aporta: {player} El jugador que reaparece, {world} El mundo de reaparición

block.break Un jugador rompe un bloque cancelable

Antes de que el bloque desaparezca. Cancelable: el bloque permanece en su sitio.

aporta: {player} El jugador que rompe, {world} Su mundo, {block} El tipo de bloque (DIAMOND_ORE), {x} Coordenada X del bloque, {y} Altura del bloque, {z} Coordenada Z del bloque

block.place Un jugador coloca un bloque cancelable

Antes de que se coloque el bloque. Cancelable: el bloque no se coloca.

aporta: {player} El jugador que coloca, {world} Su mundo, {block} El tipo de bloque colocado, {x} Coordenada X del bloque, {y} Altura del bloque, {z} Coordenada Z del bloque

command Un jugador escribe un comando

Crea un comando nuevo en el servidor. ⚠️ Un comando cuyo nombre no existía todavía solo aparece al reiniciar el servidor: Paper solo acepta nombres de comandos nuevos al arrancar. El contenido de la regla, en cambio, se recarga sin reiniciar.

  • name text Nombre del comando, sin la barra
  • description text opcional Descripción mostrada en la ayuda
  • permission text opcional Permiso requerido, vacío para todos

aporta: {player} El jugador que escribió el comando, {world} Su mundo, {args} Lo que sigue al comando, tal cual

schedule.repeat Cada N segundos

Repite la regla mientras el servidor esté en marcha. No interviene ningún jugador: solo se pueden usar acciones que no lo necesiten.

  • seconds integer [1..86400] Intervalo en segundos

Las condiciones

Lo que filtra. De cero a 20 por regla, combinadas por match.

player.has_permission El jugador tiene el permiso necesita un jugador
  • permission text Permiso
player.is_op El jugador es operador necesita un jugador
player.in_world El jugador está en el mundo necesita un jugador
  • world text Nombre del mundo
player.health_below El jugador tiene menos de X corazones necesita un jugador

En puntos de vida: 20 puntos equivalen a 10 corazones.

  • value number [0..1024] Puntos de vida
text.contains Un texto contiene
  • text text Texto examinado
  • search text Lo que se busca en él
  • ignoreCase boolean opcional, por defecto: true Ignorar mayúsculas y minúsculas
text.equals Un texto es igual a
  • text text Texto examinado
  • value text Valor esperado
  • ignoreCase boolean opcional, por defecto: true Ignorar mayúsculas y minúsculas
number.compare Comparar dos números

Ambos lados son plantillas: {y} se compara con 62 sin escribir código.

  • left text Lado izquierdo
  • operator < | <= | == | != | >= | > Comparación
  • right text Lado derecho
chance Al azar, X % de las veces
  • percent number [0..100] Porcentaje

Las acciones

Lo que ocurre. De una a 50 por regla, ejecutadas en orden.

player.send_message Enviar un mensaje al jugador necesita un jugador
  • message text Mensaje
player.send_actionbar Mostrar un mensaje encima de la barra de acción necesita un jugador
  • message text Mensaje
player.send_title Mostrar un título en grande necesita un jugador
  • title text Título
  • subtitle text opcional Subtítulo
  • fadeIn number [0..60] opcional, por defecto: 0.5 Aparición en segundos
  • stay number [0..600] opcional, por defecto: 3 Duración en segundos
  • fadeOut number [0..60] opcional, por defecto: 0.5 Desaparición en segundos
player.play_sound Reproducir un sonido para el jugador necesita un jugador
  • sound identifier Sonido, como clave de Minecraft (entity.player.levelup)
  • volume number [0..10] opcional, por defecto: 1 Volumen
  • pitch number [0.5..2] opcional, por defecto: 1 Tono
player.give_item Dar un objeto al jugador necesita un jugador

Lo que no cabe en el inventario cae al suelo a sus pies.

  • material identifier Objeto (DIAMOND_SWORD)
  • amount integer [1..2304] opcional, por defecto: 1 Cantidad
player.give_effect Aplicar un efecto al jugador necesita un jugador
  • effect identifier Efecto, como clave de Minecraft (night_vision)
  • seconds integer [1..86400] opcional, por defecto: 10 Duración en segundos
  • amplifier integer [0..255] opcional, por defecto: 0 Nivel, a partir de 0
player.heal Curar al jugador necesita un jugador

Vida al máximo, hambre saciada.

player.teleport Teletransportar al jugador necesita un jugador
  • x number [-30000000..30000000] Coordenada X
  • y number [-512..1024] Altura
  • z number [-30000000..30000000] Coordenada Z
  • world text opcional Mundo, vacío para el del jugador
player.kick Expulsar al jugador necesita un jugador
  • reason text Motivo mostrado
broadcast Anunciar a todo el servidor
  • message text Mensaje
server.run_command Ejecutar un comando como la consola

La puerta de salida de la gramática: todo lo que la consola sabe hacer. Es el poder de la consola sobre este servidor, así que escríbelo sabiendo lo que haces.

  • command text Comando, sin la barra
cancel_event Cancelar lo que acaba de pasar necesita un evento cancelable

Solo existe en los desencadenantes cancelables: el mensaje no se envía, el bloque no se rompe.

log Escribir en la consola del servidor
  • message text Mensaje
  • level info | warn opcional, por defecto: info Nivel
delay Esperar

Pausa el resto de las acciones de la regla. Una regla que espera ya no puede cancelar nada: «Cancelar» debe ir antes.

  • seconds number [0.05..3600] Segundos

Los archivos publicados

El JSON Schema se genera a partir del catálogo en cada despliegue, y es la referencia legible por una máquina: un editor de texto que lo sigue completa los identificadores y rechaza los parámetros desconocidos.

  • plugin-1.schema.json : el esquema, en JSON Schema draft 2020-12.
  • catalogue-1.json : el catálogo en sí, la fuente de la que deriva todo lo demás (el validador, el esquema, los bloques del editor y el motor Java).

El catálogo lleva las etiquetas en francés, que es la lengua de origen. Los identificadores, en cambio, no se traducen: son ellos los que ejecuta el motor.

Qué pasa cuando la gramática cambia

Añadir un disparador, una condición o una acción no rompe nada: una spec escrita antes sigue funcionando. Un cambio disruptivo, en cambio, incrementa specVersion, y el motor entonces rechaza lo que no entiende en lugar de ejecutarlo mal, y lo dice en la consola del servidor.

Las URL de arriba llevan ese número: plugin-1.schema.json designará siempre esta gramática, incluso el día en que exista otra.