NimBlock ログイン

プラグインの文法

specVersion 1

NimBlockのプラグインはコードではない。ルールを記述したJSONファイルであり、みんなのために 一度だけ書かれたPaperエンジンがそれを読み込んで実行する。このページはそこに書いてよいこと を正確に示すもので、それ以外は一切できない。

ルールはひとつ、それだけ

文法のすべてはこの一文に収まる:これが起きたとき、これらの条件が満たされて いれば、これを行う。プラグインとはルールの一覧であり、ひとつのルールは トリガーがひとつ、条件が0個から20個、アクションが1個から 50個で構成される。

どこかでJavaが生成されるわけでも、コンパイルされるわけでもない。サーバーはspecを 読み込むエンジンを起動するだけだ。これが後々効いてくる性質で、新しい Minecraftのバージョンへの対応コストはこのエンジンで一度だけ支払えばよく、そこで 書かれた個々のプラグインごとには発生しない。

specの形

{
    "specVersion": 1,
    "name": "ようこそ",
    "rules": [
        {
            "id": "welcome",
            "name": "ようこそメッセージ",
            "match": "all",
            "trigger": { "type": "player.join" },
            "conditions": [
                { "type": "player.has_permission", "params": { "permission": "nimblock.vip" } }
            ],
            "actions": [
                { "type": "player.send_message", "params": { "message": "&6ようこそ、{player}さん!" } }
            ]
        }
    ]
}

matchall(既定)かanyのどちらかで、条件 には"not": trueを付けられる。enabled: falseはルールを ファイルに残したまま無効化する。

文法、EBNFで

下にある終端記号の一覧はカタログから生成されたものだ。つまりエンジンが実行できる 識別子そのものであり、手作業で書き写したものではない。

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

この生成規則には表れない事柄が2つある。どちらも形ではなく整合性にかかわるものだ からだ。プレイヤーを要求する条件やアクションは、プレイヤーを渡すトリガーの下でしか 書けない。「プレイヤーを回復する」は「N秒ごと」の下では意味を持たない。そして cancel_eventはキャンセル可能なトリガーの下でしか書けない。どちらも 実行時ではなく、書いた時点で拒否される。

テンプレート

