ScriptPlay(剧本播放)
| 项 | 内容 |
|---|---|
| 契约版本 | scriptplay@1 |
| 当前状态 | 测试中 |
| 源码目录 | src/core/scriptplay/ |
| 页面定位 | 剧本执行参考:IR 如何被推进、指令如何分派、界面与自动播放如何插接 |
ScriptPlay 把剧本 IR 变成一次可存档、可回看、可自动推进的播放过程。它只决定「下一步是什么」与「该由谁执行」,具体画面、声音与文本排版交给对应大插件。
职责
负责:
- 剧本 VM:启动、逐条执行、停止、快照与恢复。
- 指令与条件解析:按命名空间分派、读取旗标、选择分支。
- 章节推进:按插入点排序、跨图跳转、一章结束自动接下一章。
- 演出派发:把演出类指令交给
script:presentation@1的执行者,并逐条等待完成。 - 旗标读写协调:命名空间声明、新写入的所有者校验。
- 内置对话框、选项、历史面板、自动播放与快进策略。
不负责:
- 不绘制背景、立绘、特效;这些由 Stage 的演出执行者完成。
- 不播放 BGM、音效、语音;这些由 Audio 完成。
- 不做文本排版、断行与字体管理;台词经
text:view绘制。 - 不持久化存档、不登记旗标归属;这些由 Save 完成。
- 不决定外壳如何展示菜单、设置或存档界面,只发
platform:on-ui-request。
对外接口
服务
| 名称 | policy | 说明 |
|---|---|---|
script:vm | trusted | start(图 ID, 种子?)、play()、stop()、snapshot()、restore(快照)、notices();给了种子即新开一局 |
script:flow | trusted | chapters() 按插入点排序并标出 requires 是否成立;hidden() 列出锚点缺失而暂不出现的章节;play(章节 ID) 一章播完自动接下一章;resume() 从恢复的位置继续 |
state:flags | trusted | 旗标读写;新写入须有单元在 flagNamespaces 中声明所属命名空间;core/sui 只由框架写 |
三个服务均为 trusted:Mod 对它们 intercept 或在 isolate 域内 provide,与改写机制插槽一样须提权并审计。
机制插槽
| 名称 | policy | 说明 |
|---|---|---|
script:resolve-command | trusted | 输入指令,输出处理函数;默认按命名空间查 script:command-set@1 |
script:resolve-condition | trusted | 输入条件,输出布尔;默认只读 state:flags |
script:resolve-presentation | trusted | 输入演出类指令,输出执行函数 (args, { signal, fast });默认按指令名查 script:presentation@1,查不到即留降级记录照常推进 |
script:resolve-flow | trusted | 输入候选出边,输出所选边;结果必须属于本次候选集合,否则告警并按默认规则重选;默认带权重抽取(未写权重按 1 计),否则取第一条 |
script:resolve-presentation 是演出接缝的核心:它只负责「找执行者」,不保证执行者存在。缺失执行者时剧本继续推进,降级记录留给调用方查询。
通知
| 名称 | 载荷 | 触发时机 |
|---|---|---|
script:on-command | 指令 | 指令执行 |
script:on-scene-enter | 场景信息 | 进入节点 |
script:on-choice | 选项信息 | 做出选择 |
script:on-flow-end | { status: 'ended', at }、{ status: 'halted', code, op, at } 或 { status: 'error', detail, at? },可选 chapter 表示当时的章节 ID | 由 core:story 发起的播放结束、遇到不支持指令或出错;主动停播不广播 |
script:on-advance | { at, line: { speaker?, text, voice? } } | 台词推进 |
通知只广播事实,返回值不改变 VM 的推进结果。某个监听器同步抛错或异步拒绝时只记录告警,其他监听器仍会收到通知,也不会改变本体流程。
实现位与 registry
| 名称 | 类别 | policy | 基数/默认 | 说明 |
|---|---|---|---|---|
script:command-set@1 | 实现位 | public | multi/— | 按命名空间分派;内置条目 core,不随扩展卸载 |
script:presentation@1 | 实现位 | public | multi/— | 演出执行者 { run(args, { signal, fast }) },实现名即指令名 |
dialog:renderer@1 | 实现位 | public | single/builtin | 对话框渲染器;卸载回落内置条目 |
choice:renderer@1 | 实现位 | public | single/builtin | 选项渲染器;卸载回落内置条目 |
history:view@1 | 实现位 | public | single/builtin | { render(container, { close }) → 撤销 };外壳在模态层开容器交给它画 |
autoplay:policy@1 | 实现位 | public | single/builtin | { get() → { mode, delayMs, fastUnread }, set(部分字段, 原因?), subscribe(listener) } |
script:graphs | registry | public | — | 剧本图,按 IR v1 校验;正文 id 须与条目 ID 一致 |
script:chapters | registry | public | — | 章节序列;成环即 LOADER_REGISTRY_CYCLE |
Stage 向 script:presentation@1 登记 core:bg/core:cast/core:effect/core:transition/core:camera;Audio 登记 core:bgm/core:se。实现名就是指令名,VM 按完整指令名分派。
剧情界面
ScriptPlay 向 Platform 的 ui:screen@1 登记内置 core:story。导航打开时只接受 { chapter: <章节 ID> } 或 { resume: true };其余参数报错。界面挂载时按参数播放,撤销时停播。舞台与对话框仍在 platform:host.surface() 中绘制,不由导航容器重复渲染。
外壳通过 script:on-flow-end 接收播放结果并展示后续操作;通知本身不替外壳决定跳转。主页、设置与返回键的关系见 Platform。
关键语义
播放与停止
text
start(图 ID, 种子?) → 初始化 VM 与随机数状态
play() → 从当前节点逐条执行,遇到等待点暂停
stop() → 可中断等待点击、选项或延时
snapshot() → 当前节点、旗标视图、随机数状态等
restore(快照) → 先整体校验,再替换当前状态stop()同时取消章节衔接;即使在两章之间调用,也不会继续接下一章。stop()返回Promise<void>;命令上下文提供只读signal,say(line, signal?)与choose(request, signal?)的第二参数供渲染器清理界面。- 旧渲染器不处理该参数也不会阻止 VM 停止,但它自己的界面只能由其实现负责清理。
- 停止后的旧命令上下文不能再写旗标、选择分支或发送推进通知;这不是对扩展自行持有的外部引用或任意代码的沙箱保证。
restore在改变当前状态前检查整个快照;损坏的位置、旗标或随机数状态不会部分覆盖现有进度。- 恢复旧存档不丢弃缺失单元的旗标;对其新写入仍须等待所有者装载。
章节与恢复
- 章节播放把身份保存到可选的
position.chapter,跨图跳转保留它。 resume()在章节结束后自动接下一章;缺失已记录的章节会明确报错。- 没有章节身份的旧存档只继续当前图,不根据图名反推章节。
chapters()标出requires是否成立;hidden()列出锚点缺失而暂不出现的章节,避免把「暂时不可见」误判为「不存在」。章节条目先按 IR 的条件深度与文本预算核对;排序按前挂、章节本身、后挂子树组成连续组,使用显式栈一遍排出,避免深链递归与重复扫描。
旗标
- 新写入须有单元在清单
flagNamespaces中声明所属命名空间;core/sui保留给框架。 - 单元卸载只把归属标为孤儿,不删除任何进度;Save 记录稳定 ID、版本、数据块、字节数、首次写入与最后认领时间。
- 缺失单元的旗标在存档中保留原值;恢复后对其新写入仍须等待所有者装载。
演出派发
- VM 逐条等待演出指令完成;执行者报错即留降级记录、照常推进。
- 快进中
ctx.fast为真;Stage 的执行者据此把过渡与镜头动作按ms: 0立即完成。 script:resolve-presentation可以由 Mod 改写,用于替换或包裹某条演出指令;改写同样受trusted的提权与审计约束。
已读、快进与自动
- 台词的键为
<剧本图>#<节点>#<指令位置>;推进即经 Save 的progress:stats.markRead记为已读。没有 Save 时一律视为未读。 - 快进只跳已读:
fastUnread为假时遇到未读台词即以原因unread切回off;遇到选项以原因choice切回off,自动保持。 set的第二参数(原因)缺省user;VM 自动停下时传unread/choice。- 自动模式在文字画好、语音结束(或没有语音)后等
delayMs推进;玩家点击照常立即推进。快进中不播语音,文字画好即推进。 - 音频未解锁时语音会被 Audio 丢弃,这一句按没有语音处理,不等结束事件。
- 快进因未读停下时,说话人旁标「★ 未读新台词」,对话框上方说明「遭遇未读新剧本 · 快进已自动停止」,推进即消失。
对话框、历史与工具条
- 内置对话框与选项只绘制到
platform:host.surface();文本经text:view,菜单请求经platform:on-ui-request。 - 内置对话框遇到带
voice的台词经audio:channel.playVoice播放,推进、停止或读档时即停;没有 Audio 时照旧标出「音频暂未提供」。 history:view@1的容器由外壳在模态层打开;内置面板打开即停在最新一条,旁白不画说话人、加引用线。有语音的条目可重播(再点一次打断),往上滚出现「返回最新台词」;换语言按当前语言重画,关闭时停掉正在重播的语音。- 内存中保留最近 500 条推进过的台词,同一次游玩内跨章节累加;新开一局(
start带种子)与读档时清空。 - 历史条目及其中的文本段是深冻结的副本;监听器出错不会改变剧本推进。
- 工具条有 AUTO、SKIP、LOG、存读档、隐藏、设置六个按钮。AUTO/SKIP 改
autoplay:policy@1的模式并高亮当前模式;LOG 与设置经platform:on-ui-request的history/settings请外壳开面板。快捷键 A/S/L 对应自动、快进、历史;Esc 先停自动或快进,都没开时才请外壳开菜单。没有自动策略时 AUTO/SKIP 置灰。 - 已接入键盘、150 毫秒防连击、隐藏后只恢复、禁用选项提示、语言重绘与停止清理。
当前实现范围
已完成:
- VM、旗标、章节推进、内置指令集与四个机制插槽;契约用例在 Node 与系统 Edge 中通过。
- 对话框、选项、历史、自动与快进的内置实现,以及工具条交互。
- Windows WebView2 已通过停止、渲染器、章节与 Save 的 30 项相关契约用例;网页已实测完整的分支存读与接章流程。
- 第二个对话渲染器夹具以普通插件登记卡片外观,经 Cordis 取得播放区与文字服务,不改变剧本或 VM;相关三项契约在系统 Edge 与 Windows WebView2 通过。
未完成/限制:
- Phase 2 已完成;两条真实分支的播放、自动推进与演出派发已随原生出口核验。
- 旧命令上下文只做流程级失效,不构成对任意代码的沙箱。
- 历史仅保留内存中的最近 500 条,不跨进程持久化。