- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧 - inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域 - 包含其他累积的功能开发和测试文件
157 lines
20 KiB
Markdown
157 lines
20 KiB
Markdown
# 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。
|