Skip to content

fix(story-player): characteraction 阻塞键/exit 方向表/shake 阻塞/zoom pivot 等原生语义对齐 - #349

Merged
daflyinbed merged 3 commits into
mainfrom
fix/story-player-characteraction
Oct 21, 2026
Merged

daflyinbed merged 3 commits into
mainfrom
fix/story-player-characteraction

Conversation

@daflyinbed

Copy link
Copy Markdown
Contributor

本 PR 修复 StoryPlayer 对 AVG characteraction 命令移植中的全部 P1/P2 行为差异与可顺手对齐的 P3 默认值(共 10 项)。依据是对明日方舟官服客户端(2.7.61 / build 2761,Unity IL2CPP)GameAssembly.dll 的逆向分析,关键结论已在下文直接给出,不依赖外部文档。

背景:native 的 characteraction 机制(2.7.61 逆向结论)

命令由 Torappu.AVG.CharacterPanel 处理(与 character 命令共用面板),分发入口 _ExecuteCharacterAction(VA 0x183E699F0),按 type 明文比较分 move/jump/exit/zoom/shake 五支,未知 type 记 LogError 且不阻塞。关键语义:

  • 阻塞(_AddFinishCommand,VA 0x183E698E0):只读 isblock 参数;block= 键被整个 CharacterPanel 忽略(全库仅 cutin panel 读 block)。tween 非空且 isblock 时 OnComplete(FinishCommand) 阻塞,tween 为空(零时长)不阻塞。
  • 时长缩放:五个分支全部经 AVGUtils.CalculateFadetime(VA 0x183E69360,animateRatio × fadetime)。快进(animateRatio→0)时 NeedSkipAnimation(VA 0x183E69680)为真,jump/shake 整体退化重新走 _ExecuteCharacterMove(瞬时应用 xpos/ypos)。
  • exit(0x183E69C80):direction 缺省 "left";_GenExitPosition(VA 0x183E6B670)按槽位给定位移:left 槽 left/right → (-952,0)/(1352,0),right 槽 → (-1352,0)/(952,0),middle → (-1152,0)/(1152,0),up/down → (0,±1072),非法 direction → (0,0);经 AVGCharacterSlot.SetCharPos(0x183EB3290)绝对定位(水平退出同时把纵向漂移归零)。xpos/ypos 经 TryGetParam<int> 双双存在才作绝对坐标。fadetime 缺省 1.0。
  • zoom(0x183E6AC90 → AVGCharacterSlot.CharZoom,0x183EB2B50):xpos/ypos 是 [0,1] 区间的 pivot(缺省 0.5/0.5,作用于 _foreImage.rectTransform),越界 → 返回 null,整条命令 no-op(scale 也不生效);scale 缺省 1.0,fadetime 缺省 1.0;无 NeedSkipAnimation。
  • shake(0x183E6A870 → CharShake,0x183EB29F0):DOShakePosition(fadeTime, power, times, random, ...),随机度键名是 random(int,缺省 10),power 缺省 10、times 缺省 3、fadetime 缺省 0.5;与其它分支一样经 _AddFinishCommand 可阻塞。
  • jump(0x183E6A050 → CharJump,0x183EB2690):DOLocalJump(cur+(x,y), power, times, fadeTime) 是单 tween,总时长 = fadeTime(times 次跳动内含其中),阻塞等完整 tween。

修复内容

1. 阻塞键别名(P1)

  • native:只读 isblock;block= 恒忽略(2.7.61 语料 characteraction 行内 block= 4183 次 vs isblock= 187 次)。
  • 前端原状:toBoolean(block, toBoolean(isblock, false)),block= 优先生效,数千条命令产生原生没有的停顿。
  • 修复:只读 isblock;block= 不再参与。同步改写原来声称"native 接受两种拼写"的错误 provenance 注释。

