NimBlock Sign in

The plugin grammar

specVersion 1

A NimBlock plugin is not code: it is a JSON file describing rules, read and executed by a Paper engine written once for everyone. This page states exactly what you are allowed to write in it, and nothing else is possible.

One rule, and nothing but a rule

The whole grammar fits in one sentence: when this happens, if these conditions hold, then do that. A plugin is a list of rules; a rule is one trigger, zero to 20 conditions and one to 50 actions.

No Java is generated anywhere, and nothing is compiled: the server loads an engine that reads the spec. That is the property that matters later, because a new Minecraft version is paid for once in that engine, and not in every plugin ever written with it.

What a spec looks like

{
    "specVersion": 1,
    "name": "Welcome",
    "rules": [
        {
            "id": "welcome",
            "name": "Welcome message",
            "match": "all",
            "trigger": { "type": "player.join" },
            "conditions": [
                { "type": "player.has_permission", "params": { "permission": "nimblock.vip" } }
            ],
            "actions": [
                { "type": "player.send_message", "params": { "message": "&6Welcome {player}!" } }
            ]
        }
    ]
}

match is all (default) or any, and a condition may carry "not": true. enabled: false keeps a rule in the file without arming it.

The grammar, in EBNF

The terminal lists below are generated from the catalogue: they are literally the identifiers the engine can execute, not a hand-written transcription.

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

Two things do not show up in those production rules, because they are about coherence rather than form. A condition or an action that requires a player can only be written under a trigger that provides one: "heal the player" means nothing under "every N seconds". And cancel_event can only be written under a cancellable trigger. Both are rejected as you write, not at runtime.

Templates

