NimBlock Anmelden

Die Plugin-Grammatik

specVersion 1

Ein NimBlock-Plugin ist kein Code: Es ist eine JSON-Datei, die Regeln beschreibt und von einer Paper-Engine gelesen und ausgeführt wird, die einmal für alle geschrieben wurde. Diese Seite sagt genau, was du darin schreiben darfst, und mehr ist nicht möglich.

Eine Regel, und nichts als eine Regel

Die ganze Grammatik passt in einen Satz: wenn dies passiert, falls diese Bedingungen erfüllt sind, dann tue das. Ein Plugin ist eine Liste von Regeln, eine Regel besteht aus einem Trigger, null bis 20 Bedingungen und einer bis 50 Aktionen.

Es wird nirgendwo Java erzeugt, und es gibt keine Kompilierung: Der Server lädt eine Engine, die die Spec liest. Das ist die Eigenschaft, auf die es später ankommt, denn eine neue Minecraft-Version wird einmal in dieser Engine bezahlt, und nicht in jedem einzelnen Plugin, das damit geschrieben wurde.

Wie eine Spec aussieht

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

match ist all (Standard) oder any, und eine Bedingung kann "not": true tragen. enabled: false behält eine Regel in der Datei, ohne sie zu aktivieren.

Die Grammatik, in EBNF

Die unten stehenden Terminallisten werden aus dem Katalog erzeugt: Es sind buchstäblich die Bezeichner, die die Engine ausführen kann, keine von Hand erstellte Abschrift.

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

Zwei Dinge stehen nicht in diesen Produktionsregeln, weil sie die Kohärenz betreffen und nicht die Form. Eine Bedingung oder eine Aktion, die einen Spieler voraussetzt, lässt sich nur unter einem Trigger schreiben, der einen bereitstellt: „den Spieler heilen" ergibt unter „alle N Sekunden" keinen Sinn. Und cancel_event lässt sich nur unter einem abbrechbaren Trigger schreiben. Beides wird schon beim Schreiben abgelehnt, nicht erst bei der Ausführung.

Vorlagen

