插件语法
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": 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"
有两点在这些产生式规则里看不出来,因为它们关乎一致性,而不是语法形式。
需要玩家对象的条件或动作,只能写在会提供玩家对象的触发器之下:在
"每 N 秒"之下写"治疗玩家"是没有意义的。
cancel_event 也只能写在可取消的触发器之下。这两条规则都是在
编写时就被拒绝,而不是等到运行时才发现。
模板
text 类型的参数就是模板:
{player} 会被替换成触发器提供的内容,
{{ 会得到一个字面意义上的花括号,颜色代码
&a &l 也会被转换。
触发器不知道的变量是一个错误,而不是一段文本。 每个触发器都会声明自己提供什么内容(见下面的列表):列表之外没有任何 东西可读,拼写错误会在编写时就被拒绝,而不会直接出现在玩家的聊天框里。 触发器本身的参数不是模板:它们在服务器启动时就被读取,那时还 没有任何东西可以替换。
Minecraft 名称有两种写法,取决于它指代的是什么。物品
可以用两种拼写方式(DIAMOND_SWORD 或
diamond_sword)。声音或效果
则要用 Minecraft 键名来指定(entity.player.levelup、
speed),而不是 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 只在启动时接受新的命令名称。规则的内容本身则可以随时热重载。
-
nametext 命令名称,不含斜杠 -
descriptiontext 可选 帮助信息中显示的说明 -
permissiontext 可选 所需权限,留空表示所有人可用
提供:
{player} 输入该命令的玩家,
{world} 该玩家所在的世界,
{args} 命令后面的内容,原样保留
schedule.repeat
每 N 秒
只要服务器在运行,就重复执行该规则。此时不涉及任何玩家:只能使用不需要玩家的动作。
-
secondsinteger [1..86400] 间隔秒数
条件
用来过滤的部分。每条规则零到 20 个,由 match 组合。
player.has_permission
玩家拥有该权限
需要一名玩家
-
permissiontext 权限
player.is_op
玩家是管理员
需要一名玩家
player.in_world
玩家位于该世界
需要一名玩家
-
worldtext 世界名称
player.health_below
玩家生命值低于 X 颗心
需要一名玩家
以生命值计算:20 点等于 10 颗心。
-
valuenumber [0..1024] 生命值点数
text.contains
文本包含
-
texttext 要检查的文本 -
searchtext 要查找的内容 -
ignoreCaseboolean 可选, 默认:true忽略大小写
text.equals
文本等于
-
texttext 要检查的文本 -
valuetext 期望的值 -
ignoreCaseboolean 可选, 默认:true忽略大小写
number.compare
比较两个数字
两侧都可以是模板:{y} 无需写代码即可与 62 比较。
-
lefttext 左侧数值 -
operator< | <= | == | != | >= | > 比较方式 -
righttext 右侧数值
chance
随机,X% 的概率
-
percentnumber [0..100] 百分比
动作
会发生的事。每条规则一到 50 个,按顺序执行。
player.send_message
向玩家发送消息
需要一名玩家
-
messagetext 消息
player.send_actionbar
在动作栏上方显示消息
需要一名玩家
-
messagetext 消息
player.send_title
以大字体显示标题
需要一名玩家
-
titletext 标题 -
subtitletext 可选 副标题 -
fadeInnumber [0..60] 可选, 默认:0.5淡入时间(秒) -
staynumber [0..600] 可选, 默认:3停留时间(秒) -
fadeOutnumber [0..60] 可选, 默认:0.5淡出时间(秒)
player.play_sound
为玩家播放音效
需要一名玩家
-
soundidentifier 音效,使用 Minecraft 键名(entity.player.levelup) -
volumenumber [0..10] 可选, 默认:1音量 -
pitchnumber [0.5..2] 可选, 默认:1音调
player.give_item
给予玩家物品
需要一名玩家
背包放不下的部分会掉落在玩家脚下。
-
materialidentifier 物品(DIAMOND_SWORD) -
amountinteger [1..2304] 可选, 默认:1数量
player.give_effect
为玩家施加效果
需要一名玩家
-
effectidentifier 效果,使用 Minecraft 键名(night_vision) -
secondsinteger [1..86400] 可选, 默认:10持续时间(秒) -
amplifierinteger [0..255] 可选, 默认:0等级,从 0 开始
player.heal
治疗玩家
需要一名玩家
生命值恢复满,饥饿值恢复满。
player.teleport
传送玩家
需要一名玩家
-
xnumber [-30000000..30000000] X 坐标 -
ynumber [-512..1024] 高度 -
znumber [-30000000..30000000] Z 坐标 -
worldtext 可选 世界,留空表示玩家当前所在的世界
player.kick
踢出玩家
需要一名玩家
-
reasontext 显示的原因
broadcast
向全服广播
-
messagetext 消息
server.run_command
以控制台身份执行命令
语法之外的出口:控制台能做到的任何事,这里都能做到。这就是控制台对本服务器拥有的全部权力,请务必在清楚自己在做什么的情况下使用。
-
commandtext 命令,不含斜杠
cancel_event
取消刚刚发生的事
需要可取消的触发器
只存在于可取消的触发器上:消息不会送达,方块不会被破坏。
log
写入服务器控制台
-
messagetext 消息 -
levelinfo | warn 可选, 默认:info级别
delay
等待
暂停规则中后续的动作。处于等待状态的规则不能再取消任何事:「取消」必须放在它之前。
-
secondsnumber [0.05..3600] 秒数
已发布的文件
JSON Schema 在每次部署时都会从目录重新生成,它是机器可读的 参考:遵循它的编辑器能够自动补全标识符,并拒绝未知的参数。
- plugin-1.schema.json :该模式文件,采用 JSON Schema draft 2020-12 规范。
- catalogue-1.json :目录文件本身,是其余一切内容的来源(校验器、模式文件、编辑器积木和 Java 引擎都由它派生而来)。
目录里的标签是法语,因为法语是源语言。标识符不会被翻译:引擎执行的正是 这些标识符。
语法变更时会发生什么
新增一个触发器、条件或动作不会破坏任何东西:之前写好的规格文件照常运行。
而破坏性变更会让 specVersion 递增,这时引擎会
拒绝执行自己不理解的内容,而不是错误地运行它,并会在服务器控制台里说明原因。
上面的网址都带着这个版本号:plugin-1.schema.json
将永远指向这套语法,即便日后出现了另一套语法也是如此。