Parameters of type text are templates: {player} is replaced by what the trigger provided, {{ gives a literal brace, and the colour codes &a &l are translated.

A variable the trigger knows nothing about is an error, not text. Every trigger declares what it provides (see the list below): outside that list there is nothing to read, and a typo is rejected as you write rather than landing in the players' chat. A trigger's own parameters are not templates: they are read when the server starts, before there is anything to substitute.

Minecraft names come in two shapes depending on what they designate. An item is named in either spelling (DIAMOND_SWORD or diamond_sword). A sound or an effect is named by its Minecraft key (entity.player.levelup, speed) rather than the Bukkit constant: the key goes to the client as is, and it survives version changes.

What the grammar does not allow

It is closed, and that is worth saying before anything else. What the catalogue lists is possible, the rest is impossible and will stay that way: there is no way to run arbitrary Java. That is what makes the studio instant (nothing to compile) and a spec harmless to install, wherever it came from.

Conditions do not nest: that is a deliberate limit of version 1, a boolean tree draws badly in blocks, and all / any / not are almost always enough. The one way out is the "run a command as console" action, which on that server has exactly the power of the console, no more and no less.

Triggers

What starts a rule, and the variables each one provides to templates.

player.join A player joins

The moment the player enters the server, once their world has loaded.

provides: {player} The player joining, {world} The world they arrive in

player.quit A player leaves

The moment the player leaves the server. They are still reachable, but any message sent is lost.

provides: {player} The player leaving, {world} The world they leave

player.chat A player writes in chat cancellable

Before the message is delivered to the other players. Cancellable: nobody then sees the message.

provides: {player} Who wrote the message, {world} Their world, {message} The message written

player.death A player dies

On the player's death, before the respawn screen.

provides: {player} The player who died, {world} Their world, {killer} The player responsible, empty if there is none

player.respawn A player respawns

When the player comes back into the game after dying.

provides: {player} The player respawning, {world} The respawn world

block.break A player breaks a block cancellable

Before the block disappears. Cancellable: the block stays put.

provides: {player} The player breaking it, {world} Their world, {block} The block type (DIAMOND_ORE), {x} Block X coordinate, {y} Block height, {z} Block Z coordinate

block.place A player places a block cancellable

Before the block is placed. Cancellable: the block is not placed.

provides: {player} The player placing it, {world} Their world, {block} The block type placed, {x} Block X coordinate, {y} Block height, {z} Block Z coordinate

command A player types a command

Creates a new command on the server. ⚠️ A command whose name did not exist yet only appears after a server restart: Paper accepts new command names at startup only. The rule's contents, on the other hand, reload without a restart.

  • name text Command name, without the slash
  • description text optional Description shown in the help
  • permission text optional Permission required, empty for everyone

provides: {player} The player who typed the command, {world} Their world, {args} Whatever follows the command, as typed

schedule.repeat Every N seconds

Repeats the rule for as long as the server runs. No player is involved: only actions that need none can be used.

  • seconds integer [1..86400] Interval in seconds

Conditions

What filters. Zero to 20 per rule, combined by match.

player.has_permission The player has the permission needs a player
  • permission text Permission
player.is_op The player is an operator needs a player
player.in_world The player is in the world needs a player
  • world text World name
player.health_below The player has fewer than X hearts needs a player

In health points: 20 points make 10 hearts.

  • value number [0..1024] Health points
text.contains A text contains
  • text text Text examined
  • search text What to look for in it
  • ignoreCase boolean optional, default: true Ignore case
text.equals A text equals
  • text text Text examined
  • value text Expected value
  • ignoreCase boolean optional, default: true Ignore case
number.compare Compare two numbers

Both sides are templates: {y} compares against 62 without writing any code.

  • left text Left-hand side
  • operator < | <= | == | != | >= | > Comparison
  • right text Right-hand side
chance At random, X % of the time
  • percent number [0..100] Percentage

Actions

What happens. One to 50 per rule, executed in order.

player.send_message Send a message to the player needs a player
  • message text Message
player.send_actionbar Show a message above the action bar needs a player
  • message text Message
player.send_title Show a title in large text needs a player
  • title text Title
  • subtitle text optional Subtitle
  • fadeIn number [0..60] optional, default: 0.5 Fade in, in seconds
  • stay number [0..600] optional, default: 3 Duration in seconds
  • fadeOut number [0..60] optional, default: 0.5 Fade out, in seconds
player.play_sound Play a sound for the player needs a player
  • sound identifier Sound, as a Minecraft key (entity.player.levelup)
  • volume number [0..10] optional, default: 1 Volume
  • pitch number [0.5..2] optional, default: 1 Pitch
player.give_item Give an item to the player needs a player

Whatever does not fit in the inventory drops on the ground at their feet.

  • material identifier Item (DIAMOND_SWORD)
  • amount integer [1..2304] optional, default: 1 Amount
player.give_effect Apply an effect to the player needs a player
  • effect identifier Effect, as a Minecraft key (night_vision)
  • seconds integer [1..86400] optional, default: 10 Duration in seconds
  • amplifier integer [0..255] optional, default: 0 Level, starting at 0
player.heal Heal the player needs a player

Health to full, hunger satisfied.

player.teleport Teleport the player needs a player
  • x number [-30000000..30000000] X coordinate
  • y number [-512..1024] Height
  • z number [-30000000..30000000] Z coordinate
  • world text optional World, empty for the player's own
player.kick Kick the player needs a player
  • reason text Reason shown
broadcast Announce to the whole server
  • message text Message
server.run_command Run a command as the console

The grammar's escape hatch: anything the console can do. That is the console's full power over this server, so write it knowing what you are doing.

  • command text Command, without the slash
cancel_event Cancel what just happened needs a cancellable trigger

Only exists on cancellable triggers: the message is not delivered, the block is not broken.

log Write to the server console
  • message text Message
  • level info | warn optional, default: info Level
delay Wait

Pauses the rest of the rule's actions. A rule that waits can no longer cancel anything: "Cancel" must come first.

  • seconds number [0.05..3600] Seconds

The published files

The JSON Schema is generated from the catalogue on every deploy, and it is the machine-readable reference: an editor that follows it completes identifiers and rejects unknown parameters.

  • plugin-1.schema.json : the schema, JSON Schema draft 2020-12.
  • catalogue-1.json : the catalogue itself, the source everything else derives from (the validator, the schema, the editor blocks and the Java engine).

The catalogue carries French labels, French being the source language. The identifiers are not translated: they are what the engine executes.

What happens when the grammar changes

Adding a trigger, a condition or an action breaks nothing: a spec written before keeps running. A breaking change increments specVersion, and the engine then refuses what it does not understand rather than running it sideways, and says so in the server console.

The URLs above carry that number: plugin-1.schema.json will always designate this grammar, even once another one exists.