剧本 IR v1 参考
一份 IR 文档就是一张剧本图:UTF-8 JSON,描述节点拓扑与节点内的线性指令流。剧本图作为数据包条目写入 script:graphs,装载时按本页所列规则校验。
字段与约束的权威来源是 src/shared/ir.ts;对外发布的 schema/ir-1.schema.json 由它序列化生成。两者不一致时以代码为准,并通过反馈渠道报告。
顶层对象
| 字段 | 类型 | 必填 | 约束与含义 |
|---|---|---|---|
ir | string | 是 | 文档实际使用的版本,写作 主.次,如 "1.0"、"1.1";不得写范围 |
id | string | 是 | 剧本图的命名空间 ID,写作 <ns>:<局部名>;须与 registry 条目 ID 一致 |
entry | string | 是 | 入口节点 ID;必须存在于 nodes |
nodes | object | 是 | 节点 ID → { "body": 指令[] } |
edges | array | 是 | 边数组;可以为空 |
顶层不允许其他字段。ir 的次版本只增加可选字段与新指令;主版本才允许破坏兼容。用到 core:cast.params 的图须写 "1.1","1.0" 的图照旧有效。
节点与指令
节点只承载指令流,拓扑一律写在 edges 里:
{
"start": {
"body": [
{ "op": "core:say", "args": { "text": "雨停了。" } },
{ "op": "core:end" }
]
}
}每个节点必须带 body 数组,可以为空。节点体按顺序执行;执行完后按出边求值。节点字段名是 body;schema 里的 run 是文本段定义,不是节点字段。
指令对象的字段:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
op | string | 是 | 写作 <ns>:<名>,如 core:say;局部名须匹配 [a-z][a-z0-9-]{0,63};命名空间只能由已装载单元提供 |
args | object | 否 | 参数;各内置指令的形状见下表 |
label | string | 否 | 节点内标签,供 core:jump/core:jump-if 跳转;同一节点内不得重复 |
optional | true | 否 | 指令没有提供者时跳过并留提示;不写则停止剧本并报 SCRIPT_UNKNOWN_COMMAND |
节点 ID、边 ID 与标签取 1–128 个字符,不含空白与 #。跨节点只能走边;节点内跳转只能落到本节点标签,不能借标签跳到另一节点。
内置指令参数
| 指令 | 参数 | 要点 |
|---|---|---|
core:say | { speaker?: 文本或 null, text: 文本, voice?: VPath } | speaker 为空或 null 表示旁白;voice 指向语音资源 |
core:choice | { prompt?: 文本, options: [{ text, edge }] } | 至少 1 项、至多 64 项;edge 为边 ID 或 "continue" |
core:set | { ops: 旗标操作[] } | 至少 1 项、至多 1,024 项 |
core:jump | { to: 标签 } | 跳到同一节点内的标签 |
core:jump-if | { when: 条件, to: 标签 } | 条件成立才跳转 |
core:wait | { ms: 非负整数 } | 毫秒 |
core:end | {} | 结束本次播放 |
core:bg | { asset: VPath 或 null, transition?: 过渡名, ms? } | null 清空背景 |
core:cast | { id, action, asset?, position?, layer?, params?, transition?, ms? } | action 取 show/hide/change/move;params 序列化后不超过 4 KiB |
core:bgm | { action: play/stop, asset?, loop?, volume?, fadeMs? } | play 必须给 asset;volume 取 0–1 |
core:se | { asset: VPath, volume? } | 音效 |
core:effect | { name: 命名空间名, target?: "screen" 或登场者 ID, params? } | 效果由实现位解释 |
core:transition | { name: 命名空间名, ms? } | 整屏转场 |
core:camera | { action: shake/zoom/pan/reset, params?, ms? } | 镜头动作 |
core:cast 的 show 必须给 asset;change 在 schema 层面要求 asset 与 params 至少一个;hide/move 不带 params。不在场的 id 收到 change 时按 show 处理,此时 asset 必填。
core:bg、core:cast、core:bgm、core:se、core:effect、core:transition、core:camera 属演出类指令:在对应执行者就位前按「已识别、暂无执行者」处理,照常推进并留降级记录。
边与流程
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | string | 否 | 边 ID;同图内唯一,不得取 continue;选项引用边时必须提供 |
from | string | 是 | 出发节点 ID,必须存在 |
to | string | 是 | 本图节点 ID,或 <图 ID>#<节点 ID> 的跨图目标 |
when | 条件 | 否 | 条件不成立时该边不可选 |
weight | number | 否 | 正数;参与加权抽取 |
once | boolean | 否 | 走过一次后不再参与求值;记录存于保留命名空间 core |
unmet | hide/disable | 否 | 选项所引用的边条件不成立时的表现,缺省 hide |
unmetText | 文本 | 否 | disable 时显示的提示;未写则显示「条件未达成」 |
节点体执行完、且没有经选项离开时,按边在数组中的书写顺序收集候选:from 为当前节点、when 成立、且 once 边未走过。候选中只要有一条带 weight,就按权重抽取,未写权重的候选按 1 计;否则取第一条。没有候选即本图结束,交回章节流程。抽取使用的随机状态随存档保存,读档后结果一致。
script:resolve-flow 可以改写默认选边,但它返回的边必须属于本次过滤后的候选集合;返回其他边时记录告警并按默认规则重选。
选项不携带拓扑:core:choice 的每个选项只引用一条边的 ID,或写 "continue" 继续本节点后续指令。边 ID 不得取 continue,它是保留字。
条件与旗标
条件只读 state:flags,不内嵌求值实现:
| 形态 | 字段 | 说明 |
|---|---|---|
| 比较 | { "flag": 命名空间名, "op": ..., "value"?: ... } | op 取 eq/ne/lt/le/gt/ge/set/unset |
| 全部成立 | { "all": 条件[] } | 至少 1 项 |
| 任一成立 | { "any": 条件[] } | 至少 1 项 |
| 取反 | { "not": 条件 } | 单层取反 |
eq/ne 必须给 value;lt/le/gt/ge 必须给数字 value;set/unset 不得给 value。条件嵌套深度不超过 16 层。
旗标操作的字段为 { flag, op, value? },op 取 set/add/toggle/unset。set 必须给值,add 必须给数字,toggle/unset 不得给值。旗标值只允许布尔、有限数字与字符串;旗标名带命名空间,写入前须有单元在清单的 flagNamespaces 中声明该命名空间。
文本的三种形态
| 形态 | 示例 | 用途 |
|---|---|---|
| 字符串 | "text": "雨停了。" | 内联叙事;不解析任何标记,换行写 \n |
| 文本段数组 | [{ "text": "旧钟楼", "bold": true, "color": "accent" }] | 粗体与语义色;bold 只能为 true,color 取 accent/success/warning/danger/neutral |
| 键引用 | { "key": "example:arrival.open" } | 引用资源包 lang 表,供只换文字、不改剧本的场景使用 |
键引用查不到时原样显示 <ns>:<键>、记一条降级记录,剧本照常推进。台词、说话人、选项文字与提示文字都使用同一套文本形态。
命名空间与 VPath
| 对象 | 规则 |
|---|---|
| 命名空间名 | [a-z0-9][a-z0-9_.-]{0,63},即小写字母或数字开头,可含小写字母、数字、_、.、- |
| 跨文档 ID | <ns>:<局部名>;局部名 1–256 字符,不含空白与 # |
| 节点/边/标签 ID | 1–128 字符,不含空白与 # |
| 保留命名空间 | core 与 sui 由框架保留,单元不得创建条目 |
| VPath | assets/<ns>/<类型>/<路径>;前两段各匹配 [a-z0-9][a-z0-9_.-]{0,63},末段 1–512 字符且不含空白;完整 pattern 以 schema/ir-1.schema.json 为准 |
assets/sui/ 是安全关键界面的保留命名空间,任何单元包含其下文件都会被拒绝装载。资源包通过 VPath 覆盖表现资产;已登记的界面皮肤可使用受校验的控件外观与位置属性,但不能改写交互行为。皮肤不属于剧本 IR 指令,具体边界见 Platform。地图表现属于资源包,地图逻辑属于数据包。
校验预算
装载数据包时先查预算,再查 schema,最后查跨字段引用:
| 项目 | 上限 |
|---|---|
| 单图节点数 | 10,000 |
| 单节点指令数 | 10,000 |
| 全图指令总数 | 1,000,000 |
| 边数 | 50,000 |
| 单条文本 | 16 KiB(UTF-8 字节) |
| 条件嵌套深度 | 16 |
core:choice 选项数 | 64 |
core:set 操作数 | 1,024 |
core:cast.params 序列化体积 | 4 KiB |
超出预算以 GUARD_BUDGET_EXCEEDED 拒绝;结构或引用不合法以 LOADER_MANIFEST_INVALID 拒绝。跨字段检查包括:entry 必须存在;边的 from 必须存在;本地 to 必须存在;边 ID 不得重复;选项引用的边必须存在且从本节点出发;节点内标签不得重复,跳转目标必须存在。
最小可读示例
{
"ir": "1.0",
"id": "example:arrival",
"entry": "start",
"nodes": {
"start": {
"body": [
{ "op": "core:say", "args": { "speaker": "信使", "text": "你想走哪条路?" } },
{ "op": "core:choice", "args": {
"options": [
{ "text": "沿河边走", "edge": "river" },
{ "text": "打开锁着的捷径", "edge": "shortcut" }
]
} }
]
},
"river": { "body": [ { "op": "core:end" } ] },
"shortcut": { "body": [ { "op": "core:end" } ] }
},
"edges": [
{ "id": "river", "from": "start", "to": "river" },
{ "id": "shortcut", "from": "start", "to": "shortcut",
"when": { "flag": "example:key", "op": "set" },
"unmet": "disable", "unmetText": "需要钟楼钥匙" }
]
}该示例可直接作为数据包条目 data/script/graphs/example/arrival.json 的 value,条目 ID 为 example:arrival,与图内 id 一致。
未知指令与版本演进
| 情形 | 行为 |
|---|---|
指令的命名空间无人提供,且未写 optional | 剧本停止,返回 SCRIPT_UNKNOWN_COMMAND |
指令的命名空间无人提供,写了 "optional": true | 跳过该指令并留一条提示,继续执行 |
| 演出类指令已识别、但执行者未就位 | 照常推进并留降级记录 |
| 数据包声明的 IR 兼容范围不受支持 | 装载拒绝,报 GUARD_IR_UNSUPPORTED |
| 编辑器遇到更高的 IR 主版本 | 明确报错,不静默降级 |
IR 的演进遵循两条线:图内的 ir 是文档实际版本,清单的 ir 是兼容范围(如 ^1)。次版本只增加可选字段与新指令;新增必填字段、改变既有语义或删除字段属于主版本变更。装载器按当前支持的版本校验,不猜测未来字段的含义。
相关页面:剧本 IR 概念、包清单与目录、原因码、装载管线与 guard、ScriptPlay。