text型のパラメーターはテンプレートだ:{player} はトリガーが渡した値に置き換えられ、{{はブレース自体を 表し、カラーコード&a &lは変換される。

トリガーが知らない変数を使うとエラーになる。テキストとしてそのまま出力 されることはない。各トリガーは自分が渡す変数を宣言しており(下の一覧を 参照)、その一覧にないものは何も読み出せない。タイプミスはプレイヤーのチャットに 現れる前に、書いた時点で拒否される。一方トリガー自身のパラメーターは テンプレートではない。サーバー起動時に読み込まれるもので、その時点では置き換える べきものがまだ何もない。

Minecraftの名前は、何を指すかによって2通りの書き方がある。アイテム はどちらの表記でも通る(DIAMOND_SWORDまたはdiamond_sword)。 サウンドエフェクトはBukkitの定数ではなく Minecraft側のキーで指定する(entity.player.levelupspeed)。 このキーはそのままクライアントに送られ、バージョンが変わっても残り続ける。

この文法でできないこと

この文法は閉じている。まずそれを言っておく。カタログに載って いることはできる、それ以外はできないし、これからもできない。任意のJavaを実行する 手段は存在しない。だからこそスタジオは即座に動き(コンパイルするものが何もない)、 specはどこから来たものであっても、インストールしても害がない。

条件は入れ子にできない。これはバージョン1で割り切った制限で、 ブール木をブロックで表現すると形が崩れやすく、all / any / notでほとんどの場合は事足りる。唯一の抜け道は「コンソールとして コマンドを実行する」アクションで、これはそのサーバー上でコンソールが持つ権限を そのまま持つ。それ以上でも以下でもない。

トリガー

ルールを起動するもの、そしてそれぞれがテンプレート用に渡す変数。

player.join プレイヤーが参加

プレイヤーがサーバーに参加した瞬間。ワールドの読み込みが終わった後。

提供: {player} 参加したプレイヤー, {world} 参加先のワールド

player.quit プレイヤーが退出

プレイヤーがサーバーを退出した瞬間。まだ通信は届くが、送信したメッセージはすべて失われる。

提供: {player} 退出したプレイヤー, {world} 退出したワールド

player.chat プレイヤーがチャットに書き込む キャンセル可能

メッセージが他のプレイヤーに配信される前。キャンセル可能:キャンセルするとメッセージは誰にも見えなくなる。

提供: {player} 書き込んだプレイヤー, {world} そのワールド, {message} 書き込まれたメッセージ

player.death プレイヤーが死亡

プレイヤーが死亡した瞬間、リスポーン画面が表示される前。

提供: {player} 死亡したプレイヤー, {world} そのワールド, {killer} 死因となったプレイヤー、いなければ空

player.respawn プレイヤーがリスポーン

プレイヤーが死亡後にゲームへ戻ったとき。

提供: {player} リスポーンしたプレイヤー, {world} リスポーン先のワールド

block.break プレイヤーがブロックを破壊 キャンセル可能

ブロックが消える前。キャンセル可能:キャンセルするとブロックはそのまま残る。

提供: {player} 破壊したプレイヤー, {world} そのワールド, {block} ブロックの種類(DIAMOND_ORE), {x} ブロックのX座標, {y} ブロックの高さ, {z} ブロックのZ座標

block.place プレイヤーがブロックを設置 キャンセル可能

ブロックが設置される前。キャンセル可能:キャンセルするとブロックは設置されない。

提供: {player} 設置したプレイヤー, {world} そのワールド, {block} 設置されたブロックの種類, {x} ブロックのX座標, {y} ブロックの高さ, {z} ブロックのZ座標

command プレイヤーがコマンドを入力

サーバーに新しいコマンドを作成する。⚠️ まだ存在しない名前のコマンドは、サーバーを再起動するまで反映されない:Paperは起動時にしか新しいコマンド名を受け付けない。ルールの中身は、再起動なしでそのまま反映される。

  • name text コマンド名(スラッシュなし)
  • description text 省略可 ヘルプに表示される説明
  • permission text 省略可 必要な権限、空なら誰でも使える

提供: {player} コマンドを入力したプレイヤー, {world} そのワールド, {args} コマンドに続く部分、そのまま

schedule.repeat N秒ごと

サーバーが動いている間、ルールを繰り返す。プレイヤーは関係しない:プレイヤーを必要としないアクションしか使えない。

  • seconds integer [1..86400] 間隔(秒)

条件

絞り込むもの。1ルールにつき0個から20個まで、matchで組み合わせる。

player.has_permission プレイヤーが権限を持っている プレイヤーが必要
  • permission text 権限
player.is_op プレイヤーがOPである プレイヤーが必要
player.in_world プレイヤーがそのワールドにいる プレイヤーが必要
  • world text ワールド名
player.health_below プレイヤーの体力がXハート未満 プレイヤーが必要

体力ポイントで指定する:20ポイントが10ハートにあたる。

  • value number [0..1024] 体力ポイント
text.contains 文字列を含む
  • text text 調べる文字列
  • search text 検索する文字列
  • ignoreCase boolean 省略可, 既定値: true 大文字と小文字を区別しない
text.equals 文字列が等しい
  • text text 調べる文字列
  • value text 期待する値
  • ignoreCase boolean 省略可, 既定値: true 大文字と小文字を区別しない
number.compare 2つの数値を比較

両辺はテンプレート:{y}を62と比較するのにコードは要らない。

  • left text 左辺
  • operator < | <= | == | != | >= | > 比較演算子
  • right text 右辺
chance ランダムでX%の確率
  • percent number [0..100] パーセンテージ

アクション

実際に起きること。1ルールにつき1個から50個まで、順番に実行される。

player.send_message プレイヤーにメッセージを送る プレイヤーが必要
  • message text メッセージ
player.send_actionbar アクションバーの上にメッセージを表示 プレイヤーが必要
  • message text メッセージ
player.send_title 大きなタイトルを表示 プレイヤーが必要
  • title text タイトル
  • subtitle text 省略可 サブタイトル
  • fadeIn number [0..60] 省略可, 既定値: 0.5 フェードイン(秒)
  • stay number [0..600] 省略可, 既定値: 3 表示時間(秒)
  • fadeOut number [0..60] 省略可, 既定値: 0.5 フェードアウト(秒)
player.play_sound プレイヤーに音を再生 プレイヤーが必要
  • sound identifier 音、Minecraftのキーで指定(entity.player.levelup)
  • volume number [0..10] 省略可, 既定値: 1 音量
  • pitch number [0.5..2] 省略可, 既定値: 1 ピッチ
player.give_item プレイヤーにアイテムを渡す プレイヤーが必要

インベントリに入りきらない分は足元に落ちる。

  • material identifier アイテム(DIAMOND_SWORD)
  • amount integer [1..2304] 省略可, 既定値: 1 個数
player.give_effect プレイヤーに効果を付与 プレイヤーが必要
  • effect identifier 効果、Minecraftのキーで指定(night_vision)
  • seconds integer [1..86400] 省略可, 既定値: 10 持続時間(秒)
  • amplifier integer [0..255] 省略可, 既定値: 0 レベル(0から)
player.heal プレイヤーを回復 プレイヤーが必要

体力を最大に、満腹度も満たす。

player.teleport プレイヤーをテレポート プレイヤーが必要
  • x number [-30000000..30000000] X座標
  • y number [-512..1024] 高さ
  • z number [-30000000..30000000] Z座標
  • world text 省略可 ワールド、空ならプレイヤーがいるワールド
player.kick プレイヤーをキック プレイヤーが必要
  • reason text 表示される理由
broadcast サーバー全体にアナウンス
  • message text メッセージ
server.run_command コンソールとしてコマンドを実行

文法の逃げ道:コンソールにできることは何でもできる。このサーバーに対するコンソールの権限そのものなので、わかったうえで書くこと。

  • command text コマンド(スラッシュなし)
cancel_event 直前の出来事をキャンセル キャンセル可能なトリガーが必要

キャンセル可能な発動条件にしか存在しない:メッセージは配信されず、ブロックは壊れない。

log サーバーのコンソールに書き込む
  • message text メッセージ
  • level info | warn 省略可, 既定値: info レベル
delay 待つ

ルールの残りのアクションを一時停止する。待った後のルールはもう何もキャンセルできない:「キャンセル」はそれより前に置くこと。

  • seconds number [0.05..3600] 秒数

公開されているファイル

JSON Schemaはデプロイのたびにカタログから生成される、機械可読な 参照仕様だ。これに従うエディターなら識別子を補完し、未知のパラメーターを拒否できる。

  • plugin-1.schema.json :スキーマ本体。JSON Schema draft 2020-12。
  • catalogue-1.json :カタログそのもの。他のすべて(バリデーター、スキーマ、エディターのブロック、 Javaエンジン)がここから導かれる元データ。

カタログのラベルはソース言語であるフランス語で書かれている。一方、識別子は翻訳 されない。エンジンが実行するのはこの識別子そのものだからだ。

文法が変わるとき何が起きるか

トリガー・条件・アクションを追加しても何も壊れない。以前に書かれたspecはそのまま 動き続ける。一方破壊的変更specVersionを繰り上げ、 以降エンジンは理解できないものを誤って実行するのではなく拒否し、サーバーの コンソールにそう表示する。

上のURLはこの番号を含んでいる。plugin-1.schema.jsonは、 たとえ別の文法が存在するようになった日が来ても、常にこの文法を指し続ける。