Skip to content

状态: 🟢 已修复

日期: 2026-07-15 严重程度: 🔴 P1 影响范围: frontend/src/vmd-loader.ts, frontend/src/proc-motion-bridge.ts, frontend/src/thumbnail-capture.ts发现方式: 用户反馈 修复方案: stopProcMotion() 从 vmdData 赋值前移到赋值后,确保缩略图截帧时 userVmdPresent=true

程序化动作切换与缩略图截帧时序冲突

日期: 2026-07-15 严重程度: P1(缩略图截到空姿态,用户可见) 影响范围: vmd-loader.ts(VMD 加载)+ proc-motion-bridge.ts(程序化动作停止)+ thumbnail-capture.ts(缩略图渲染) 发现方式: 用户观察 + 邻组 6 轮修复未果


问题描述

加载真实 VMD 动作时,缩略图截到空姿态(角色静止 T-Pose 或空白帧),而非动作初始帧。

触发条件:程序化动作(idle/autodance)激活状态下加载真实 VMD。

根因分析

三层时序叠加导致的隐蔽 bug:

  1. stopProcMotion() 内部有 userVmdPresent 守卫(inst.vmdData !== null),用于防止已有 VMD 时误清动画
  2. stopProcMotion()inst.vmdData 赋值之前调用(vmd-loader.ts:82)
  3. 缩略图截帧发生在 stopProcMotion() 之后、vmdData 赋值之前

时序图:

stopProcMotion()  →  userVmdPresent=false  →  清空动画
缩略图截帧        →  动画已空              →  截到空姿态
inst.vmdData = data  →  此时赋值已晚

为什么隐蔽

  1. 跨文件状态依赖stopProcMotion()proc-motion-bridge.tsvmdData 赋值在 vmd-loader.ts,状态读取在 config.ts——三处各管一段
  2. 触发条件窄:只在"程序化动作激活 + 加载真实 VMD"时复现,普通测试不会同时开 autoSwitch 又截缩略图
  3. userVmdPresent 守卫是后加的修复:原本 stopProcMotion() 无条件清动画,守卫是为了防止"已有 VMD 时被程序化动作覆盖",但引入了新的时序依赖
  4. 代码读起来合理:字面上"先停程序化、再绑 VMD、再截图"是正确顺序,但 userVmdPresent 的检查把副作用延迟到了绑 VMD 之后

修复方案

stopProcMotion()inst.vmdData 赋值前移到赋值后:

typescript
// vmd-loader.ts
inst.vmdData = data;
// 停止程序化动作(延迟到 vmdData 赋值后,确保 userVmdPresent=true)
if (isProcVmdActive() && name !== PROC_VMD_NAME_IDLE && name !== PROC_VMD_NAME_AUTODANCE) {
    stopProcMotion();
}

这样缩略图截帧时 userVmdPresent=true,动画不会被清空。

教训

  1. 异步时序假象:代码读起来像"先 A 再 B 再 C",但 A 的副作用依赖 C 之后才赋值的状态
  2. 守卫条件的隐式依赖userVmdPresent 守卫假设 vmdData 已赋值,但调用点在赋值之前
  3. 跨文件状态读取的盲区:改一处看似合理,但另一处的隐式依赖就断了