NimBlock Iniciar sessão

A gramática dos plugins

specVersion 1

Um plugin NimBlock não é código: é um ficheiro JSON que descreve regras, lido e executado por um motor Paper escrito uma vez para todos. Esta página diz exatamente o que podes escrever nele, e mais nada é possível.

Uma regra, e nada mais que uma regra

Toda a gramática cabe numa frase: quando isto acontece, se estas condições se verificam, então fazer aquilo. Um plugin é uma lista de regras, uma regra é um gatilho, zero a 20 condições e uma a 50 ações.

Não há Java gerado em lado nenhum, nem compilação: o servidor carrega um motor que a spec. É essa propriedade que conta a seguir, porque uma nova versão do Minecraft paga-se uma vez nesse motor, e não em cada um dos plugins escritos com ele.

A forma de uma spec

{
    "specVersion": 1,
    "name": "Bem-vindo",
    "rules": [
        {
            "id": "boas-vindas",
            "name": "Mensagem de boas-vindas",
            "match": "all",
            "trigger": { "type": "player.join" },
            "conditions": [
                { "type": "player.has_permission", "params": { "permission": "nimblock.vip" } }
            ],
            "actions": [
                { "type": "player.send_message", "params": { "message": "&6Bem-vindo {player}!" } }
            ]
        }
    ]
}

match vale all (predefinição) ou any, e uma condição pode ter "not": true. enabled: false mantém uma regra no ficheiro sem a ativar.

A gramática, em EBNF

As listas de terminais abaixo são geradas a partir do catálogo: são literalmente os identificadores que o motor sabe executar, não uma transcrição feita à mão.

<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"

Há duas coisas que não se leem nestas regras de produção, porque dizem respeito à coerência e não à forma. Uma condição ou uma ação que exige um jogador só se escreve sob um gatilho que forneça um: «curar o jogador» não faz sentido sob «a cada N segundos». E cancel_event só se escreve sob um gatilho cancelável. Ambas são recusadas na escrita, não na execução.

Os modelos

Os parâmetros do tipo text são modelos: {player} é substituído pelo que o gatilho forneceu, {{ dá uma chaveta literal, e os códigos de cor &a &l são traduzidos.

Uma variável desconhecida do gatilho é um erro, não um texto. Cada gatilho declara o que fornece (ver a lista mais abaixo): fora dessa lista, não há nada para ler, e um erro de escrita é recusado logo na escrita em vez de aterrar no chat dos jogadores. Os parâmetros de um gatilho, esses, não são modelos: são lidos no arranque do servidor, antes de haver seja o que for para substituir.

Os nomes do Minecraft escrevem-se de duas formas consoante o que designam. Um item nomeia-se em qualquer uma das grafias (DIAMOND_SWORD ou diamond_sword). Um som ou um efeito nomeia-se pela sua chave Minecraft (entity.player.levelup, speed) e não pela constante Bukkit: a chave segue tal e qual para o cliente, sobrevive às mudanças de versão.

O que a gramática não permite

É fechada, e isso diz-se antes do resto. O que consta no catálogo é possível, o resto é impossível, e assim continuará: não há forma nenhuma de executar Java arbitrário. É isto que torna o estúdio instantâneo (nada para compilar) e uma spec inofensiva de instalar, seja qual for a sua origem.

As condições não se aninham: é um limite assumido da versão 1, uma árvore booleana desenha-se mal em blocos, e all / any / not quase sempre chegam. A única saída é a ação «executar um comando como consola», que nesse servidor tem exatamente o poder da consola, nem mais nem menos.

Os gatilhos

O que desencadeia uma regra, e as variáveis que cada um fornece para os modelos.

player.join Um jogador entra

No momento em que o jogador entra no servidor, depois de o mundo dele estar carregado.

fornece: {player} O jogador que entra, {world} O mundo onde ele chega

player.quit Um jogador sai

No momento em que o jogador sai do servidor. Continua contactável, mas qualquer mensagem enviada perde-se.

fornece: {player} O jogador que sai, {world} O mundo que ele deixa

player.chat Um jogador escreve no chat cancelável

Antes de a mensagem ser distribuída aos outros jogadores. Anulável: nesse caso, ninguém vê a mensagem.

fornece: {player} O autor da mensagem, {world} O mundo dele, {message} A mensagem escrita

player.death Um jogador morre

Na morte do jogador, antes do ecrã de reaparecimento.

fornece: {player} O jogador morto, {world} O mundo dele, {killer} O jogador responsável, vazio se não houver nenhum

player.respawn Um jogador reaparece

Quando o jogador volta ao jogo depois de morrer.

fornece: {player} O jogador que reaparece, {world} O mundo de reaparecimento

block.break Um jogador parte um bloco cancelável

Antes de o bloco desaparecer. Anulável: o bloco continua no lugar.

fornece: {player} O jogador que parte, {world} O mundo dele, {block} O tipo de bloco (DIAMOND_ORE), {x} Coordenada X do bloco, {y} Altura do bloco, {z} Coordenada Z do bloco

block.place Um jogador coloca um bloco cancelável

Antes de o bloco ser colocado. Anulável: o bloco não é colocado.

fornece: {player} O jogador que coloca, {world} O mundo dele, {block} O tipo de bloco colocado, {x} Coordenada X do bloco, {y} Altura do bloco, {z} Coordenada Z do bloco

command Um jogador escreve um comando

Cria um novo comando no servidor. ⚠️ Um comando cujo nome ainda não existia só aparece depois de reiniciar o servidor: o Paper só aceita novos nomes de comandos no arranque. O conteúdo da regra, esse, recarrega sem reiniciar.

  • name text Nome do comando, sem a barra
  • description text facultativo Descrição apresentada na ajuda
  • permission text facultativo Permissão exigida, vazio para todos

fornece: {player} O jogador que escreveu o comando, {world} O mundo dele, {args} O que se segue ao comando, tal como escrito

schedule.repeat A cada N segundos

Repete a regra enquanto o servidor estiver a funcionar. Nenhum jogador está envolvido: só se podem usar ações que não precisem de nenhum.

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

As condições

O que filtra. Zero a 20 por regra, combinadas por match.

player.has_permission O jogador tem a permissão precisa de um jogador
  • permission text Permissão
player.is_op O jogador é operador precisa de um jogador
player.in_world O jogador está no mundo precisa de um jogador
  • world text Nome do mundo
player.health_below O jogador tem menos de X corações precisa de um jogador

Em pontos de vida: 20 pontos equivalem a 10 corações.

  • value number [0..1024] Pontos de vida
text.contains Um texto contém
  • text text Texto examinado
  • search text O que procurar nele
  • ignoreCase boolean facultativo, predefinição: true Ignorar maiúsculas e minúsculas
text.equals Um texto é igual a
  • text text Texto examinado
  • value text Valor esperado
  • ignoreCase boolean facultativo, predefinição: true Ignorar maiúsculas e minúsculas
number.compare Comparar dois números

Os dois lados são modelos: {y} compara-se com 62 sem escrever código.

  • left text Lado esquerdo
  • operator < | <= | == | != | >= | > Comparação
  • right text Lado direito
chance Ao acaso, X % das vezes
  • percent number [0..100] Percentagem

As ações

O que acontece. Uma a 50 por regra, executadas por ordem.

player.send_message Enviar uma mensagem ao jogador precisa de um jogador
  • message text Mensagem
player.send_actionbar Mostrar uma mensagem acima da barra de ação precisa de um jogador
  • message text Mensagem
player.send_title Mostrar um título em grande precisa de um jogador
  • title text Título
  • subtitle text facultativo Subtítulo
  • fadeIn number [0..60] facultativo, predefinição: 0.5 Aparecimento em segundos
  • stay number [0..600] facultativo, predefinição: 3 Duração em segundos
  • fadeOut number [0..60] facultativo, predefinição: 0.5 Desaparecimento em segundos
player.play_sound Tocar um som para o jogador precisa de um jogador
  • sound identifier Som, em chave Minecraft (entity.player.levelup)
  • volume number [0..10] facultativo, predefinição: 1 Volume
  • pitch number [0.5..2] facultativo, predefinição: 1 Tom
player.give_item Dar um item ao jogador precisa de um jogador

O que não couber no inventário cai no chão, aos pés dele.

  • material identifier Item (DIAMOND_SWORD)
  • amount integer [1..2304] facultativo, predefinição: 1 Quantidade
player.give_effect Aplicar um efeito ao jogador precisa de um jogador
  • effect identifier Efeito, em chave Minecraft (night_vision)
  • seconds integer [1..86400] facultativo, predefinição: 10 Duração em segundos
  • amplifier integer [0..255] facultativo, predefinição: 0 Nível, a partir de 0
player.heal Curar o jogador precisa de um jogador

Vida ao máximo, fome saciada.

player.teleport Teletransportar o jogador precisa de um jogador
  • x number [-30000000..30000000] Coordenada X
  • y number [-512..1024] Altura
  • z number [-30000000..30000000] Coordenada Z
  • world text facultativo Mundo, vazio para o do jogador
player.kick Expulsar o jogador precisa de um jogador
  • reason text Motivo apresentado
broadcast Anunciar a todo o servidor
  • message text Mensagem
server.run_command Executar um comando como consola

A válvula de escape da gramática: tudo o que a consola sabe fazer. Ou seja, o poder da consola sobre este servidor, por isso só se deve escrever com conhecimento de causa.

  • command text Comando, sem a barra
cancel_event Anular o que acabou de acontecer precisa de um acionador cancelável

Só existe em gatilhos anuláveis: a mensagem não é distribuída, o bloco não é partido.

log Escrever na consola do servidor
  • message text Mensagem
  • level info | warn facultativo, predefinição: info Nível
delay Esperar

Coloca em pausa as restantes ações da regra. Uma regra que espera já não pode anular nada: "Anular" tem de vir antes.

  • seconds number [0.05..3600] Segundos

Os ficheiros publicados

O JSON Schema é gerado a partir do catálogo, a cada implementação, e é a referência legível por uma máquina: um editor de texto que o segue completa os identificadores e recusa parâmetros desconhecidos.

  • plugin-1.schema.json : o esquema, em JSON Schema draft 2020-12.
  • catalogue-1.json : o próprio catálogo, a fonte de onde tudo o resto deriva (o validador, o esquema, os blocos do editor e o motor Java).

O catálogo tem as etiquetas em francês, que é a língua de origem. Os identificadores, esses, não se traduzem: são eles que o motor executa.

O que acontece quando a gramática muda

Adicionar um gatilho, uma condição ou uma ação não parte nada: uma spec escrita antes continua a funcionar. Uma alteração de rutura, essa, incrementa specVersion, e o motor recusa então o que não entende em vez de o executar de forma errada, dizendo-o na consola do servidor.

As URLs acima têm esse número: plugin-1.schema.json designará sempre esta gramática, mesmo no dia em que exista outra.