460 lines
18 KiB
Markdown
460 lines
18 KiB
Markdown
# ichni Official 剧本编写与 Yarn Script 制作手册
|
||
|
||
> 适用对象:剧本编写者、叙事设计师、负责初步拆分剧情 Block 的策划
|
||
> 适用项目:`ichni Official`
|
||
> 剧情工具:Yarn Spinner `3.2.1` + Unity Localization
|
||
> 最后更新:2026-07-21
|
||
|
||
本文说明如何把一段剧情写成可交付给程序员的 Yarn Scripts。本文只描述当前项目已经存在的语法和运行规则;所有玩家可见文本使用 Unity Localization,也不要使用 Yarn 原生临时变量保存本项目的剧情进度。
|
||
|
||
## 1. 先理解游戏中的剧情结构
|
||
|
||
一个章节由一份 `StoryData` 定义。`StoryData` 中预先排好完整剧情路线,运行时根据玩家进度把每个 Block 显示为以下状态之一:
|
||
|
||
- `Locked`:前置流程或额外条件尚未满足,不能点击。
|
||
- `Current`:当前可进入。
|
||
- `Completed`:已经完成,仍可点击回顾。
|
||
- `Forbidden`:玩家的分支选择已经排除了这条路线;Block 保留显示,但不能点击。
|
||
|
||
Block 有三种类型:
|
||
|
||
| 类型 | 用途 | 剧本编写者需要提供的内容 |
|
||
|---|---|---|
|
||
| `TextBlock` | 播放 Yarn 对话 | Yarn 文件、入口 Node、Block 标题、重要性、变量与分支说明 |
|
||
| `SongBlock` | 进入指定歌曲的选曲界面 | 目标歌曲及它在叙事中的意义;不需要单独写 Yarn |
|
||
| `TutorialBlock` | 弹出“游玩/跳过教程”的选择 | 教程前后文案与目标教程;不需要单独写 Yarn |
|
||
|
||
`TextBlock` 又分为:
|
||
|
||
- `Important`:主线转折、重要选择、适合作为 Timeline 回滚点的内容。
|
||
- `Secondary`:补充信息、支线或非关键叙事。
|
||
|
||
剧情图不是运行时自动排版。策划和程序员会把所有可能路线预先放入 `StoryData`;玩家的选择通过章节变量、`Unlock Condition` 和 `Forbidden Condition` 动态开放或禁用对应 Block。
|
||
|
||
## 2. 存档与对话事务规则
|
||
|
||
每次进入一个 `TextBlock`,系统会建立一份“本次对话事务”:
|
||
|
||
1. 进入前复制当前章节剧情存档。
|
||
2. 对话中的 `set_*`、选项记录和 Block 进度先只修改内存。
|
||
3. 对话正常走到结束时,一次性提交本次变化并把 TextBlock 标记为 `Completed`。
|
||
4. 玩家中途按 `Back`,或 Yarn 启动失败时,恢复进入前的章节快照。
|
||
|
||
因此:
|
||
|
||
- 没有正常结束的对话不会保存其中的选项和剧情变量。
|
||
- 再次进入已完成的 TextBlock 时,已经记忆的选项会自动采用,不再显示选择页面。
|
||
- Timeline 回滚到某个 Marker 时,会恢复该列之前的 Block、选项和章节变量。
|
||
- Timeline 回滚不会撤销歌曲成绩、`isTried`、设置、Offline 解锁 Key 或其它章节。
|
||
- `grant_unlock`、`unlock_song` 和 `show_message` 会等到对话正常提交后才执行;中途退出不会留下全局解锁或孤立弹窗。
|
||
|
||
## 3. 命名规范
|
||
|
||
稳定 ID 一旦进入测试存档就不要随意重命名。玩家可见文本必须进入 Unity Localization;稳定 ID 不翻译。
|
||
|
||
| 对象 | 规范 | 示例 |
|
||
|---|---|---|
|
||
| Chapter Index | 必须与 `ChapterSelectionUnit`、`StoryData`、`StoryManager` 完全一致 | `Chapter0` |
|
||
| StoryData | `StoryData_Chapter{n}` | `StoryData_Chapter0` |
|
||
| Yarn Project | `Chapter{n}.yarnproject` | `Chapter0.yarnproject` |
|
||
| Yarn 文件 | `C{n}_{主题}.yarn` | `C0_SignalCheckpoint.yarn` |
|
||
| Yarn 入口 Node | `C{n}_{Block用途}`,在整个 Yarn Project 内唯一 | `C0_SignalCheckpoint` |
|
||
| Yarn 内部 Node | 在入口名后增加语义后缀 | `C0_SignalCheckpoint_Record` |
|
||
| Block ID | 小写英文、数字、下划线;建议包含章节、类型和用途 | `c0_text_signal_checkpoint` |
|
||
| 章节变量 | `c{n}_{范围}_{事实}`,使用小写英文、数字和下划线 | `c0_signal_route` |
|
||
| Timeline Marker ID | `c{n}_marker_{时间或事件}` | `c0_marker_day_01` |
|
||
| Block 标题本地化 Key | `story_c{n}_block_{用途}_title` | `story_c0_block_signal_title` |
|
||
| Timeline 文本 Key | `story_c{n}_timeline_{时间或事件}` | `story_c0_timeline_day_01` |
|
||
| Character ID | 与 Yarn 台词冒号前的名字完全一致,区分大小写 | `Asahi` |
|
||
| Character Name Key | `character_{角色}_name` | `character_asahi_name` |
|
||
| Unlock Key | 遵守 Offline Key 规范:小写英文、数字、下划线,以字母开头 | `story_ch0_prologue_completed` |
|
||
|
||
不要使用 `Record`、`Start`、`End` 这类过短的 Yarn Node 名。Node 名在同一 Yarn Project 内不是文件局部名称,多个文件出现同名 Node 会冲突。
|
||
|
||
变量应描述已经发生的事实,而不是按钮动作:
|
||
|
||
- 推荐:`c0_signal_observed`、`c0_signal_route`、`c0_intro_read`。
|
||
- 不推荐:`clicked_left`、`do_next`、`temp`、`choice1`。
|
||
|
||
Bool 变量尽量使用正向语义,例如 `c0_tutorial_resolved`,避免 `not_skipped` 这类双重否定。Int 路线编码必须在交付说明中列出每个数字的含义,例如 `0 = 未选择,1 = 观测,2 = 保护`。
|
||
|
||
## 4. Yarn 文件的基本结构
|
||
|
||
最小可运行 Node:
|
||
|
||
```yarn
|
||
title: C0_Intro
|
||
---
|
||
SLS: 记录开始。
|
||
Asahi: 我已经准备好了。
|
||
===
|
||
```
|
||
|
||
规则:
|
||
|
||
- `title:` 后是 Node 名,必须在当前 Yarn Project 内唯一。
|
||
- `---` 表示正文开始。
|
||
- `===` 表示 Node 结束。
|
||
- `//` 是只给作者和程序员看的注释。
|
||
- `Character: 台词` 中冒号前的名称会成为 `CharacterName`。
|
||
- 没有角色名前缀的文本按旁白处理。
|
||
- 不要手工删除或复用 `#line:xxxxxxx`。这些 Line ID 由 Yarn 工具添加,是本地化和选项记忆的稳定身份。
|
||
|
||
一个文件可以包含多个 Node,但建议“一份入口剧情 + 它的内部小分段”放在同一文件;跨 Block 的入口 Node 则分文件保存。
|
||
|
||
## 5. 流程、跳转与选项
|
||
|
||
### 5.1 跳转
|
||
|
||
```yarn
|
||
title: C0_Intro
|
||
---
|
||
SLS: 先从这里开始。
|
||
<<jump C0_Intro_Continue>>
|
||
===
|
||
|
||
title: C0_Intro_Continue
|
||
---
|
||
Asahi: 我们继续吧。
|
||
===
|
||
```
|
||
|
||
`<<jump NodeName>>` 会转到另一个 Node。目标必须存在,名称必须完全一致。
|
||
|
||
`<<stop>>` 会立即结束当前 Yarn 对话。只有确认这就是本 TextBlock 的正常终点时才使用;正常结束会提交事务并完成 Block。
|
||
|
||
### 5.2 玩家选项
|
||
|
||
```yarn
|
||
SLS: 你打算怎样处理信号?
|
||
-> 记录信号的变化
|
||
<<set_int "c0_signal_route" 1>>
|
||
<<jump C0_SignalCheckpoint_Record>>
|
||
-> 优先保护观测装置
|
||
<<set_int "c0_signal_route" 2>>
|
||
<<jump C0_SignalCheckpoint_Protect>>
|
||
```
|
||
|
||
注意:
|
||
|
||
- 选项正文必须缩进。
|
||
- 当前 `DialogUIPage` 只有 4 个 `ChoiceButton`,一个选择组最多写 4 个可显示选项;需要更多时先与程序员确认 UI 扩容。
|
||
- 选项的 Line ID 会被保存。重新排序选项不会改变记忆;增加/删除选项或更换 Line ID 会形成新的选项组,玩家可能需要重新选择。
|
||
- 已保存的选项如果在新版台本中消失或变为不可用,系统会删除旧记录并重新显示选择页面。
|
||
|
||
### 5.3 条件选项
|
||
|
||
```yarn
|
||
-> 询问朝日的过去 <<if get_bool("c0_asahi_origin_known")>>
|
||
Asahi: 我现在可以谈起那段记忆了。
|
||
```
|
||
|
||
条件不满足时,该选项由 Yarn 标记为不可用。不要用 Yarn 原生 `$variable` 作为条件。
|
||
|
||
## 6. 条件语法
|
||
|
||
```yarn
|
||
<<if get_int("c0_signal_route") == 1>>
|
||
SLS: 你选择了观测路线。
|
||
<<elseif get_int("c0_signal_route") == 2>>
|
||
SLS: 你选择了保护路线。
|
||
<<else>>
|
||
SLS: 目前还没有选择路线。
|
||
<<endif>>
|
||
```
|
||
|
||
常用运算:
|
||
|
||
- 相等/不等:`==`、`!=`
|
||
- 数值比较:`>`、`>=`、`<`、`<=`
|
||
- 逻辑组合:`and`、`or`、`not`
|
||
- Bool 函数可直接作为条件:`<<if get_bool("c0_intro_read")>>`
|
||
|
||
复杂条件应优先拆成可读的事实变量。不要把大量业务规则全部塞在一行 Yarn 表达式中;对应 Block 的开放和禁用仍应由程序员在 `StoryData` 条件树中复核。
|
||
|
||
## 7. 永久章节变量:全部命令与函数
|
||
|
||
新台本禁止使用:
|
||
|
||
```yarn
|
||
<<declare $temporary = false>>
|
||
<<set $temporary = true>>
|
||
<<if $temporary>>
|
||
```
|
||
|
||
这些变量不受 ichni Official 的章节存档、对话事务和 Timeline 回滚管理。
|
||
|
||
### 7.1 Bool
|
||
|
||
```yarn
|
||
<<set_bool "c0_intro_read" true>>
|
||
<<if get_bool("c0_intro_read")>>
|
||
SLS: 你已经读过开场。
|
||
<<endif>>
|
||
```
|
||
|
||
| 接口 | 用途 |
|
||
|---|---|
|
||
| `<<set_bool "key" true>>` | 写入 Bool |
|
||
| `get_bool("key")` | 读取 Bool;用于 `if` 或表达式 |
|
||
|
||
### 7.2 Int
|
||
|
||
```yarn
|
||
<<set_int "c0_signal_route" 1>>
|
||
<<add_int "c0_resonance" 2>>
|
||
<<if get_int("c0_resonance") >= 3>>
|
||
SLS: 共鸣已经足够强烈。
|
||
<<endif>>
|
||
```
|
||
|
||
| 接口 | 用途 |
|
||
|---|---|
|
||
| `<<set_int "key" 1>>` | 写入整数语义的数值 |
|
||
| `<<add_int "key" 1>>` | 在当前值上增加整数 |
|
||
| `get_int("key")` | 读取整数 |
|
||
|
||
### 7.3 Float
|
||
|
||
```yarn
|
||
<<set_float "c0_signal_strength" 0.5>>
|
||
<<add_float "c0_signal_strength" 0.25>>
|
||
<<if get_float("c0_signal_strength") >= 0.75>>
|
||
SLS: 信号强度已达到阈值。
|
||
<<endif>>
|
||
```
|
||
|
||
| 接口 | 用途 |
|
||
|---|---|
|
||
| `<<set_float "key" 0.5>>` | 写入小数 |
|
||
| `<<add_float "key" 0.25>>` | 在当前值上增加小数 |
|
||
| `get_float("key")` | 读取小数 |
|
||
|
||
### 7.4 String
|
||
|
||
```yarn
|
||
<<set_string "c0_signal_source" "moon">>
|
||
<<if get_string("c0_signal_source") == "moon">>
|
||
Asahi: 信号来自月面。
|
||
<<endif>>
|
||
```
|
||
|
||
| 接口 | 用途 |
|
||
|---|---|
|
||
| `<<set_string "key" "value">>` | 写入字符串 |
|
||
| `get_string("key")` | 读取字符串 |
|
||
|
||
### 7.5 通用变量操作
|
||
|
||
| 接口 | 用途 |
|
||
|---|---|
|
||
| `has_variable("key")` | 存档中有该变量,或 `StoryData.Initial Variables` 声明了该 Key 时返回 `true` |
|
||
| `<<remove_variable "key">>` | 删除存档覆盖值;若 `Initial Variables` 有同名定义,之后读取会重新得到默认值 |
|
||
|
||
所有变量只属于当前章节。不要依赖 Chapter 0 的变量直接解锁 Chapter 1;跨章节内容开放应使用 `Unlock Key`。
|
||
|
||
## 8. 立绘命令
|
||
|
||
立绘坐标采用推荐范围 `-100` 到 `100`,`(0, 0)` 为舞台中心;角色自己的 `CharacterData.positionOffset` 会额外叠加。当前代码不会强制 Clamp,超出范围可能让立绘离开画面。
|
||
|
||
| 命令 | 作用 |
|
||
|---|---|
|
||
| `<<set_portrait "Asahi" 30 0 "normal">>` | 角色不在场时创建;已在场时设置位置和表情 |
|
||
| `<<set_portrait_current 30 0 "happy">>` | 操作当前说话者;必须已经显示过一行带角色名的台词 |
|
||
| `<<set_portrait_position "Asahi" -30 0>>` | 只改位置;角色不在场时以 `normal` 创建 |
|
||
| `<<set_portrait_emotion "Asahi" "angry">>` | 只改表情;角色必须已在场 |
|
||
| `<<hide_portrait "Asahi">>` | 隐藏并移除指定角色 |
|
||
| `<<hide_all_portraits>>` | 清空全部立绘 |
|
||
| `<<move_portrait "Asahi" 0 0 0.5>>` | 在指定秒数内平滑移动 |
|
||
| `<<jump_portrait "Asahi" 5 2 0.8>>` | 跳跃;参数为高度、次数、总时长 |
|
||
| `<<shake_portrait "Asahi" 0.5 2 10>>` | 横向震动;参数为时长、强度、频率 |
|
||
|
||
动画命令启动 DOTween 后会立即让 Yarn 继续执行,不会自动等待动画完成。若下一句必须等动画结束,可以使用 Yarn Spinner 内置命令:
|
||
|
||
```yarn
|
||
<<move_portrait "Asahi" 0 0 0.5>>
|
||
<<wait 0.5>>
|
||
Asahi: 我到了。
|
||
```
|
||
|
||
建议每个独立 TextBlock 开头明确建立所需立绘,结束前按设计调用 `hide_portrait` 或 `hide_all_portraits`,不要隐式依赖上一次对话留下的舞台状态。
|
||
|
||
## 9. 解锁、提示和调试命令
|
||
|
||
| 命令 | 作用 | 是否等待对话正常提交 |
|
||
|---|---|---|
|
||
| `<<grant_unlock "story_ch0_prologue_completed">>` | 授予通用 Offline 内容 Key,不自动弹提示 | 是 |
|
||
| `<<unlock_song "story_ch0_song_space_rain_available">>` | 兼容旧流程:授予 Key,并在首次授予时显示歌曲提示 | 是 |
|
||
| `<<show_message "title_key" "content_key" "Message">>` | 从指定 Unity String Table 解析并显示 MessageBox | 是 |
|
||
| `<<log "checkpoint reached" "info">>` | 输出 `info`、`warning` 或 `error` 日志 | 否;只影响 Console |
|
||
|
||
推荐把通用解锁和玩家提示明确拆开:
|
||
|
||
```yarn
|
||
<<grant_unlock "story_ch0_prologue_completed">>
|
||
<<show_message "story_c0_unlock_title" "story_c0_unlock_space_rain" "Message">>
|
||
```
|
||
|
||
`unlock_song` 主要用于兼容现有内容。它当前的专用提示仍可能显示内部 Key,正式文本优先使用 `grant_unlock + show_message`。
|
||
|
||
## 10. 完整分支案例
|
||
|
||
### 10.1 第一段主线
|
||
|
||
```yarn
|
||
title: C0_SignalIntro
|
||
---
|
||
<<set_portrait "Asahi" 30 -5 "normal">>
|
||
SLS: 记录开始。异常信号将在一分钟后抵达。
|
||
Asahi: 我们要观察它,还是先保护装置?
|
||
<<set_bool "c0_signal_intro_read" true>>
|
||
<<add_int "c0_signal_resonance" 1>>
|
||
===
|
||
```
|
||
|
||
### 10.2 带 Timeline Marker 的重要选择
|
||
|
||
```yarn
|
||
title: C0_SignalCheckpoint
|
||
---
|
||
<<if get_bool("c0_signal_intro_read")>>
|
||
SLS: 前置记录已经同步。现在决定路线。
|
||
<<else>>
|
||
SLS: 前置记录缺失,请程序员检查 StoryData 连接。
|
||
<<endif>>
|
||
|
||
-> 继续观测
|
||
<<set_int "c0_signal_route" 1>>
|
||
<<jump C0_SignalCheckpoint_Observe>>
|
||
-> 保护装置
|
||
<<set_int "c0_signal_route" 2>>
|
||
<<jump C0_SignalCheckpoint_Protect>>
|
||
===
|
||
|
||
title: C0_SignalCheckpoint_Observe
|
||
---
|
||
Asahi: 我会记录每一次波形变化。
|
||
===
|
||
|
||
title: C0_SignalCheckpoint_Protect
|
||
---
|
||
Asahi: 我先切断装置与外界的连接。
|
||
===
|
||
```
|
||
|
||
程序员会把这个 Important TextBlock 配置为 Timeline Marker 的 Anchor。第一次完成后再次进入,会自动采用第一次保存的选项;回滚到该 Marker 后,选项和 `c0_signal_route` 都恢复到该列之前。
|
||
|
||
### 10.3 两条互斥分支
|
||
|
||
观测分支:
|
||
|
||
```yarn
|
||
title: C0_SignalObserve
|
||
---
|
||
<<if get_int("c0_signal_route") == 1>>
|
||
SLS: 观测结果已经归档。
|
||
<<set_bool "c0_signal_observed" true>>
|
||
<<add_int "c0_signal_resonance" 2>>
|
||
<<endif>>
|
||
===
|
||
```
|
||
|
||
保护分支:
|
||
|
||
```yarn
|
||
title: C0_SignalProtect
|
||
---
|
||
<<if get_int("c0_signal_route") == 2>>
|
||
SLS: 保护程序已经完成。
|
||
<<set_bool "c0_signal_protected" true>>
|
||
<<add_int "c0_signal_resonance" 1>>
|
||
<<endif>>
|
||
===
|
||
```
|
||
|
||
程序员应同时配置:
|
||
|
||
- 观测 Block:`Unlock c0_signal_route == 1`,`Forbidden c0_signal_route == 2`。
|
||
- 保护 Block:`Unlock c0_signal_route == 2`,`Forbidden c0_signal_route == 1`。
|
||
|
||
### 10.4 汇合段与内容解锁
|
||
|
||
```yarn
|
||
title: C0_SignalConvergence
|
||
---
|
||
<<if get_bool("c0_signal_observed")>>
|
||
SLS: 你从时间的回声中取得了答案。
|
||
<<elseif get_bool("c0_signal_protected")>>
|
||
SLS: 你为下一次观测保留了机会。
|
||
<<else>>
|
||
SLS: 没有有效路线记录,请检查剧情配置。
|
||
<<endif>>
|
||
|
||
<<set_bool "c0_signal_convergence_read" true>>
|
||
<<grant_unlock "story_ch0_signal_convergence_completed">>
|
||
<<show_message "story_c0_signal_complete_title" "story_c0_signal_complete_body" "Message">>
|
||
<<hide_all_portraits>>
|
||
===
|
||
```
|
||
|
||
仓库中还有一组可直接参考的 `1-1-2-1` 示例:
|
||
|
||
- `Assets/Story/Chapter0/C0_RollbackDemo_Intro.yarn`
|
||
- `Assets/Story/Chapter0/C0_RollbackDemo_Checkpoint.yarn`
|
||
- `Assets/Story/Chapter0/C0_RollbackDemo_Observe.yarn`
|
||
- `Assets/Story/Chapter0/C0_RollbackDemo_Protect.yarn`
|
||
- `Assets/Story/Chapter0/C0_RollbackDemo_Convergence.yarn`
|
||
|
||
## 11. 本地化交付规则
|
||
|
||
Yarn 台词使用 Unity Localization。当前 `Chapter0.yarnproject` 的 `baseLanguage` 是 `en`,因此正式生产有两种选择:
|
||
|
||
1. Yarn 源文件直接写英文,翻译到 `zh-CN`、`ja`、`ko`、`vi-VN`。
|
||
2. 如果团队以中文为原稿,程序员必须在批量生产前把 Yarn Project 的 Base Language 一次性改为 `zh-CN`,再建立其它语言翻译。
|
||
|
||
不要在已经大量翻译后频繁切换 Base Language,也不要让“配置为 English 的项目”长期保存中文源文本。
|
||
|
||
交付给程序员时,每个 Yarn 文件还应附带:
|
||
|
||
- 所属 Chapter 和目标 Block ID。
|
||
- 入口 Node 名。
|
||
- Important / Secondary。
|
||
- 新增变量清单、类型、默认值和语义。
|
||
- 路线编号含义。
|
||
- 哪些 Block 应被开放或 Forbidden。
|
||
- 是否需要 Timeline Marker,以及显示的时间文本。
|
||
- 需要授予的 Unlock Key 和 Message 文案 Key。
|
||
- 新角色、表情和立绘资源需求。
|
||
|
||
翻译人员只修改 String Table 中的玩家可见文本,不修改 Line ID、Block ID、Node 名、变量 Key、Character ID 或 Unlock Key。
|
||
|
||
## 12. 剧本自检清单
|
||
|
||
提交前逐项确认:
|
||
|
||
1. 每个文件和 Node 都遵守命名规范,Node 在当前 Yarn Project 内唯一。
|
||
2. 每个入口 Node 都有 `---` 和 `===`,所有 `if` 都有 `endif`。
|
||
3. 选项缩进正确,单组选项不超过当前 UI 的 4 个按钮。
|
||
4. 没有 `<<declare>>`、`<<set $...>>` 或 `$variable`。
|
||
5. 所有剧情变量使用 `set_*` / `get_*`,并附带类型与默认值说明。
|
||
6. 互斥路线同时说明 Unlock 和 Forbidden 规则。
|
||
7. 重要选择说明是否需要 Timeline Marker。
|
||
8. 立绘 Character ID 与 `CharacterData.characterId` 完全一致。
|
||
9. 需要等待的立绘动画显式添加 `<<wait>>`。
|
||
10. 玩家可见标题、提示和时间文本提供 Unity Localization Key。
|
||
11. Line ID 由 Yarn 工具生成后不再手工改动。
|
||
12. 对话正常结束路径、Back 中断路径和已记忆选项回顾路径都能成立。
|
||
|
||
## 13. 常见错误
|
||
|
||
| 现象 | 常见原因 |
|
||
|---|---|
|
||
| TextBlock 点击后没有对话 | `Yarn Node` 为空、Node 名拼错、Yarn Project 编译失败 |
|
||
| 变量在重开后没有恢复 | 使用了 `$variable`、`declare` 或原生 `set` |
|
||
| 第二次进入仍显示选项 | Option Line ID 变化、选项组结构变化、上次对话中途退出 |
|
||
| 某条分支仍可点击 | 只写了 Yarn `if`,没有在 StoryData 配置 Forbidden Condition |
|
||
| 立绘命令找不到角色 | Character ID 大小写不一致,或 CharacterRegistry 未登记 |
|
||
| `set_portrait_current` 无效果 | 命令执行前还没有显示任何带角色名的台词 |
|
||
| Marker 回滚按钮不可用 | 该 Marker 的“列之前快照”尚未创建,或 Marker 配置错误 |
|
||
| 翻译后选项记忆错乱 | 不应发生;系统按 Line ID 记忆。若发生,优先检查 Line ID 是否被重建 |
|