NimBlock 登录

插件语法

specVersion 1

NimBlock 插件不是代码:它是一个描述规则的 JSON 文件,由一个为所有人统一编写的 Paper 引擎读取并执行。本页准确说明你可以在其中编写什么,除此之外别无可能。

一条规则,仅此而已

整个语法可以浓缩成一句话:当这件事发生时,如果满足这些 条件,就执行这个动作。一个插件是一组规则的列表,一条规则由一个触发器、零到 20 个条件、一到 50 个动作组成。

这里不会生成任何 Java 代码,也没有任何编译过程:服务器加载的引擎会 读取规格文件。这个特性之后才显出重要性,因为一次 Minecraft 版本更新 只需要在这个引擎里处理一次,而不必在用它写出的每一个插件里都处理一次。

规格文件的样子

{
    "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}!" } }
            ]
        }
    ]
}

match 的取值为 all(默认)或 any,条件 还可以带上 "not": trueenabled: 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"

有两点在这些产生式规则里看不出来,因为它们关乎一致性,而不是语法形式。 需要玩家对象的条件或动作,只能写在会提供玩家对象的触发器之下:在 "每 N 秒"之下写"治疗玩家"是没有意义的。 cancel_event 也只能写在可取消的触发器之下。这两条规则都是在 编写时就被拒绝,而不是等到运行时才发现。

模板

text 类型的参数就是模板: {player} 会被替换成触发器提供的内容, {{ 会得到一个字面意义上的花括号,颜色代码 &a &l 也会被转换。

触发器不知道的变量是一个错误,而不是一段文本。 每个触发器都会声明自己提供什么内容(见下面的列表):列表之外没有任何 东西可读,拼写错误会在编写时就被拒绝,而不会直接出现在玩家的聊天框里。 触发器本身的参数不是模板:它们在服务器启动时就被读取,那时还 没有任何东西可以替换。

Minecraft 名称有两种写法,取决于它指代的是什么。物品 可以用两种拼写方式(DIAMOND_SWORDdiamond_sword)。声音效果 则要用 Minecraft 键名来指定(entity.player.levelupspeed),而不是 Bukkit 常量:键名会原样发送给客户端, 并且能在版本更新后依然有效。

语法做不到的事

这套语法是封闭的,这一点要先说清楚。目录里列出的都是 可以做到的,其余的做不到,以后也不会做到:没有任何办法执行任意的 Java 代码。这正是编辑器能够即时生效(不需要编译)、任何来源的规格文件安装起来 都无害的原因。

条件不能嵌套:这是版本 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] 间隔秒数

条件

用来过滤的部分。每条规则零到 20 个,由 match 组合。

player.has_permission 玩家拥有该权限 需要一名玩家
  • permission text 权限
player.is_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 比较两个数字

两侧都可以是模板:{y} 无需写代码即可与 62 比较。

  • left text 左侧数值
  • operator < | <= | == | != | >= | > 比较方式
  • right text 右侧数值
chance 随机,X% 的概率
  • percent number [0..100] 百分比

动作

会发生的事。每条规则一到 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 引擎都由它派生而来)。

目录里的标签是法语,因为法语是源语言。标识符不会被翻译:引擎执行的正是 这些标识符。

语法变更时会发生什么

新增一个触发器、条件或动作不会破坏任何东西:之前写好的规格文件照常运行。 而破坏性变更会让 specVersion 递增,这时引擎会 拒绝执行自己不理解的内容,而不是错误地运行它,并会在服务器控制台里说明原因。

上面的网址都带着这个版本号:plugin-1.schema.json 将永远指向这套语法,即便日后出现了另一套语法也是如此。