2. exit 缺省方向相反(P1)

  • native:direction 缺省 "left" → 向左屏外。
  • 前端原状:缺省(及 up/down/非法值)一律向右出屏。
  • 修复:缺省改向左;新增 up/down((0,±1072) 原生值);非法值按原生 (0,0) 回到槽位静止锚点;出屏目标从"按宽度估算"改为 _GenExitPosition 原生位移表逐值照抄(l/m/r 槽位区分),水平退出同时把 actionY 归零(原生 SetCharPos 语义)。

3. shake 永不阻塞(P1)

  • native:shake 与其它分支一样经 _AddFinishCommand,isblock=true 时阻塞到抖动结束(时长 = 缩放后 fadeTime)。
  • 前端原状:case "shake" 直接 return,block 被丢弃。
  • 修复:block 时 await 一段等效时长(power=0 也阻塞,对齐"tween 恒存在"的原生行为;web 扩展 stop=true 除外)。

4. zoom 的 xpos/ypos 语义(P1)

  • native:[0,1] pivot,越界 → 整条命令 no-op。
  • 前端原状:当位移增量,越界值(语料中的 xpos=-1)仍生效 scale + 偏移。
  • 修复:runtime 侧校验 pivot ∈ [0,1](越界 warn + 整条跳过,含 scale);renderer 侧把 pivot 换算到 Pixi 锚点(Unity pivot y 自底向上,Pixi 自顶向下,做 y 翻转),缩放围绕 pivot 进行(rotationLayer 位置按当前缩放补偿,pivot 点钉在图像静止中心位置)。无 pivot 时保持原有居中锚定行为,不影响 charslot 的 scale 路径。

5. fadetime 不经 animateRatio(P2)

  • native:5 分支全过 CalculateFadetime;快进时 jump/shake 退化为 move,move/exit/zoom 走瞬时分支。
  • 前端原状:字面值;任何速度下时长不变、不退化。
  • 修复:改用与 character 命令相同的 calculateFadeMs;缩放后时长为 0 时 jump/shake 在 runtime 侧改派发为 move(对齐 NeedSkipAnimation → _ExecuteCharacterMove)。

6. exit/zoom 的 fadetime 默认(P2):按 type 取默认(exit/zoom → 1.0,move/jump/shake → 0.5),原状统一 0.5。

7. power 默认值(P2):jump/shake 默认从 0 改为原生 10(原状未写 power 的 jump 完全不弹跳;语料 761 条 jump 中 94 条未写 power)。

8. jump 总时长与阻塞边界(P2):每跳时长从 durationMs 改为 durationMs/times(单 tween 内含 times 次跳动,总时长 = durationMs,对齐 DOLocalJump);阻塞从"只等 move 段"改为 Promise.all([move, jump]) 覆盖全程。

9. shake 随机度参数键(P2):从 randomness(默认 90,clamp 0-100)改为原生 random 键(int,默认 10);pickShakeOffset 的"度数→逐 tick 触发概率"映射在注释中声明为 web 适配。

10. exit 显式坐标(P2)+ times 默认(P3):exit 的 xpos/ypos 双双存在时作绝对故事坐标(与 character 命令的 absolutePosition 约定一致),仅单个存在时忽略(原状把单独 ypos 当相对增量、丢弃 xpos);times 默认从 1 改为原生 3(语料 70 条 jump 未写 times;time= 拼写错误键也落到默认 3)。另:zoom 缺省 scale 从"保持当前"改为原生缺省 1.0,stop/start/leftend/rightend/xscale/yscale 在注释中标注为 charslot 风格 web 扩展(native 面板不读)。

不改动(review 确认一致或建议维持):5 分支 type 分发与未知 type 非阻塞、逐分支 slot 真值表(含 exit 兜底 middle)、move/jump 相对位移、fadetime=0 时不阻塞、exit 不清槽、zoom scale 目标值语义、name/type 的大小写归一(P3,语料全小写)、ease 线性(P3 视觉细节,涉及共享 TweenRunner,不在本命令范围内动)。

验证用剧情文件

生产剧情语料(2.7.61)本地路径 torappu/storage/asset/gamedata/latest/story:

  • 修复 1(block= 不再阻塞):obt/main/level_main_09-03_beg.txt 144/158/183 行(type="move" + block=true 无 isblock)——播放时这些位移不再把播放器卡住(与官服一致)。
  • 修复 2(exit 缺省向左):activities/act15d0/level_act15d0_07_beg.txt 90 行 [characteraction(name="left", type="exit",fadetime=0.5, block=true)]——无 direction,角色应向左出屏(旧版向右),且 block= 不生效。
  • 修复 4(zoom pivot/no-op):activities/act17side/level_act17side_01_beg.txt 414/417/429 行与 activities/act17side/level_act17side_st03.txt 361/363/368/371 行(xpos=-1)——整条 zoom 不再执行(旧版会把 scale 一并应用);activities/act12side/level_act12side_08_end.txt 264 行(xpos=0.5,ypos=0.3,scale=1.5)、activities/act22side/level_act22side_08_end.txt 192 行(xpos=0.8,ypos=0.8)、activities/act20side/level_act20side_03_end.txt 180 行——缩放围绕指定 pivot 进行。
  • 修复 7(power 默认 10):obt/main/level_main_09-16_end.txt 168/174/175 行(未写 power 的 jump,旧版不弹跳)。
  • 修复 10(times 默认 3):obt/memory/story_asbest_1_1.txt 381 行(未写 times,原生弹 3 次);obt/memory/story_billro_1_1.txt 36 行(time=1 拼写错误键,同样落默认 3)。
  • 修复 5(jump 零时长退化):obt/memory/story_bstalk_1_1.txt 263 行与 activities/act20side/level_act20side_02_end.txt 154 行(fadetime=0,后者带 isblock=true 但零时长不阻塞,瞬时位移)。
  • 修复 3(shake 阻塞)、9(random 键)、exit up/down、exit 显式坐标:语料 0 命中(shake 无 isblock=true、无 random=、exit 无 up/down、无 xpos/ypos),前瞻对齐,由单测锁定:tests/runtime.spec.ts(「awaits the full shake duration…」位于 tests/renderer.spec.ts、「reads the shake randomness from the native random key」、「defaults the exit direction to left and accepts native up/down」、「uses exit xpos/ypos as absolute coordinates when both are present」)与 tests/renderer.spec.ts(「exits along the native direction table including up and down」)。

测试

  • pnpm test:20 个文件 234 个用例全部通过(基线 222,新增/改写 characteraction 相关 12 个)。
  • pnpm exec vue-tsc -b 通过;pnpm exec eslint(改动文件)通过。
  • STORY_CORPUS_ROOT=… pnpm test:story-log 全语料 Log All 回归通过(2 用例,约 71s)。

…vot 语义

依据 2.7.61(build 2761)GameAssembly.dll IDA 反编译复核:

- 阻塞只读 isblock(native _AddFinishCommand 不读 block=),消除语料
  4183 条 block= 拼写凭空产生的停顿
- exit 缺省 direction=left,支持 up/down,位移改用 _GenExitPosition
  原生表(l/m/r 槽位 -952/1352 等),xpos/ypos 双双存在时作绝对坐标
- shake 按原生 _AddFinishCommand 语义阻塞完整时长
- zoom 的 xpos/ypos 改为 [0,1] pivot(越界整条 no-op),缩放围绕 pivot
- fadetime 全分支过 CalculateFadetime(animateRatio),exit/zoom 默认 1.0s;
  jump/shake 在快进/零时长下退化为瞬时 move(NeedSkipAnimation)
- power 默认 10、times 默认 3、randomness 读原生 random 键默认 10
- jump 改单 tween(总时长=fadeTime,times 次跳动内含),阻塞覆盖全程
@daflyinbed
daflyinbed force-pushed the fix/story-player-characteraction branch from fe0ab22 to 5e95bee Compare October 19, 2026 15:12
按 2.7.81 反编译复核(DOTween::Shake 0x18424e2d0 逐行还原)对齐:

- shake 重写为路点模型:路点数 trunc(vibrato×时长) 钳 2、方向逐次
  180°±random 度、幅度线性衰减、段长按 (i+1) 加权归一、末路点归零,
  OutCubic 全局扭曲;isblock 时 await 该 tween。charslot 的 action=shake
  共用同一路径(原生 _GenCharslotShake 即转调 CharShake),删除旧
  pickShakeOffset 概率-抽档模型
- type/name/direction 改 String.op_Equality 精确比较(去 trim/小写归一);
  name 缺失先取 GetOrDefault 默认 "left" 再进真值表(exit 缺 name 走左槽)
- times/random/exit 显式 xpos/ypos 改走 Convert.ToInt32 银行家舍入
  (toNativeInt);scale/power 去掉 Math.max(0,·) 钳制
- zoom fadetime=0 仍阻塞(DOTween.To 零时长返回非 null tween)
- move/exit 补 Ease.OutCubic、jump x 轴保持 Linear(sequence 自身 ease 无效)
- 测试:isblock shake 用例改两次 tween 断言,新增 move/exit/jump ease 用例

vue-tsc -b、eslint、vitest 760/760 通过
@daflyinbed
daflyinbed marked this pull request as ready for review October 20, 2026 13:26
- shake:DOShakePosition 标记 SpecialStartupMode.SetShake,启动时
  SpecialPluginsUtils.SetShake(0x18427f590)把 easeType 强制为 Linear,
  CharShake 的 SetEase(OutCubic) 被覆盖;Vector3ArrayPlugin 逐段求值
  缓动,故每段线性插值。去掉全局 OutCubic 扭曲,并复用 core/ShakePath
  (DOShakePosition 传 ignoreZAxis=false,Full 模式 tilt 与镜头抖动一致)
- jump:Sequence.DoApplyTween(0x1842525c0)会先用 sequence 自身 ease
  扭曲时间轴,CharJump 的 SetEase(OutCubic) 生效;DOLocalJump 的 y 位移
  走 EasedValue(OutQuad)(<DOLocalJump>b__7)。x=OutCubic、
  y=OutQuad∘OutCubic、弹跳弧采样 OutCubic 时间
- exit 显式 xpos/ypos:SetCharPos 直接写 _offset.localPosition,与
  _GenExitPosition 同一槽位位移空间,不再按 baseX/baseY 换算故事坐标
- charslot times 默认对齐原生 GetOrDefault<int>("times", 0)
- 复用已有 toNativeIntParam,移除重复的 toNativeInt 与失效的 shakeTimeout
@StarHeartHunt

Copy link
Copy Markdown
Member

Review(对照 2.7.81 GameAssembly.dll 复核)

我对照 2.7.81 的反编译逐条复核了 PR 描述和两个 commit 里的原生结论。大部分对齐都成立,但有 3 处与原生不符,已在 b343ba4 里直接修好。

已修复

1. shake 不应有 OutCubic 全局扭曲

  • CharShake(0x184029850)确实链了 SetEase(OutCubic)。但 DOShakePosition(0x18425cda0)给 tween 标了 SpecialStartupMode.SetShake,启动时会跑 SpecialPluginsUtils.SetShake(0x18427f590):先 SetPunch(里面设 easeType = OutQuad),再把 easeType 改成 1(Linear)。这一步发生在首帧 startup,晚于 SetEase,所以 OutCubic 被覆盖了。
  • 另外 Vector3ArrayPlugin.EvaluateAndApply(0x184282330)是按每段的 elapsed/duration 求 ease,并不扭曲整条路径的全局进度。所以原生实际效果是:各段线性插值。
  • 修法:去掉 { ease: easeOutCubic }。顺带把手写的路点 walk 换成已有的 core/ShakePath(buildShakePath/sampleShakePath),它本来就是同一个 DOTween.Shake(0x18424e2d0)的移植。DOShakePosition 传的是 ignoreZAxis = false,所以 Full 模式下绕 Vector3.up 的 tilt 也会作用(x 分量乘 cos(tilt)),与镜头抖动的实现一致。原来手写版没有这一项。

2. jump:sequence 自身的 ease 是生效的

  • 注释里说"DOTween ignores a sequence's own ease"不成立。Sequence.DoApplyTween(0x1842525c0)里,只要 easeType != Linear,就先用 EaseManager.Evaluate 扭曲 sequence 的 position,再交给 ApplyInternalCycle。所以 CharJump(0x1840294f0)的 SetEase(OutCubic) 会作用到整条时间轴。
  • 还有一点:DOLocalJump(0x184257a20)的 y 方向位移不在 x/z 的 Linear tween 里,而是在 yTween 的 OnUpdate(<DOLocalJump>b__7,0x184265320)中用 DOVirtual.EasedValue(0, offsetY, yTween.ElapsedPercentage, Ease.OutQuad) 叠加上去的。
  • 修法:x 用 OutCubic,y 位移用 OutQuad∘OutCubic,弹跳弧在 OutCubic(p) 的时间上采样 yoyo。_GenCharslotJump 走的也是同一个 CharJump,所以 charslot 的 jump 弧会一起变。

3. exit 的显式 xpos/ypos 不是故事坐标

  • _ExecuteCharacterExit(0x183fd6f20)在两个 TryGetParam<int> 都命中时,直接调 SetCharPos(x, y, t)(0x18402a1e0),写的是 _offset.localPosition。这和 _GenExitPosition 的表值是同一个"相对槽位锚点的位移"空间。
  • PR 里按 x - baseX / baseY - y 当故事坐标换算(baseX 还含了立绘尺寸和 link 偏移),换出来的位置是错的。描述里提到的 "character 命令的 absolutePosition 约定" 在 main 上也不存在。
  • 修法:actionX/actionY 直接取 (xpos, ypos)。

顺手清理

  • runtime.ts 里已经有 toNativeIntParam/roundHalfToEven(同样是 Convert.ToInt32 的银行家舍入),所以删掉了新加的重复 toNativeInt。
  • shakeTimeout 在改成 tween 之后再也没被赋值,已删除这个死字段。
  • charslot 的 shake 现在共用路点模型,times 变成了 vibrato,所以默认值改为与原生 GetOrDefault<int>("times", 0) 一致(原来是 trunc(…, 1))。jump 一侧 DOLocalJump 会把 < 1 钳到 1,行为不变。
  • 测试:exit 坐标、jump 缓动和弧采样的用例改成按原生语义断言;新增 1 个 shake 线性逐段采样的用例。

验证:vitest 761/761、vue-tsc -b、eslint 均通过。

复核一致(未改动)

  • 五个分支按 String.op_Equality(ordinal)分发;未知 type 走 LogError 并返回 false,不阻塞。
  • 只读 isblock;_AddFinishCommand 在 tween 为 null 时不阻塞。
  • 各分支默认值:move/jump/shake 的 fadetime = 0.5,exit/zoom 的 fadetime = 1.0,power = 10,times = 3,random = 10,zoom 的 xpos/ypos = 0.5,scale = 1。
  • jump/shake 在 NeedSkipAnimation 时回落到 _ExecuteCharacterMove。
  • _GenExitPosition 表值(±952 / ±1352 / ±1152 / ±1072)以及 exit 槽位真值表(else 落到 middle)。
  • CharZoom 的 pivot 越界返回 null;它没有瞬时分支,零时长时 DOTween.To 仍返回 tween。
  • MoveChar/SetCharPos 都是 OutCubic,IsZero 时返回 null。

非阻塞备注(未在本 PR 处理)

  • charslot action=jump 的位移仍走 characterSlotMove 的线性 tween。原生经 CharJump,位移也是 OutCubic / OutQuad∘OutCubic。这一点不属于 characteraction 的范围。
  • charslot 的 power 有 Math.max(0, …),random 被 clamp(0, 100),原生都不钳。语料中没有触发这两种情况。
  • 分支落后 main 4 个提交,git merge-tree 显示无冲突。

@daflyinbed
daflyinbed merged commit 9f076c5 into main Oct 21, 2026
1 check passed
@daflyinbed
daflyinbed deleted the fix/story-player-characteraction branch October 21, 2026 03:33
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