Parameter vom Typ text sind Vorlagen: {player} wird durch das ersetzt, was der Trigger bereitgestellt hat, {{ ergibt eine literale geschweifte Klammer, und die Farbcodes &a &l werden übersetzt.

Eine Variable, die der Trigger nicht kennt, ist ein Fehler, kein Text. Jeder Trigger erklärt, was er bereitstellt (siehe Liste weiter unten): Außerhalb dieser Liste gibt es nichts zu lesen, und ein Tippfehler wird schon beim Schreiben abgelehnt, statt im Chat der Spieler zu landen. Die Parameter eines Triggers selbst sind dagegen keine Vorlagen: Sie werden beim Start des Servers gelesen, bevor es überhaupt etwas zu ersetzen gibt.

Minecraft-Namen werden auf zwei Arten geschrieben, je nachdem, was sie bezeichnen. Ein Gegenstand wird in beiden Schreibweisen benannt (DIAMOND_SWORD oder diamond_sword). Ein Sound oder ein Effekt wird über seinen Minecraft-Schlüssel benannt (entity.player.levelup, speed) und nicht über die Bukkit-Konstante: Der Schlüssel geht unverändert an den Client und übersteht Versionswechsel.

Was die Grammatik nicht erlaubt

Sie ist geschlossen, und das muss vor allem anderen gesagt werden. Was im Katalog steht, ist möglich, der Rest ist unmöglich und wird es bleiben: Es gibt keine Möglichkeit, beliebigen Java-Code auszuführen. Das macht das Studio sofort einsatzbereit (nichts zu kompilieren) und eine Spec ungefährlich zu installieren, ganz gleich, woher sie stammt.

Bedingungen lassen sich nicht verschachteln: Das ist eine bewusste Einschränkung von Version 1, ein boolescher Baum lässt sich schlecht in Blöcken darstellen, und all / any / not reichen fast immer aus. Der einzige Ausweg ist die Aktion „Befehl als Konsole ausführen", die auf diesem Server genau die Macht der Konsole hat, nicht mehr und nicht weniger.

Trigger

Was eine Regel auslöst, und die Variablen, die jeder Trigger für Vorlagen bereitstellt.

player.join Ein Spieler verbindet sich

In dem Moment, in dem der Spieler den Server betritt, nachdem seine Welt geladen wurde.

liefert: {player} Der Spieler, der sich verbindet, {world} Die Welt, in der er ankommt

player.quit Ein Spieler verlässt den Server

In dem Moment, in dem der Spieler den Server verlässt. Er ist noch erreichbar, aber jede gesendete Nachricht geht verloren.

liefert: {player} Der Spieler, der geht, {world} Die Welt, die er verlässt

player.chat Ein Spieler schreibt im Chat abbrechbar

Bevor die Nachricht an die anderen Spieler verteilt wird. Abbrechbar: Die Nachricht wird dann von niemandem gesehen.

liefert: {player} Der Verfasser der Nachricht, {world} Seine Welt, {message} Die geschriebene Nachricht

player.death Ein Spieler stirbt

Beim Tod des Spielers, vor dem Respawn-Bildschirm.

liefert: {player} Der gestorbene Spieler, {world} Seine Welt, {killer} Der verantwortliche Spieler, leer, wenn es keinen gibt

player.respawn Ein Spieler respawnt

Wenn der Spieler nach seinem Tod ins Spiel zurückkehrt.

liefert: {player} Der Spieler, der respawnt, {world} Die Respawn-Welt

block.break Ein Spieler baut einen Block ab abbrechbar

Bevor der Block verschwindet. Abbrechbar: Der Block bleibt bestehen.

liefert: {player} Der Spieler, der abbaut, {world} Seine Welt, {block} Der Blocktyp (DIAMOND_ORE), {x} X-Koordinate des Blocks, {y} Höhe des Blocks, {z} Z-Koordinate des Blocks

block.place Ein Spieler platziert einen Block abbrechbar

Bevor der Block platziert wird. Abbrechbar: Der Block wird nicht platziert.

liefert: {player} Der Spieler, der platziert, {world} Seine Welt, {block} Der platzierte Blocktyp, {x} X-Koordinate des Blocks, {y} Höhe des Blocks, {z} Z-Koordinate des Blocks

command Ein Spieler gibt einen Befehl ein

Erstellt einen neuen Befehl auf dem Server. ⚠️ Ein Befehl, dessen Name noch nicht existierte, erscheint erst nach einem Neustart des Servers: Paper akzeptiert neue Befehlsnamen nur beim Start. Der Inhalt der Regel dagegen wird ohne Neustart neu geladen.

  • name text Name des Befehls, ohne Schrägstrich
  • description text optional Beschreibung, die in der Hilfe angezeigt wird
  • permission text optional Erforderliche Berechtigung, leer für alle

liefert: {player} Der Spieler, der den Befehl eingegeben hat, {world} Seine Welt, {args} Was dem Befehl folgt, unverändert

schedule.repeat Alle N Sekunden

Wiederholt die Regel, solange der Server läuft. Kein Spieler ist beteiligt: Nur Aktionen, die keinen benötigen, können verwendet werden.

  • seconds integer [1..86400] Intervall in Sekunden

Bedingungen

Was filtert. Null bis 20 pro Regel, kombiniert durch match.

player.has_permission Der Spieler hat die Berechtigung braucht einen Spieler
  • permission text Berechtigung
player.is_op Der Spieler ist Operator braucht einen Spieler
player.in_world Der Spieler ist in der Welt braucht einen Spieler
  • world text Name der Welt
player.health_below Der Spieler hat weniger als X Herzen braucht einen Spieler

In Lebenspunkten: 20 Punkte entsprechen 10 Herzen.

  • value number [0..1024] Lebenspunkte
text.contains Ein Text enthält
  • text text Untersuchter Text
  • search text Wonach darin gesucht wird
  • ignoreCase boolean optional, Standard: true Groß-/Kleinschreibung ignorieren
text.equals Ein Text ist gleich
  • text text Untersuchter Text
  • value text Erwarteter Wert
  • ignoreCase boolean optional, Standard: true Groß-/Kleinschreibung ignorieren
number.compare Zwei Zahlen vergleichen

Beide Seiten sind Vorlagen: {y} wird ohne Code zu schreiben mit 62 verglichen.

  • left text Linke Seite
  • operator < | <= | == | != | >= | > Vergleich
  • right text Rechte Seite
chance Zufällig, X % der Zeit
  • percent number [0..100] Prozentsatz

Aktionen

Was passiert. Eine bis 50 pro Regel, in der Reihenfolge ausgeführt.

player.send_message Dem Spieler eine Nachricht senden braucht einen Spieler
  • message text Nachricht
player.send_actionbar Eine Nachricht über der Aktionsleiste anzeigen braucht einen Spieler
  • message text Nachricht
player.send_title Einen großen Titel anzeigen braucht einen Spieler
  • title text Titel
  • subtitle text optional Untertitel
  • fadeIn number [0..60] optional, Standard: 0.5 Einblenden in Sekunden
  • stay number [0..600] optional, Standard: 3 Dauer in Sekunden
  • fadeOut number [0..60] optional, Standard: 0.5 Ausblenden in Sekunden
player.play_sound Einen Sound für den Spieler abspielen braucht einen Spieler
  • sound identifier Sound, als Minecraft-Schlüssel (entity.player.levelup)
  • volume number [0..10] optional, Standard: 1 Lautstärke
  • pitch number [0.5..2] optional, Standard: 1 Tonhöhe
player.give_item Dem Spieler einen Gegenstand geben braucht einen Spieler

Was nicht ins Inventar passt, fällt zu seinen Füßen auf den Boden.

  • material identifier Gegenstand (DIAMOND_SWORD)
  • amount integer [1..2304] optional, Standard: 1 Menge
player.give_effect Einen Effekt auf den Spieler anwenden braucht einen Spieler
  • effect identifier Effekt, als Minecraft-Schlüssel (night_vision)
  • seconds integer [1..86400] optional, Standard: 10 Dauer in Sekunden
  • amplifier integer [0..255] optional, Standard: 0 Stufe, beginnend bei 0
player.heal Den Spieler heilen braucht einen Spieler

Leben auf Maximum, Hunger gestillt.

player.teleport Den Spieler teleportieren braucht einen Spieler
  • x number [-30000000..30000000] X-Koordinate
  • y number [-512..1024] Höhe
  • z number [-30000000..30000000] Z-Koordinate
  • world text optional Welt, leer für die des Spielers
player.kick Den Spieler kicken braucht einen Spieler
  • reason text Angezeigter Grund
broadcast Dem ganzen Server ankündigen
  • message text Nachricht
server.run_command Einen Befehl als Konsole ausführen

Der Notausgang der Grammatik: alles, was die Konsole kann. Das ist die volle Macht der Konsole über diesen Server, also nur mit Bedacht einsetzen.

  • command text Befehl, ohne Schrägstrich
cancel_event Das gerade Geschehene abbrechen braucht einen abbrechbaren Auslöser

Existiert nur bei abbrechbaren Auslösern: Die Nachricht wird nicht verteilt, der Block wird nicht abgebaut.

log In die Serverkonsole schreiben
  • message text Nachricht
  • level info | warn optional, Standard: info Stufe
delay Warten

Pausiert die restlichen Aktionen der Regel. Eine Regel, die wartet, kann nichts mehr abbrechen: „Abbrechen“ muss davor kommen.

  • seconds number [0.05..3600] Sekunden

Die veröffentlichten Dateien

Das JSON Schema wird bei jedem Deployment aus dem Katalog erzeugt und ist die maschinenlesbare Referenz: Ein Editor, der es befolgt, vervollständigt Bezeichner und lehnt unbekannte Parameter ab.

  • plugin-1.schema.json : das Schema, in JSON Schema draft 2020-12.
  • catalogue-1.json : der Katalog selbst, die Quelle, von der alles andere abgeleitet wird (der Validator, das Schema, die Editor-Blöcke und die Java-Engine).

Der Katalog trägt die Bezeichnungen auf Französisch, der Ausgangssprache. Die Bezeichner hingegen werden nicht übersetzt: Sie sind es, was die Engine ausführt.

Was passiert, wenn sich die Grammatik ändert

Das Hinzufügen eines Triggers, einer Bedingung oder einer Aktion bricht nichts: Eine zuvor geschriebene Spec läuft weiter. Ein Breaking Change dagegen erhöht specVersion, und die Engine verweigert dann, was sie nicht versteht, statt es fehlerhaft auszuführen, und meldet das in der Server-Konsole.

Die oben genannten URLs tragen diese Nummer: plugin-1.schema.json wird immer diese Grammatik hier bezeichnen, selbst wenn eines Tages eine andere existiert.