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.
-
nametext Nombre del comando, sin la barra -
descriptiontext opcional Descripción mostrada en la ayuda -
permissiontext 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.
-
secondsinteger [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
-
permissiontext Permiso
player.is_op
El jugador es operador
necesita un jugador
player.in_world
El jugador está en el mundo
necesita un jugador
-
worldtext 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.
-
valuenumber [0..1024] Puntos de vida
text.contains
Un texto contiene
-
texttext Texto examinado -
searchtext Lo que se busca en él -
ignoreCaseboolean opcional, por defecto:trueIgnorar mayúsculas y minúsculas
text.equals
Un texto es igual a
-
texttext Texto examinado -
valuetext Valor esperado -
ignoreCaseboolean opcional, por defecto:trueIgnorar mayúsculas y minúsculas
number.compare
Comparar dos números
Ambos lados son plantillas: {y} se compara con 62 sin escribir código.
-
lefttext Lado izquierdo -
operator< | <= | == | != | >= | > Comparación -
righttext Lado derecho
chance
Al azar, X % de las veces
-
percentnumber [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
-
messagetext Mensaje
player.send_actionbar
Mostrar un mensaje encima de la barra de acción
necesita un jugador
-
messagetext Mensaje
player.send_title
Mostrar un título en grande
necesita un jugador
-
titletext Título -
subtitletext opcional Subtítulo -
fadeInnumber [0..60] opcional, por defecto:0.5Aparición en segundos -
staynumber [0..600] opcional, por defecto:3Duración en segundos -
fadeOutnumber [0..60] opcional, por defecto:0.5Desaparición en segundos
player.play_sound
Reproducir un sonido para el jugador
necesita un jugador
-
soundidentifier Sonido, como clave de Minecraft (entity.player.levelup) -
volumenumber [0..10] opcional, por defecto:1Volumen -
pitchnumber [0.5..2] opcional, por defecto:1Tono
player.give_item
Dar un objeto al jugador
necesita un jugador
Lo que no cabe en el inventario cae al suelo a sus pies.
-
materialidentifier Objeto (DIAMOND_SWORD) -
amountinteger [1..2304] opcional, por defecto:1Cantidad
player.give_effect
Aplicar un efecto al jugador
necesita un jugador
-
effectidentifier Efecto, como clave de Minecraft (night_vision) -
secondsinteger [1..86400] opcional, por defecto:10Duración en segundos -
amplifierinteger [0..255] opcional, por defecto:0Nivel, 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
-
xnumber [-30000000..30000000] Coordenada X -
ynumber [-512..1024] Altura -
znumber [-30000000..30000000] Coordenada Z -
worldtext opcional Mundo, vacío para el del jugador
player.kick
Expulsar al jugador
necesita un jugador
-
reasontext Motivo mostrado
broadcast
Anunciar a todo el servidor
-
messagetext 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.
-
commandtext 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
-
messagetext Mensaje -
levelinfo | warn opcional, por defecto:infoNivel
delay
Esperar
Pausa el resto de las acciones de la regla. Una regla que espera ya no puede cancelar nada: «Cancelar» debe ir antes.
-
secondsnumber [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.