剧本 IR
内部剧本 IR 采用混合形态:节点图 + 节点内指令流。节点图负责场景、章节与路线的拓扑;指令流负责一个节点内部的线性执行。两层各管一件事,不互相嵌套实现细节。
相关页面:ScriptPlay 大插件、剧本 IR v1 Schema、翻译层、包清单与目录。
两层结构
剧本图(graph)
├─ 节点 start
│ └─ 指令流:指令 1 → 指令 2 → 指令 3 → …
├─ 节点 hall
│ └─ 指令流:…
└─ 边
├─ start → hall 条件:flag(demo:met) == true,权重 1,一次性
└─ start → stay 未满足时:隐藏 / 禁用| 层 | 职责 | 主要使用方 |
|---|---|---|
| 节点图 | 场景、章节、路线的拓扑;边携带条件、权重、一次性标记 | Author(编辑)、ScriptPlay(推进) |
| 指令流 | 节点内的线性执行:台词、演出、变量操作、条件跳转 | Translate(转换)、ScriptPlay(执行) |
一次推进的典型顺序是:VM 进入节点,顺序执行指令流;遇到选项或跳转时,按边选择下一个节点;跨节点的状态写入统一落到旗标服务。
IR1–IR5
| 编号 | 规则 | 可操作的判断 |
|---|---|---|
| IR1 | 节点图与指令流必须分层 | 图里不写指令流实现细节;指令里不嵌整套子图。需要跨节点时用边或跳转,不把另一个节点塞进指令参数 |
| IR2 | 指令集必须可扩展 | 内置指令由 script:command-set@1 的 core 条目提供;扩展指令经 script:resolve-command(trusted)解析 |
| IR3 | IR 必须为多输入源留形 | 不得出现「只有某个输入源才能满足」的结构前提;格式差异留在翻译层 |
| IR4 | 包清单必须声明 IR 版本 | 数据包与资源包的 manifest 写 ir(如 ^1);不声明就无法判定兼容,装载器按不支持处理 |
| IR5 | 跨节点变量与旗标一律经 state:flags | IR 只表达「读写哪个旗标」,不内嵌求值器、脚本片段或宿主函数 |
v1 的形状
顶层是剧本图对象:
| 字段 | 类型 | 说明 |
|---|---|---|
ir | string | 文档实际使用的版本,形如 1.0、1.1;不得写范围 |
id | string | 命名空间化的图 ID;必须与注册条目 ID 一致 |
entry | string | 入口节点 ID |
nodes | object | 节点 ID → { body: Instruction[] } |
edges | array | 边列表 |
节点只有 body,即指令数组。节点不直接包含下一个节点的内容;拓扑只由边表达。
边
| 字段 | 必填 | 说明 |
|---|---|---|
id | 否 | 边 ID;选项通过它指向具体边 |
from | 是 | 起点节点 ID |
to | 是 | 终点节点 ID,或 <图 ID>#<节点 ID> 的跨图形式 |
when | 否 | 条件;不满足时按 unmet 处理 |
weight | 否 | 权重,必须大于 0;未写按 1 计 |
once | 否 | 一次性标记 |
unmet | 否 | 条件不满足时的表现:hide(隐藏)或 disable(禁用) |
unmetText | 否 | 禁用时展示的文本 |
默认选边规则由 script:resolve-flow 决定:带权重抽取(未写权重按 1 计),否则取第一条候选。机制插槽可以改写选边,但返回值必须仍是本次 when/once 过滤后的候选之一;不在候选集合中的结果按默认规则重选。机制插槽本身仍必须按 trusted 口径提权并审计。
指令
| 字段 | 必填 | 说明 |
|---|---|---|
op | 是 | 命名空间化指令名,如 core:say |
args | 视指令 | 参数对象;参数形状由 schema 按 op 校验 |
指令名必须是 命名空间:名称 形式。命名空间既用于分派,也用于避免扩展指令与内置指令撞名。
条件
条件只读旗标,不执行任意代码:
| 形式 | 含义 |
|---|---|
{ flag, op: "eq"|"ne", value } | 等于/不等于;必须给 value |
{ flag, op: "lt"|"le"|"gt"|"ge", value } | 数值比较;value 必须是数字 |
{ flag, op: "set" } | 旗标已设置 |
{ flag, op: "unset" } | 旗标未设置 |
{ all: Condition[] } | 全部成立 |
{ any: Condition[] } | 任一成立 |
{ not: Condition } | 取反 |
条件嵌套深度有上限,避免用超深布尔树制造解析与求值压力。
旗标
- 旗标值只能是布尔、数字或字符串。
- 旗标名必须是命名空间形式;写入方须在
flagNamespaces中声明所属命名空间。 core与sui命名空间只由框架写入,扩展不得占用。- 指令流中的旗标操作有
set、add、toggle、unset;set与add必须给值。 - 跨节点的状态一律经
state:flags服务读写;IR 里不出现求值实现。
文本与文本段
台词文本有三种形态:
| 形态 | 说明 |
|---|---|
| 字符串 | 原样显示,不解析任何标记语法 |
| 文本段数组 | 每段为 { text, bold?, color? };bold 只能为 true,颜色取语义色 |
{ key } | 引用资源包 lang 文本表中的键 |
语义色只有 accent、success、warning、danger、neutral,不写具体色值。字符串与文本段都不解析 Markdown、HTML 或自定义标签;需要强调时用文本段,不能靠内嵌标记。
单条文本(包括整段文本段数组)有字节上限。{ key } 让同一份剧本按当前语言取词,切换语言时重画,而不必改动 IR。
一个最小示例
{
"ir": "1.0",
"id": "demo:prologue",
"entry": "start",
"nodes": {
"start": {
"body": [
{ "op": "core:set", "args": { "ops": [
{ "flag": "demo:met", "op": "set", "value": true }
] } },
{ "op": "core:say", "args": {
"speaker": "旁白",
"text": [
{ "text": "风从走廊尽头吹来。", "color": "neutral" },
{ "text": "她停下了脚步。", "bold": true }
] } },
{ "op": "core:choice", "args": {
"prompt": "要过去吗?",
"options": [
{ "text": "过去", "edge": "go" },
{ "text": "留下", "edge": "stay" }
] } }
]
},
"hall": { "body": [
{ "op": "core:say", "args": { "text": { "key": "demo:hall-line" } } },
{ "op": "core:end" }
] },
"stay": { "body": [{ "op": "core:end" }] }
},
"edges": [
{ "id": "go", "from": "start", "to": "hall",
"when": { "flag": "demo:met", "op": "eq", "value": true },
"weight": 1, "once": true },
{ "id": "stay", "from": "start", "to": "stay",
"unmet": "disable", "unmetText": "条件未满足" }
]
}这个例子包含:三个节点、两条边(一条带条件与一次性标记,一条带禁用态)、一次旗标写入、一条带文本段的台词、一次选项分派,以及一个语言键引用。
内置指令集与扩展
script:command-set@1 是分命名空间的实现位,内置条目为 core:
| 类别 | 指令 |
|---|---|
| 流程 | core:say、core:choice、core:jump、core:jump-if、core:wait、core:end |
| 状态 | core:set |
| 演出 | core:bg、core:cast、core:bgm、core:se、core:effect、core:transition、core:camera |
演出类指令不是由 VM 自己画:它们经 script:resolve-presentation 分派到 script:presentation@1。Stage 登记画面相关指令,Audio 登记音频相关指令;没有对应实现时留下降级记录并照常推进,不把整段剧本卡死。
扩展指令:
- 经
script:resolve-command(trusted)解析;默认按命名空间查script:command-set@1。 - 条件求值经
script:resolve-condition(trusted),默认实现只读state:flags。 - 选边经
script:resolve-flow(trusted),默认实现执行带权重抽取。 - 任何改写这些入口的单元都必须按
trusted口径提权并审计;这不是普通插件可以静默替换的位置。
校验、预算与版本
校验分两层:
- 结构校验:按 IR v1 schema 检查字段、类型、必填项与指令参数形状。
- 跨字段校验:schema 表达不了的引用、标签与预算,在装载校验中补查。
主要预算:
| 项目 | 上限 |
|---|---|
| 节点数 | 10,000 |
| 单节点指令数 | 10,000 |
| 全图指令总数 | 1,000,000 |
| 边数 | 50,000 |
| 单条文本 | 16 KiB |
| 条件嵌套深度 | 16 |
| 单次选项数 | 64 |
| 单次旗标操作数 | 1,024 |
预算在结构校验之前先做计数,避免畸形文档耗尽解析资源。章节条目复用同一套条件深度与文本预算。超限按装载失败处理,不截断、不猜测。
版本协商由 manifest 的 ir 字段承担。装载器只接受支持的版本范围;不声明或超出范围即拒绝,并给出明确原因。完整字段与约束见剧本 IR v1 Schema,装载失败原因见装载原因码。
相关边界
- IR 不含实现:不嵌入宿主函数、脚本片段或求值器;条件与旗标语义由框架服务解释。
- IR 不含格式知识:某个输入源的字段名、指令名与编码只存在于翻译规则与翻译器,不进入内部协议。
- IR 不承诺布局:文本段只给语义样式,具体字体、断行与排版由 Text 与外观层决定。
- 图 ID 与注册条目一致:不一致即拒绝,避免引用与配置错位。
参见
- 翻译层:外部剧本如何被转换为 IR。
- ScriptPlay:VM、指令分派、条件求值与章节推进。
- 插槽与实现位:
script:command-set@1与script:resolve-command的关系。 - 剧本 IR v1 Schema:逐字段参考。
- 包清单与目录:
ir版本声明的落点。