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.
-
nametext Name des Befehls, ohne Schrägstrich -
descriptiontext optional Beschreibung, die in der Hilfe angezeigt wird -
permissiontext 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.
-
secondsinteger [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
-
permissiontext Berechtigung
player.is_op
Der Spieler ist Operator
braucht einen Spieler
player.in_world
Der Spieler ist in der Welt
braucht einen Spieler
-
worldtext Name der Welt
player.health_below
Der Spieler hat weniger als X Herzen
braucht einen Spieler
In Lebenspunkten: 20 Punkte entsprechen 10 Herzen.
-
valuenumber [0..1024] Lebenspunkte
text.contains
Ein Text enthält
-
texttext Untersuchter Text -
searchtext Wonach darin gesucht wird -
ignoreCaseboolean optional, Standard:trueGroß-/Kleinschreibung ignorieren
text.equals
Ein Text ist gleich
-
texttext Untersuchter Text -
valuetext Erwarteter Wert -
ignoreCaseboolean optional, Standard:trueGroß-/Kleinschreibung ignorieren
number.compare
Zwei Zahlen vergleichen
Beide Seiten sind Vorlagen: {y} wird ohne Code zu schreiben mit 62 verglichen.
-
lefttext Linke Seite -
operator< | <= | == | != | >= | > Vergleich -
righttext Rechte Seite
chance
Zufällig, X % der Zeit
-
percentnumber [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
-
messagetext Nachricht
player.send_actionbar
Eine Nachricht über der Aktionsleiste anzeigen
braucht einen Spieler
-
messagetext Nachricht
player.send_title
Einen großen Titel anzeigen
braucht einen Spieler
-
titletext Titel -
subtitletext optional Untertitel -
fadeInnumber [0..60] optional, Standard:0.5Einblenden in Sekunden -
staynumber [0..600] optional, Standard:3Dauer in Sekunden -
fadeOutnumber [0..60] optional, Standard:0.5Ausblenden in Sekunden
player.play_sound
Einen Sound für den Spieler abspielen
braucht einen Spieler
-
soundidentifier Sound, als Minecraft-Schlüssel (entity.player.levelup) -
volumenumber [0..10] optional, Standard:1Lautstärke -
pitchnumber [0.5..2] optional, Standard:1Tonhö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.
-
materialidentifier Gegenstand (DIAMOND_SWORD) -
amountinteger [1..2304] optional, Standard:1Menge
player.give_effect
Einen Effekt auf den Spieler anwenden
braucht einen Spieler
-
effectidentifier Effekt, als Minecraft-Schlüssel (night_vision) -
secondsinteger [1..86400] optional, Standard:10Dauer in Sekunden -
amplifierinteger [0..255] optional, Standard:0Stufe, beginnend bei 0
player.heal
Den Spieler heilen
braucht einen Spieler
Leben auf Maximum, Hunger gestillt.
player.teleport
Den Spieler teleportieren
braucht einen Spieler
-
xnumber [-30000000..30000000] X-Koordinate -
ynumber [-512..1024] Höhe -
znumber [-30000000..30000000] Z-Koordinate -
worldtext optional Welt, leer für die des Spielers
player.kick
Den Spieler kicken
braucht einen Spieler
-
reasontext Angezeigter Grund
broadcast
Dem ganzen Server ankündigen
-
messagetext 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.
-
commandtext 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
-
messagetext Nachricht -
levelinfo | warn optional, Standard:infoStufe
delay
Warten
Pausiert die restlichen Aktionen der Regel. Eine Regel, die wartet, kann nichts mehr abbrechen: „Abbrechen“ muss davor kommen.
-
secondsnumber [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.