Skip to content

fix(story-player): cgitem 纹理加载不阻塞剧情推进与同 key 瞬间替换 - #367

Merged
StarHeartHunt merged 2 commits into
mainfrom
fix/story-player-cgitem
Oct 19, 2026
Merged

StarHeartHunt merged 2 commits into
mainfrom
fix/story-player-cgitem

Conversation

@daflyinbed

@daflyinbed daflyinbed commented Aug 24, 2026 •

Copy link
Copy Markdown
Contributor

本 PR 处理 StoryPlayer 中 AVG cgitem 移植 review 差异清单里纹理加载 / 同 key 替换相关的 3 项 P2,以及 1 项 P3 注释修正。依据是对明日方舟官服客户端 GameAssembly.dll(Unity IL2CPP)的逆向:最初分析的是 2.7.61(build 2761),review 时在 2.7.81 上用 IDA 反编译和 Il2CppDumper dump.cs 复核过。下文直接给出关键结论,不依赖外部文档。

勘误(2026-10-19 review):本 PR 最初把 cgitem 的 ease 默认值改成了 InSine,理由是「DOTween Ease 枚举 Linear = 0, InSine = 1」。这个理由是错的:2.7.81 dump.cs 里 DG.Tweening.Ease 的定义是 Unset = 0, Linear = 1, InSine = 2, …,所以 GetEnum<Ease>(param, "ease", (Ease)1, …) 的默认值是 Ease.Linear。这和 main 上共享 DotweenEase 库(#407)的序号表一致,前端原来的实现本就正确。9b282e9 已撤回 InSine 默认值和 cgEaseCurve 重映射,ease 行为与 main 相同,本 PR 只保留注释更正和测试锁定。

背景:native 的 cgitem 机制

  • Torappu.AVG.AVGShowItemCgSlot.Show(2.7.61 0x183ed1810 / 2.7.81 0x18405dde0)用 GetEnum<Ease>(param, "ease", (Ease)1, ignoreCase: false) 读缓动。(Ease)1 是 Linear;GetEnum 解析失败时同样回落 Linear。语料 188 条 cgitem 里只有 3 条显式写了 ease,都是 OutQuad。

  • Torappu.AVG.AVGCgItemPanel._ShowItem(2.7.61 0x183e35d80 / 2.7.81 0x183fb12b0)的执行顺序:

    1. Instantiate 新 slot;
    2. 立即 Dispose() 旧的同 key 项(瞬间消失,不淡出);
    3. 写入 m_slotsInUseDict;
    4. 绑定 PostDisplay;
    5. 最后才加载 sprite。

    _LoadSprite 是同步取缓存(由 InternalResRefCollector.GatherResRefs 预加载保证),executor 不挂起,也没有回调。也就是说:注册先于加载,替换没有空窗,加载不在命令关键路径上。

  • blocker style:同样走上面的公共前段(实例化 blocker 样式的 slot prefab、写 dict、绑 PostDisplay),然后跳过 sprite 加载,调用 Show(command, null, null),并立即 FinishCommand()。语料中 0 次使用。

  • ExecutorComponent.Execute(2.7.61 0x183e45b10):executor 返回 false(即 block=false)时立即 FinishCommand(),命令瞬时完成。

修复内容

1. 纹理加载移出命令关键路径(P2,差异 #2)

  • native:executor 同步完成;block=false 时命令瞬时返回,加载不在关键路径上(资源已预加载)。
  • 前端原行为:show() 无条件先 await loadTexture(网络 + 解码),CG 未缓存时剧情推进会被卡住。
  • 改法:CgItemPanel.show() 改成「命令侧先行」:同步建好占位 root 后立即返回(block=false),纹理到位后再建 sprite、启动各 tween 轨道。block=true 仍等待纹理和全部轨道,相当于 native 等整个 Sequence,再加上 web 特有的取纹理等待。纹理到位后才开始运动,比 native 多一段加载耗时的起步延迟,属于 web 适配。

2. 同 key 替换的空窗(P2,差异 #3)

  • native:Instantiate 新 slot 后立即 Dispose 旧 slot(瞬间消失)。旧图先走,新图加载后出现。
  • 前端原行为:先 await 新纹理,加载完成后才 dispose(key),旧图一直保留到新图就绪。
  • 改法:把 dispose(input.key) 提前到加载之前,对齐「瞬间替换」语义。占位 slot 的语义是 review 建议的可接受方案。

3. 加载期竞态残留(P2,差异 #4)

  • native:dict 在加载前写入,加载期间到来的 hidecgitem 能命中 key。
  • 前端原行为:states 在加载完成后才写入。加载期间的 hidecgitem(key) 查不到 key,变成 no-op;CG 随后出现,一直残留到 clear-all。
  • 改法:
    • 命令一进来就写 states(pending 注册)。
    • 纹理到位时校验 map 中的 state 和 sessionId;如果期间被替换、隐藏或清空,就丢弃这一帧。
    • 加载失败(纹理为 null)时撤销注册并告警。
    • root 在命令执行时入层,多张 CG 的叠放顺序仍按命令顺序。

4. ease 默认值注释更正与测试锁定(差异 #1 / #5,不改行为)

  • 差异 Create ISEvent.ts #1 原来判断「默认应为 InSine」,依据的是错误的枚举序号,已撤回。默认值维持 "Linear";runtime 注释更正为 Unset = 0, Linear = 1, InSine = 2 并注明出处。

  • 差异 XbMapViewer: Init #5(ease 名集合)由共享 DotweenEase 库覆盖:

    • 全部 DOTween 曲线名大小写敏感命中;
    • 整数字符串按序号解析;
    • 未知名字回落 Linear,与 native GetEnum 一致。

    panel 直接使用 dotweenEaseCurve。

5. blocker 注释更正(P3,差异 #6)

原注释说 blocker "deliberately skips m_slotsInUseDict",与反编译结果不符。整条命令跳过的行为保持不变(有意省略),只更正注释。

未动条目:以下按 review 建议维持现状:

Stack 关系

fix/story-player-hidecgitem(#365,hidecgitem 命令的同源修复)叠在本分支之上。先合本 PR,再合 #365。

验证用剧情文件

语料:torappu gamedata story/ 目录(2026-10-14 版本复核行号)。

  • obt/main/level_main_15-15_end.txt:393,396,398:不写 ease 的 Ken Burns 镜头。
    • 回归:默认 Linear 运镜不变;
    • block 缺省为 false,验证 Create ISEvent.ts #1:推进不被加载卡住。
  • obt/main/level_main_15-15_end.txt:398→400:同 key cgitem_60_i12_3 重复下发,验证 ISEvent: Init #2 瞬间替换。
  • obt/main/level_main_15-15_end.txt:400→407:400 show 之后 407 紧跟 hidecgitem(image="cgitem_60_i12_3"),验证 Refactor: isEvent #3:加载期 hide 不再漏掉 CG。
  • obt/main/level_main_15-16_beg.txt:443,454,458:同 key cgitem_60_i16 三连发,验证 ISEvent: Init #2;每条 block 缺省 false,验证 Create ISEvent.ts #1。
  • activities/act51side/level_act51side_01_end.txt:662-664:显式 ease="OutQuad",回归:显式曲线保持不变。

测试

  • pnpm test:725 个用例全部通过(main 基线 719 + 新增 6)。
    • tests/cgItemPanel.spec.ts 新增 5 条:
      • 曲线解析:Linear 默认 / 显式 InSine / 未知名字回落 Linear;
      • 非阻塞返回;
      • 瞬间替换;
      • 加载期 hide;
      • 纹理缺失时撤销注册。
    • tests/runtime/panels.spec.ts 新增 1 条:默认 ease = Linear。
  • pnpm exec vue-tsc -b:无错误。
  • pnpm exec eslint(改动文件):无告警。

- ease 默认 Linear 改为 DOTween Ease 枚举 1 = InSine(AVGShowItemCgSlot.Show 0x183ed1810 的 GetEnum 默认值),接入共享 DotweenEase 库并把未解析名字重映射到 InSine
- show() 命令侧先行:同步 dispose 旧同 key 项、建占位 root、写 states 后立即返回(block=false),纹理到位后再建 sprite 并启动各 tween 轨道;block=true 等纹理 + 全部轨道
- 同 key 替换对齐 native 瞬间替换语义;加载期 hidecgitem 可命中 pending 注册;纹理缺失撤销注册并告警
- blocker 注释勘误(2.7.61 blocker 也走公共前段,仅跳过 sprite 加载)
DOTween `Ease` 枚举是 `Unset = 0, Linear = 1, InSine = 2`(2.7.81 dump.cs
DG.Tweening.Ease),`AVGShowItemCgSlot.Show`(2.7.81 0x18405dde0)的
`GetEnum<Ease>(param, "ease", (Ease)1, ...)` 默认值即 Ease.Linear,与共享
DotweenEase 库的 Linear 回落一致。撤回 InSine 默认值与 cgEaseCurve 重映射,
测试同步改为 Linear 默认/回落。blocker 注释按 2.7.81 `_ShowItem`
(0x183fb12b0)更正:实例化 blocker 样式 prefab、`Show(command, null, null)`
后立即 FinishCommand。
@StarHeartHunt

Copy link
Copy Markdown
Member

Review(native 复核:2.7.81 GameAssembly.dll + dump.cs)

P1 回归:(Ease)1 是 Linear,不是 InSine(已修复 9b282e9)

  • dump.cs 里的 DG.Tweening.Ease(TypeDefIndex 48379)是 Unset = 0, Linear = 1, InSine = 2, OutSine = 3, …。PR 依据的「Linear = 0, InSine = 1」漏算了 Unset。
  • 2.7.81 的 AVGShowItemCgSlot.Show(0x18405dde0)反编译出来仍是 GetEnum<Ease>(param, "ease", (Ease)1, ignoreCase: false),也就是默认 Ease.Linear。main 上共享的 DotweenEase.ts 序号表(refactor(story-player): 抽出共享 DOTween ease 曲线库与 TweenRunner ease/loop 选项 #407)本来就是这样写的,原注释「Ease.Linear (= 1)」是对的。
  • 照 PR 原来的写法,语料里 185/188 条不写 ease 的 cgitem 运镜都会从线性变成先慢后快。cgEaseCurve 把无法解析的名字重映射到 InSine 也不对,GetEnum 回落的同样是 Linear。另外它会误伤序号写法:ease="1"(Linear 的序号)也会被改成 InSine。
  • 修复内容:
    • runtime 默认值改回 "Linear",并更正注释。
    • 删除 cgEaseCurve,直接用 dotweenEaseCurve。
    • 两条测试改为断言:默认为 Linear;未知名字回落 Linear;显式写 InSine 时生效。

blocker 注释小修正(同一 commit)

2.7.81 _ShowItem(0x183fb12b0)的 blocker 分支流程:

  1. 先实例化 _slotStyles 里 blocker 样式的 slot prefab(不是场景常驻对象)。
  2. 调用 Show(command, null, null)。
  3. 立即 FinishCommand(),不看 block。

行为不变,只改了注释。

已复核,没有问题

  • _ShowItem 的顺序与 PR 描述一致:
    1. Instantiate
    2. GetValueOrDefault 命中就 SlotInUseItem.Dispose()(瞬间消失)
    3. 写 dict
    4. 绑 PostDisplay
    5. 同步 _LoadSprite
    6. slot.Show(cmd, sprite, FinishCommand)
  • pending 注册方案:
    • 各种竞态都有 states.get(key) !== state / sessionId 守卫:同 key 替换、加载期间的 hide / clear-all、destroy() 之后纹理才回来。
    • root 在命令执行时就入层,多张 CG 的叠放顺序仍按命令顺序。
    • textureForUrl 内部 catch 后返回 null,所以 void completion 不会产生 unhandled rejection。
    • seek 走 quick_play 实时推进,延后创建的 tween 不会被跳过。
  • 纹理到位后才启动 tween。native 是同步加载、命令一下就开始运动,所以前端会多出一段加载耗时的起步延迟。这属于可以接受的 web 适配。

验证

pnpm test 725 条全部通过,vue-tsc -b 和 eslint(改动文件)通过。

⚠️ PR 标题和描述里的「默认 InSine」需要同步改掉,squash 合并时会用标题。可以改成「cgitem 纹理加载不阻塞剧情推进与同 key 瞬间替换」之类。

@StarHeartHunt StarHeartHunt changed the title fix(story-player): cgitem 默认 InSine 缓动与纹理加载不阻塞剧情推进 fix(story-player): cgitem 纹理加载不阻塞剧情推进与同 key 瞬间替换 Oct 19, 2026
@StarHeartHunt
StarHeartHunt merged commit 7ae1a2f into main Oct 19, 2026
1 check passed
@StarHeartHunt
StarHeartHunt deleted the fix/story-player-cgitem branch October 19, 2026 15:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants