Files
mtgodot-poc/audit/contracts/movement.motion-resource-events.md
T
shenandshen c93894313a fix: 装备属性面板避让逻辑 + 多项功能更新
- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧
- inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域
- 包含其他累积的功能开发和测试文件
2026-09-21 16:38:59 -07:00

157 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# movement.motion_resource_events
## Scope
审计 40250 Windows 客户端的 `.msa`/`.msm` 动作资源加载、动作模式与 motion key、随机权重、`MotionDuration`/`Accumulation`、LoopData、连击输入窗口、攻击命中窗、MotionEventData、动作队列、速度倍率、音效/特效事件,以及当前端 `Metin2AnimPlayer`、PC/NPC/远端 PC 视图和 NetPlay 的对应实现。判断标准是两端是否按同一资源字段、分支、帧时序和动作队列推进;“能播放一个动作”不等于实现统一。
## Reference call chain
- `GameLib/RaceData.h` / `GameLib/RaceData.cpp` / `GameLib/RaceManager.cpp`
- `CRaceData` 按 motion mode 建立 `TMotionVectorMap`,每个 motion index 可有多个带百分比的 `TMotion`;`GetMotionKey`、motion mode parent、normal attack index 和 combo table 决定最终 motion key 与随机资源。
- `NEW_RegisterMotion` 将 `.msa` 对应的 `CRaceMotionData` 与 GR2 资源绑定;缺失 motion key 时 `SetLoopMotion`/`InterceptMotion` 返回失败,不凭空替换成同类动作。
- `Client/Eternexus/root/playersettingmodule.py`
- PC 的 `RegisterCacheMotionData` 注册顺序、默认 100 权重、`SetMotionRandomWeight` 覆盖和各职业/武器/骑马模式目录是 RaceData 的实际数据来源;同一资源不能用当前端自行推导的等权列表替代。
- `GameLib/RaceMotionData.cpp` / `GameLib/RaceMotionData.h`
- `.msa` 读取 `MotionFileName`、`MotionDuration`、三轴 `Accumulation`、`ComboInputData`、`AttackingData`、`LoopData` 和 `MotionEventData`;事件的 `StartingTime` 按 `g_fGameFPS` 转为帧。
- `CRaceMotionData` 还加载 motion sound script,保留攻击硬直、无敌、外力、命中次数、多个攻击采样窗和技能取消条件。
- `GameLib/ActorInstanceMotion.cpp` / `GameLib/ActorInstanceMotionEvent.cpp`
- `SetLoopMotion`、`InterceptMotion`、`PushOnceMotion`、`PushLoopMotion` 将动作放入当前/预约队列,按 `GetMotionDuration()/fSpeedRatio` 计算结束时间;`CurrentMotionProcess` 按 LoopData 的 start/end/count 回绕或回到 WAIT。
- `__MotionEventProcess` 每个 actor 按当前帧调用 `MotionEventProcess` / `SoundEventProcess`;事件分支包括独立特效、挂骨特效、目标特效、屏幕震动、特殊攻击碰撞窗、3D 声音、飞行物、显示/隐藏和 warp。
- `UserInterface/InstanceBaseMotion.cpp` / `InstanceBaseBattle.cpp`
- `CInstanceBase` 的 motion mode、钓鱼/受击/情绪动作委托给 actor motion queue;普攻/连击/技能的 `ComboInputData` 和 `GetComboDataPointer` 决定输入窗口,动作资源本身决定速度与命中时序。
## Current call chain
- `formats/msa.cpp` 解析 `MotionFileName`、`MotionDuration`、`Accumulation`、LoopData、ComboInputData、AttackingData 和一组通用 MotionEvent 字段;`formats/msm.cpp` 解析 `BaseModelFileName`、`MotionListFileName` 与 `HairData`。
- `extension/src/metin2_anim.cpp` 的 `Metin2AnimPlayer` 缓存/加载 MSA 与 GR2,进行 CPU/GPU skin、0.15 秒 crossfade、事件时间穿越派发、命中窗/连击窗口 getter、root accumulation getter 和 sound/event signal。
- `project/ui/player_view.gd` 结合 `project/ui/motion_registry.gd` 读取发布的 `playersettingmodule.py`,按注册的 mode/index/权重选择 PC motion;`project/ui/mob_view.gd` 按选中 `.msm` 的 `MotionListFileName` 解析 NPC/怪物 `motlist`,把别名归并到 40250 `EName` 动作索引并按权重选择;`remote_player_view.gd` 复用 PlayerView 并刷新远端装备动作模式。
- `project/net_play.gd` 消费 `get_motion_data()`,推进普攻/连击输入窗和 `.msa` 命中窗;`project/game_scene.gd` 将本地主角的 `motion_event_detailed` 接到特效、声音、屏幕震动、弓箭发射和 WARP,`NetWorld` 将真实远端 Actor 的事件接到同一消费边界。
- `project/ui/player_view.gd` / `mob_view.gd` 对受击动作使用本地 `HitReaction` 队列;循环/一次性动作由原生 LoopData 和 playback_finished 驱动,远端 NPC/PC 的真实 animator 事件由 `NetWorld` 连接到 GameScene 的统一分派。
## Branch matrix
| 场景 | 40250 | 当前端 | 结论 |
|---|---|---|---|
| motion mode → motion key | `CRaceData::GetMotionKey` 查 mode/index/subindex、父 mode 和注册资源 | PC 按注册表查具体目录;普通模式缺 key 回 GENERAL,HORSE_* 缺 key 只回 HORSE,父模式也缺失时保持失败;NPC 按 `motlist` 的 `EName` 索引解析 | PARTIAL:PC 父模式顺序和 NPC 常用动作索引已统一,完整 RaceData fallback/失败边界仍不同 |
| 随机动作资源 | `TMotion.byPercentage` 按注册顺序、`random()%100` 和百分比选择 | `motion_registry.gd` 保留 `playersettingmodule.py` 注册顺序、重复项、默认 100 及 `SetMotionRandomWeight`,PlayerView 按相同百分比选择;MobView 对 `motlist` 别名按 `EName` 归并并按同一百分比算法选择 | PARTIAL:PC/NPC 常用权重已复刻,完整注册快照和缺失资源的原生失败语义仍未闭合 |
| MotionDuration | `CRaceMotionData` 的 `.msa MotionDuration`,结束时间除以速度倍率 | `Metin2AnimPlayer` 对 `.msa` 现在以 `msa_metadata.duration` 为播放/动作结束权威,只有 raw `.gr2` 或缺失时回退 GR2 时长 | PARTIAL:权威字段已统一;GR2 采样长度、速度倍率和所有资源边界仍需固定样本矩阵 |
| Accumulation | actor motion/root movement 按三轴累计量推进 | MSA 解析和 `get_accumulation` 存在;远端主要用 walk/run speed 交给 EntityStore,本地主角位置由 PlayerController/NetPlay 驱动 | PARTIAL:远端移动有近似入口,本地 actor root motion 与三轴逐帧应用未统一证明 |
| LoopData | `MotionLoopCount`、`LoopStartTime`、`LoopEndTime` 控制片段回绕与次数 | 原生播放器按 `LoopStartTime..LoopEndTime` 回绕并递减 `motion_loop_count`,尾部再发 `playback_finished`;`anim.loop` 仍负责普通循环动作 | PARTIAL:主循环语义已统一,跨大 delta、事件边界和动作队列仍需继续证明 |
| 动作队列 | Set/Intercept 清队列,Push once/loop 预约,结束时继续队列;死亡/晕倒有特殊分支 | Player/Mob 的 HitReaction 维护本地链;普通 one-shot 与循环动作以字符串状态和 playback_finished 驱动 | PARTIAL:常见攻击/受击路径可用,但预约队列、死/晕/站起、同动作重入和技能取消分支不完整 |
| 连击输入窗 | `PreInputTime`/`DirectInputTime`/`InputLimitTime`/`LinkTime` 来自当前 `.msa`,combo segment 来自 `CRaceData` | parser/get_motion_data、NetPlay 窗口和 combo table 已接入 | PARTIAL:主要算法有;mode parent、资源权重、段动作队列和动画真实完成时刻未完全统一 |
| 普攻/技能命中窗 | `AttackingData` 的多个 THitData、sphere sweep、stiffen/invisible/external force/limit | parser 暴露 hit_windows,NetPlay 使用 `.msm` 防御球与扫掠判定 | PARTIAL:结构已映射,真实多事件 actor 时序、资源缺失和帧边界仍需 live 证据 |
| MotionEventData | 每帧遍历每个 actor,按类型执行 effect/target effect/special attack/sound/fly/show/hide/warp | `Metin2AnimPlayer` 以时间区间派发详细 signal;本地主角消费已有入口,type 6 按 `.msa` 的 `FlyFileName/FlyPosition/AttachingBoneName` 创建本地 FlyManager 实例并单独发送 `CG_SHOOT`,真实远端 Actor 经 `NetWorld` 接入,WARP 按 270cm/阻挡/朝向分支执行;有效动作绑定后按 40250 `__SetMotion -> __ShowEvent` 自动恢复隐藏状态 | PARTIAL:type 4 splash 的所有 actor 消费、type 6 骨骼/飞行特效细节和事件状态清理仍不等价 |
| 事件时序 | `StartingTime`→60 FPS frame equality;动作速度与队列决定当前帧 | 以动作时间跨越区间派发;首帧/LoopData 回绕点使用包含起点的边界,整段循环按 duration 分段遍历 | PARTIAL:首帧、回绕起点和多周期大 delta 已统一,固定帧量化、事件资源消费和所有 actor live 时序仍需证明 |
| 音效脚本 | 每个 actor 的 `SoundEventProcess` 按帧/3D 距离频率规则播放 `.mss` | Player/Mob 切动作后加载 `.mss`,按 `floor(time*60)` 更新;remote/非主角事件音效链不完整 | PARTIAL |
| `.msm`/模型动作绑定 | RaceData 同时管理 base/LOD/attachment、motion mode、collision/attaching data | `formats/msm.cpp` 已解析 base/motion-list/hair;PC/NPC 其余动作和防御球由路径约定与独立解析器提供 | PARTIAL:资源链可运行,但不是完整 RaceData 等价实现 |
| 资源加载失败 | GetMotionKey/LoadMotionData 失败即保留当前动作或返回失败;不混用别的动作 | Player/Mob 对缺失状态有别名/目录 fallback,攻击缺失回 general,另有 placeholder/测试 fallback | PARTIAL:兼容性更强但分支语义已不同,需要按 40250 逐资源决定是否允许 fallback |
## Implementation-equivalence matrix
| 项目 | 状态 | 证据/限制 |
|---|---|---|
| Preconditions | PARTIAL | MSA/GR2 加载、model skeleton、motion mode、PC playersettingmodule registry 和部分 hit/skill gate 存在;NPC/完整 RaceData registration、motion key 失败语义和 per-actor event consumer 未闭合 |
| Branch structure | PARTIAL | loop/combo/attack/event 主字段和常用状态存在;LoopData、motion queue、事件类型分支及非主角消费不等价 |
| Algorithms/formulas | PARTIAL | PC motion random 已按注册顺序、`random()%100` 和百分比实现;Accumulation/Duration/Combo/HitWindow 基础公式有实现,NPC 权重、帧量化、三轴 root motion、LoopData 回绕和事件特效绑定仍是近似 |
| State transition order | PARTIAL | bind→motion data→NetPlay hit/combo 的顺序清晰,击倒/起身早退已补齐;参考端完整 Set/Intercept/Push queue、事件与动作帧、结束回 WAIT 的顺序未完全复现 |
| Constants/units | PARTIAL | 60 FPS、速度倍率、MSA accumulation、0.15 blend、motion mode 目录和 combo key 有证据;GR2 duration 覆盖 MSA duration、cm/m 转换与骨骼 offset 仍需固定样本矩阵 |
| Timing/event sources | PARTIAL | MSA MotionDuration、LoopData 播放和 Godot 跨越事件已有统一入口;大 delta、loop count、事件重复/跳过、远端 actor 事件尚未完全覆盖 |
| Resource/data sources | PARTIAL | PC `playersettingmodule.py` 的主要 mode/index/weight 已由 `MotionRegistry` 读取并有回归;NPC/GR2/MSA/MSS/MSM 全量注册、LOD/attachment/collision 数据与所有事件资源尚未通过 |
| Protocol side effects | PARTIAL | 动作状态包、攻击/技能/飞行入口与 local motion event 有;非主角 MotionEvent、动作失败保持当前、服务器拒绝和重连清理缺 live 证据 |
| Interruption/failure/cleanup | PARTIAL | loop/one-shot/skill/受击/死亡/切图/断线的组合动作队列和事件实例清理未完整证明;本轮击倒/起身早退已覆盖,但跨图/断线和全 actor 清理仍未闭合 |
## Regression evidence
- `project/motion_registry_test.gd`:通过,确认 Warrior 的 GENERAL wait 70/30、ONEHAND wait 50/50、HORSE wait 90/9/1、GENERAL attack 50/50 注册顺序和权重。
- `project/player_motion_test.gd`、`project/hit_view_test.gd`、`project/keyboard_motion_timeline_test.gd`、`project/race_motion_assembly_test.gd`、`project/animation_blend_test.gd`:通过,覆盖父模式回退、加权变体、击倒/起身早退、基础动作切换、键盘时间线、8 race 资源装配和 crossfade;PlayerMotion 的 HORSE WAIT 断言接受 40250 注册的 `wait/wait_1/wait_2` 加权变体,不再把随机结果写死;不证明完整 RaceData/NPC weighted motion/LoopData/事件链。
- `project/test_combo_continuous_flow.gd`、`project/combat_fx_test.gd`、`project/hit_collision_test.gd`、`project/motion_effect_anchor_test.gd`、`project/target_effect_test.gd`:通过,覆盖本端连击、命中窗/防御球、挂点和目标特效 helper;不证明所有 actor 的 40250 MotionEventProcess。
- `project/mob_view_test.gd`:通过;验证 NPC 真模型、`MotionListFileName` 选择、motlist 别名/权重归并、SPAWN 缺省规则和动作缺失保持当前动作。
- `formats/tests/msm_test.cpp`:通过;验证 `.msm MotionListFileName` 与已有 BaseModel/HairData 字段一起解析。
- `project/motion_event_timing_test.gd`:通过;验证 `StartingTime=0` 首帧事件只触发一次,以及大时间步跨越多个整段循环时每个周期都派发事件。
- `project/player_motion_test.gd`:通过;验证 CHARACTER_HIDE 后再次请求相同动作或新动作时,按 40250 `__SetMotion` 语义自动恢复 actor 可见性。
- `project/motion_event_fly_test.gd`:通过;验证本地 type 6 按 `.msf` 创建一颗 FlyManager 弹道,同时只发送一份排队的 `CG_SHOOT`,缺失资源不创建实例。
- `project/animation_cache_test.gd`:通过;测试显式切换已注册的 wait/run/attack 资源,并按两个 actor 实际抽到的 weighted wait 变体计算唯一资源数,避免把随机结果当固定路径。本轮运行得到 `hits=16, misses=4` 或 `hits=17, misses=3`(`msa_*` 同步),均证明 decoded clip 和 MSA metadata 在热切换中复用。
- `project/eterngrn_polish_test.gd`:动作事件分派断言未作为完整 live 资源证据;运行有未入树 Node3D 的 transform 错误、ObjectDB/RID leak warning,需后续隔离测试 fixture。
- `build/extension/*` 的现有网络测试未覆盖 Metin2AnimPlayer 的 LoopData、weighted motion、每 actor MotionEventProcess、remote NPC/PC 事件和缓存复用边界。
### Active GameLib actor-core review
- `GameLib/ActorInstance.h`、`ActorInstanceEvent.cpp`、`ActorInstancePosition.cpp`、`ActorInstanceRotation.cpp` 和 `ActorInstanceBlend.cpp` 把状态分成位置源/目标/当前快照、advancing rotation、0.3 秒 `LookAt` blend、alpha blend、WAIT/MOVE/ATTACK/SKILL/HIT/WARP 事件回调;事件回调同时携带位置和朝向,不能只用一个字符串动画状态替代。
- `ActorInstanceData.cpp` 的 `SetRace`/`SetShape`/`SetPart` 会从 RaceData/ItemData 注册 base、LOD、attachment、motion mode 和碰撞数据;当前 `PlayerView`/`RemotePlayerView` 的模型和动作绑定已覆盖常见路径,但没有完整 `ActorInstance` 级 motion queue、父 mode、LOD/碰撞传播。
- `ActorInstanceEvent.cpp` 的 `OnUseSkill` 会把 moving-skill 标记编码进 loop 参数,`OnAttack` 使用目标旋转,`OnHit`/affect 直接进入消费者;当前 GameScene 主要消费本地主角 motion signal,远端/NPC 的完整 actor event consumer 仍缺。
## Status
`PARTIAL`。当前端已经具备 MSA 基础解析、以 MSA MotionDuration 为权威的 GR2 播放、crossfade、root speed、LoopData、连击/命中窗、本地主角事件入口,以及从 `playersettingmodule.py` 驱动的 PC mode/index/weighted motion 选择;但参考端的完整 RaceData motion key/权重/父模式、NPC/所有 actor 的帧事件处理、动作队列和事件实例清理尚未统一。本轮补齐了 PC 注册表权重、HORSE 父模式查找和击倒/起身早退,并补充对应回归,仍不能宣称完整动作资源链等价。
## Implementation fix round 2026-09-21T05:25Z
按 40250 `CRaceMotionData::GetMotionDuration()` 的资源来源,将
`Metin2AnimPlayer::reload()` 的 `.msa` 播放时长改为 `MotionDuration`;仅 raw `.gr2` 或无有效
MSA 时长时使用 GR2 duration。这样动作结束、LoopData 边界、MotionEventData 时间区间和
`get_motion_data().duration` 使用同一权威字段。
同时修正 `animation_cache_test.gd` 的压力方式:原测试通过重复请求仍在播放的攻击动作,实际
被 40250 的动作不可打断门拦截,并没有真正覆盖热重载;现在显式在相同三个已注册资源之间切换,
确认 decoded clip 与 MSA metadata 各为 17 hits / 3 misses。资源时长、技能循环、玩家动作、
战斗特效、键盘动作和动作缓存回归均通过。
## Implementation fix round 2026-09-21T05:37Z
按 40250 `playersettingmodule.py` → `CRaceData::RegisterMotionData` →
`CActorInstance::GetRandomMotionKey` 的完整 PC 动作资源链,新增
`project/ui/motion_registry.gd`:读取当前客户端随资源发布的 Python 注册表,保留 mode/index
注册顺序、重复资源、默认权重以及 `SetMotionRandomWeight` 修改,再由 `PlayerView` 按
`random() % 100` 选择 wait/受击/攻击资源。动作模式缺少 key 时补上 HORSE_* 到 HORSE 的
父模式查找,并保留普通模式回退路径;不再把 PC 变体当作等权文件名列表。
同时按 `ActorInstanceBattle.cpp::__HitGood/__HitGreate` 的
`IsKnockDown`/`__IsStandUpMotion` 早退语义,记录受击链当前步骤,防止击倒或起身期间因
`_state` 仍是 run/wait 而插入新的 DAMAGE;补充 `motion_registry_test`、父模式/权重动作回归,
并将已有动作测试改为验证注册变体集合而非依赖某次随机结果。PC 注册表和本轮动作回归通过,
完整 NPC/RaceData/事件消费者/队列清理仍保持 PARTIAL。
## Implementation fix round 2026-09-21T05:55Z
按 `RaceDataFile.cpp` / `RaceManager::__LoadRaceMotionList` 补齐 NPC 动作资源链:
`.msm` 现在解析 `MotionListFileName`,`MobView` 从选中的动作表读取并保留 40250
的动作索引映射、WAIT 等后缀最多两字符的别名规则、注册顺序、百分比权重、未知类型跳过,
并按 `random()%100` 从同一 `EName` vector 选资源;缺少 SPAWN 时复用最后一条 DAMAGE
资源,动作 key 缺失时不再将 WALK/RUN、DAMAGE/WAIT 等无关动作互相顶替。
新增/更新 `mob_view_test.gd` 与 `formats/tests/msm_test.cpp` 覆盖 `MotionListFileName`、
NPC alias/weight、COMBO 索引、未知动作、SPAWN fallback 和严格失败语义。NPC 常用动作表
已与 40250 对齐,但完整 RaceData mode/parent、所有 NPC 技能索引、MotionEvent 消费、动作
预约队列和跨 actor 清理仍保持 PARTIAL。
## Implementation fix round 2026-09-21T06:15Z
按 `ActorInstanceMotion.cpp::__MotionEventProcess` 的 frame-0 和逐帧语义修正
`Metin2AnimPlayer::dispatch_events`:非循环动作在首个动作帧包含 `StartingTime=0`,但普通
连续帧仍保持左开区间;LoopData 回绕后立即处理 `LoopStartTime` 点;整段循环按每个
duration 分段遍历,跨越多个周期时不再最多只派发一次事件。新增
`project/motion_event_timing_test.gd`,使用真实 `.msa` 资源验证首帧、去重和大 delta 多周期
事件数量。LoopData 的所有资源变体、60 FPS 离散帧与非主角事件消费者仍保持 PARTIAL。
## Implementation fix round 2026-09-21T06:20Z
按 `ActorInstanceMotion.cpp::__SetMotion` 的顺序补齐隐藏状态恢复:40250 在成功解析动作 key
后调用 `__ShowEvent()`,因此 CHARACTER_HIDE 后的下一次有效 `SetLoopMotion`(包括同一循环
动作)会恢复显示。当前 `PlayerView`/`MobView` 现在在动作 key 成功并进入 `_bind` 时清除隐藏
状态,并放宽同状态提前返回条件;缺失动作仍保持当前动作和隐藏状态,不会伪造成功。
新增 PlayerView 真实资源回归覆盖同状态与新状态两条路径。type 4 splash、type 6 `.fly`
资源消费、固定帧量化、所有 actor 事件消费者和动作队列清理仍保持 PARTIAL。
## Implementation fix round 2026-09-21T06:30Z
按 `ActorInstanceMotionEvent.cpp::ProcessMotionEventFly` 与
`FlyingObjectManager::CreateFlyingInstanceFlyTarget` 补齐本地主角的 type 6 事件链:有效
`FlyFileName` 现在经 `FlyData.load_msf` 进入已有 `FlyManager`,起点复用角色动作骨骼坐标
变换,目标沿用当前动作目标;GameScene 仍将 queued `uSkill` 只消费一次发送 `CG_SHOOT`。
服务器 `GC_CREATE_FLY` 的索引弹道未与这条本地路径合并,避免普通弓箭视觉重复。
`motion_event_fly_test.gd` 覆盖有效资源、缺失资源、弹道推进,以及“一个本地实例 + 一份
CG_SHOOT”的集成边界。远端 type 6 的资源化特效、type 4 全 actor 消费、固定 60 FPS
量化和完整事件清理仍保持 PARTIAL。