fix: 装备属性面板避让逻辑 + 多项功能更新
- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧 - inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域 - 包含其他累积的功能开发和测试文件
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
# audio.bgm_ambience_3d —— BGM、区域环境音、角色音效、3D 监听器和前后台恢复
|
||||
|
||||
状态:`PARTIAL`
|
||||
|
||||
本合同检查 40250 的 2D/3D/Stream 三类声音实例、BGM 音乐状态机、区域环境声、角色动作音效、监听器坐标/方向、音量曲线、限频和前后台生命周期。判断标准是播放实例的分配、状态转换、坐标/音量公式、重复播放限制、地图卸载和恢复副作用一致;“测试能播放”不等于 SoundManager 生命周期等价。
|
||||
|
||||
## 40250 参考调用链
|
||||
|
||||
- `MilesLib/SoundManager.cpp` / `SoundManager2D.cpp` / `SoundManager3D.cpp` / `SoundManagerStream.cpp`
|
||||
- `Create` 按 2D、Stream、3D 顺序初始化驱动;`Destroy` 先停止音乐和 3D 实例,再关闭 listener/provider/driver。
|
||||
- 2D/3D 音效使用固定实例池,实例完成后才可复用;3D 位置按 `(world-listener)/soundScale`,环境声使用 `ambienceSoundScale`。
|
||||
- 角色音效可按 5000cm 半径过滤,并按文件名限制 0.3 秒内重复播放;`StopAllSound3D` 同时清空历史。
|
||||
- `MilesLib/SoundManager.cpp`
|
||||
- BGM 维护多个 `TMusicInstance`,状态为 OFF、FADE_IN、FADE_LIMIT_OUT、FADE_OUT、PLAY;`Update` 每帧递增/递减音量,达到边界后切换状态或停止实例。
|
||||
- `FadeInMusic` 先淡出已有音乐;`FadeOutAllMusic` 为每个活动实例设置淡出;`FadeLimitOutMusic` 保留目标音量。
|
||||
- `SaveVolume` 同时把音乐和音效音量置零并保存备份,`RestoreVolume` 恢复两者;不是只停止或只静音 BGM。
|
||||
- `MilesLib/SoundManager3D.cpp` / `PythonSoundManagerModule.cpp`
|
||||
- 暴露 `PlaySound`、`PlaySound3D`、`PlayMusic`、FadeIn/FadeOut、StopAllSound、音乐/音效音量、SoundScale 和 AmbienceSoundScale。
|
||||
- 3D listener 维护位置、方向、up 和速度;环境声由 `CArea::UpdateAroundAmbience` 每次更新。
|
||||
- `GameLib/Area.cpp` / `Area.h`
|
||||
- `TAmbienceInstance` 从 AreaAmbienceData/Property 读取位置、范围、最大音量半径、LOOP/ONCE/STEP 和步进随机间隔。
|
||||
- LOOP 在范围内创建循环实例并按距离线性衰减;离开范围停止并清理索引。ONCE 只在进入范围时播放,STEP 按时间到期播放,离开时重置下一次时间。
|
||||
|
||||
## 当前端实现映射
|
||||
|
||||
- `project/audio.gd`
|
||||
- 已有 BGM 双播放器、UI 音效池、AudioStreamPlayer3D、listener 重定位、200/1000 缩放、音量比例曲线、`.mss` 60FPS 事件、角色音效距离/0.3 秒限频,以及 Area LOOP/ONCE/STEP。
|
||||
- 3D 播放器按需创建,没有 40250 固定实例池/Provider 生命周期;Godot `unit_size/max_distance` 衰减也不是 Miles provider 的同一声学模型。
|
||||
- BGM 使用 Tween 交叉淡入淡出,不是 `TMusicInstance::Update` 的逐帧状态机;加载失败时 `_bgm_name` 先写入,存在“资源失败后同名请求被跳过”的状态风险。
|
||||
- `project/bgm_director.gd` / `project/game_scene.gd`
|
||||
- 已按 `bgm_changed` 和 `world_reset` 选曲,服务器音量优先,缺省回落 `musicInfo.fieldMusic`,并保存最近战场曲目。
|
||||
- 生产链已创建 Audio、BgmDirector 并在每帧更新 listener/区域环境声;地图切换时会停止 BGM,但没有完整证明参考端多音乐实例的淡出状态被逐个清理。
|
||||
- `project/app_lifecycle.gd`
|
||||
- 已在后台保存并恢复音量、暂停树、降低 FPS,保持 BGM 名称。
|
||||
- 后台入口调用 `save_volume` 后同步执行 `set_music_volume(0.0)` 和 `set_sound_volume(0.0)`;恢复仍由 `restore_volume` 一次恢复两类音量。
|
||||
- `project/game_scene.gd`
|
||||
- `_exit_tree` 在卸载 native world 前调用 `Audio.shutdown()`,停止 BGM/3D 音效、解绑播放器 stream 并清空声音缓存,覆盖退出时的资源释放边界。
|
||||
- `formats/area_data.*` / `extension/src/metin2_world.cpp`
|
||||
- 已读取 AreaAmbienceData、`.pra` 属性和 `playtype/range/maxvolume/playinterval`,按流式 tile 暴露 ambience source;world unload 会移除来源。
|
||||
- 来源数据和播放状态由不同节点维护,地图卸载/快速切图时音频实例和来源索引的生产级清理尚未有组合测试。
|
||||
|
||||
## 分支等价矩阵
|
||||
|
||||
| 40250 判据/副作用 | 当前实现 | 结论 |
|
||||
|---|---|---|
|
||||
| 2D/UI/3D/Stream 三类初始化、实例复用和销毁 | Audio 创建 BGM/UI/3D 节点,使用 Godot 动态播放器 | 部分等价;缺少固定实例池、Miles provider/driver 生命周期和完整销毁证据 |
|
||||
| BGM 多实例 OFF/FADE_IN/FADE_LIMIT_OUT/FADE_OUT/PLAY 状态机 | 双播放器 Tween 交叉淡入淡出 | 部分等价;目标可听结果近似,但状态/边界/多个音乐实例和每帧速度公式不一致 |
|
||||
| FadeIn 先处理旧曲、FadeLimitOut、FadeOutAll | 有对应方法和淡出 Tween | 部分等价;没有逐实例状态快照和停止时序证明,加载失败状态回滚也不完整 |
|
||||
| 音量比例/等级曲线、音乐/音效独立音量 | `ratio_to_apply_volume`、grade 和 UI 设置测试通过 | 基础公式等价;后台 BGM/SFX 同时静音与恢复已有回归,真实 3D 播放实例仍需独立证据 |
|
||||
| SaveVolume/RestoreVolume 同时静音和恢复 BGM/SFX | 保存两类值,后台同时调用 `set_music_volume(0)`/`set_sound_volume(0)`,前台调用 `restore_volume` | 基础行为等价;真实 Miles provider 实例和 3D 播放器清理仍需独立证据 |
|
||||
| listener 位置/方向、200/1000 缩放和 3D 播放 | `GameScene._process` 更新 listener,Audio 重定位实例 | 部分等价;单位与主要公式存在,Godot attenuation/provider 行为不是 Miles 1:1 |
|
||||
| 角色动作 `.mss` 事件、5000cm 距离和 0.3s 限频 | PlayerView/Audio 已实现,相关测试通过 | 基础路径通过;远端角色、全部动作事件和实例池耗尽/失败回退未覆盖 |
|
||||
| Area LOOP/ONCE/STEP、距离衰减和离开范围清理 | world source + `update_ambience_sources` 已实现 | 部分等价;A3 真实来源测试通过,但快速换图、音频实例释放和多 source 乱序缺少证据 |
|
||||
| 地图 BGM/区域声/音效在切图、失焦、暂停、重连后恢复 | BGM 停止、listener/ambient 每帧更新、后台 BGM/SFX 恢复存在 | 部分等价;跨图活动实例清理和真实 3D 播放器释放尚未形成生产级闭环 |
|
||||
|
||||
## 已执行测试
|
||||
|
||||
- `project/audio_miles_test.gd`:通过;覆盖 200/1000 缩放、音量曲线、`.mss` 解析、角色音效距离/限频和环境声实例基本路径。
|
||||
- `project/ambience_test.gd`:通过;A3 真实地图来源解析出 1 个 LOOP 环境声,范围、类型和声音路径保持。
|
||||
- `project/bgm_test.gd`:通过;覆盖地图 BGM 选曲、服务端音量、默认回落、换图信号,以及前后台 BGM/SFX 音量保存、同时静音和恢复。
|
||||
- `project/rendering_scenario_test.gd`:通过;Debug 原生库退出无 `ObjectDB` 音频资源泄漏,验证 `GameScene._exit_tree -> Audio.shutdown` 的退出边界。
|
||||
- `project/test_bgm_jukebox_parity.gd`:18 passed、0 failed;覆盖点唱机曲目注册、停止、音量钳制、地图默认和覆写恢复,不覆盖真实 Audio 实例状态机。
|
||||
- `project/app_flow_lifecycle_test.gd`:通过;覆盖 AppLifecycle 单一属主和断线重连流程,不覆盖真实音频实例释放。
|
||||
- `project/system_option_ui_test.gd`:通过;覆盖音量设置控件绑定,不覆盖后台和播放失败边界。
|
||||
|
||||
## Deep audit round 2026-09-21
|
||||
|
||||
### Fresh regression results
|
||||
|
||||
- `audio_miles_test.gd`、`ambience_test.gd`、`bgm_test.gd`、`test_bgm_jukebox_parity.gd` 18/18、`app_flow_lifecycle_test.gd`、`system_option_ui_test.gd` 均退出码 0。
|
||||
- 回归覆盖了 3D 缩放/限频、A3 环境声、地图 BGM 选择、点唱机覆盖/恢复、生命周期属主和设置绑定;FakeAudio 仍不能证明真实播放实例状态机。
|
||||
|
||||
### Static parity conclusion
|
||||
|
||||
- BGM/区域环境声/角色 `.mss` 音效的主要入口存在,但 Godot 动态播放器/Tween 不等价于 40250 固定实例池、Provider 和 `TMusicInstance` 状态机。
|
||||
- `app_lifecycle.gd` 后台路径已同步静音并恢复 BGM/SFX;加载失败后 `_bgm_name` 可能阻止同名重试,跨图快速切换时来源索引和播放器释放仍缺少生产级证据。
|
||||
- listener、缩放和 0.3s/5000cm 规则有局部验证,但远端 Actor 动作音效、实例池耗尽、失焦/暂停/重连/退出清理和 Godot attenuation 与 Miles provider 的边界未闭合。
|
||||
|
||||
### Active source group review
|
||||
|
||||
- 对 Visual Studio `MilesLib` 活跃编译单元逐组核对后,参考链补充确认:`CSoundManager2D/3D/Stream::Initialize/Destroy` 创建并释放固定实例池;`CSoundData` 维护 pack-backed 数据缓存和 access/play time;`CSoundInstance2D/3D/Stream` 分别处理 SetSound、Play/Stop、位置/方向/速度与音量;`CSoundManager` 负责 listener、距离缩放、0.3 秒限频、环境声、音乐实例索引和多状态淡入淡出。
|
||||
- 当前端 Audio 的动态播放器和 Tween 只覆盖可听结果的部分路径,尚未证明固定池、sound data cache、播放失败/空槽、逐实例停止和 MusicIndex 状态机等副作用一致;本组静态核对完成,合同仍为 `PARTIAL`。
|
||||
|
||||
## Implementation fix round 2026-09-22T08:50Z
|
||||
|
||||
- 修复前 `bgm_test.gd` 新增的 40250 `SaveVolume` 回归失败:进入后台后 `master_bgm=0`,但 `master_sfx=0.9`。
|
||||
- 按 `CSoundManager::SaveVolume` 的顺序,`AppLifecycle._enter_background` 在保存备份后同时调用 `set_music_volume(0.0)` 与 `set_sound_volume(0.0)`;`_exit_background` 继续由 `restore_volume` 同时恢复两者。
|
||||
- 修复后 `bgm_test.gd`、`audio_miles_test.gd`、`app_flow_lifecycle_test.gd`、`disconnect_world_reset_test.gd`、`netbridge_test.gd` 均通过;渲染退出回归无 `ObjectDB` 泄漏。
|
||||
- 该轮只闭合后台音量副作用和退出音频缓存清理,BGM 多实例状态机、Miles 固定实例池、真实 3D 衰减/Provider、快速换图来源清理仍保持 `PARTIAL`。
|
||||
|
||||
本轮仍为 `PARTIAL`,完成后台音量和退出清理的实现及第二轮证据登记。
|
||||
|
||||
## 已确认差异和未验证项
|
||||
|
||||
1. 验证正在播放的 3D 实例不会绕过 master SFX,并补真实设备/实例路径的后台静音回归。
|
||||
2. 建立 BGM 多实例状态快照测试,覆盖 FadeIn、FadeLimitOut、FadeOut、FadeOutAll、相同曲目重入、加载失败回滚和切图清理。
|
||||
3. 明确 Godot 3D 衰减与 Miles provider 的适配边界,至少固定 `unit_size/max_distance`、监听器方向/速度和实例池耗尽行为。
|
||||
4. 增加真实流式地图快速切换、来源删除、重复 key、LOOP/ONCE/STEP 乱序和退出时无悬挂播放器的测试。
|
||||
5. 补齐远端 Actor 的动作音效触发、重连/死亡/换图清理和资源缺失回退证据。
|
||||
|
||||
## 下一轮验证入口
|
||||
|
||||
- 先审计 `lifecycle.platform_release`,核对 Windows 窗口、导出、自包含资源、崩溃处理和发布门禁。
|
||||
- 回到实现阶段时,优先建立 BGM 多实例状态机和真实 3D 播放器清理测试,再处理固定实例池/Provider 的适配边界。
|
||||
@@ -0,0 +1,95 @@
|
||||
# combat.affect_status —— 受击、眩晕、击倒、状态旗标和状态图标
|
||||
|
||||
状态:`PARTIAL`
|
||||
|
||||
本合同对照 40250 Windows 客户端的两条相关但不能合并的链路:
|
||||
|
||||
1. Actor 的 `AffectFlagContainer` 变化如何驱动附着效果、眩晕/睡眠显示和石头烟雾。
|
||||
2. 服务端 `GC_DAMAGE_INFO`、`GC_STUN`、`GC_AFFECT_ADD/REMOVE` 和角色更新如何进入实体、动作、状态图标与清理流程。
|
||||
|
||||
注意:40250 Windows 客户端不在本地决定普攻中毒率、眩晕率、减速率或持续伤害;这些结果由服务端计算,客户端消费状态包和伤害包。当前端的 `combat_status_effect_system.gd` 是本地/server-like 模拟器,因此即使其单元测试通过,也不能作为客户端等价实现的证据。
|
||||
|
||||
## 40250 参考调用链
|
||||
|
||||
### Actor affect flags
|
||||
|
||||
- `UserInterface/AffectFlagContainer.cpp`
|
||||
- `Clear`、`CopyInstance`、`CopyData` 按位复制固定字节数据。
|
||||
- `Set`/`IsSet` 对每个 affect bit 做边界检查。
|
||||
- `UserInterface/InstanceBaseEffect.cpp`
|
||||
- `SetAffectFlagContainer`:建筑直接忽略;石头走 `__SetStoneSmokeFlagContainer`;普通 Actor 走 `__SetNormalAffectFlagContainer`。
|
||||
- 普通 Actor 逐 bit 比较旧/新值,只对变化位调用 `__SetAffect`、`GraphicThingInstance.__OnSetAffect` 或 `__OnResetAffect`,最后复制新容器。
|
||||
- `__ClearAffects` 在删除、重建和重置时销毁附着效果并清空容器;石头按烟雾等级重建烟雾。
|
||||
- `AFFECT_INVISIBILITY` 会清附着特效和文字尾;`AFFECT_STUN` 调用 `SetSleep`。
|
||||
- `UserInterface/NetworkActorManager.cpp` / `InstanceBase.cpp`
|
||||
- 角色创建和更新顺序包含 `SetAffectFlagContainer`,随后才刷新速度、攻击速度、阵营、PK 和 state flags。
|
||||
- `SetStateFlags` 仅负责 killer/party 等展示与目标相关状态,不等同于 affect bitset。
|
||||
|
||||
### 受击、眩晕和伤害
|
||||
|
||||
- `UserInterface/PythonNetworkStreamPhaseGame.cpp`
|
||||
- `RecvDamageInfoPacket` 找到 VID 后调用 `AddDamageEffect(damage, flag, bSelf, bTarget)`;负伤害只记录错误,不由客户端扣血。
|
||||
- `RecvAffectAddPacket` 处理能量状态时间,并调用 `BINARY_NEW_AddAffect(type, point, value, duration)`。
|
||||
- `RecvAffectRemovePacket` 调用 `BINARY_NEW_RemoveAffect(type, point)`。
|
||||
- `InstanceBase`/`ActorInstanceBattle`
|
||||
- `__HitGood`、`__HitGreate`、`__HitStone` 只负责表现层受击动作、震屏、击倒/起身队列;服务器伤害和状态仍是权威来源。
|
||||
- `__CanProcessNetworkStatePacket` 在死亡、击倒或不能取消的技能动作中拒绝网络状态输入。
|
||||
- `PythonNetworkStreamPhaseGameActor.cpp`
|
||||
- `CharacterAdd2`/`CharacterUpdate` 搬运两个 32-bit affect words 和 state flag;更新进入 Actor manager 后再刷新 Actor 表现。
|
||||
|
||||
## 当前端实现映射
|
||||
|
||||
- `extension/src/net/classic/classic_parser.cpp`、`extension/src/net/entity_store.cpp`
|
||||
- 已解析角色创建/更新的两个 affect words、`GC_STUN`、`GC_AFFECT_ADD/REMOVE` 和 `GC_DAMAGE_INFO`。
|
||||
- `Entity.affect_flags` 使用 64-bit 保存 bitset,`Affect`/`AffectChange` 保存本地玩家的状态图标数据。
|
||||
- `m2_client.cpp` 已把 affect change 转为 `affect_added`/`affect_removed` 信号。
|
||||
- `project/net_world.gd`、`project/ui/player_view.gd`、`project/ui/mob_view.gd`
|
||||
- 远端节点会收到 `set_affect_flags`,并有附着特效映射;`hit_reaction.gd` 实现 damage/knockdown/standup 队列和 shake/InsertDelay 近似。
|
||||
- `game_scene.gd`/`hud.gd`/`quickbar.gd` 已消费一般 affect 数据,但图标目前是通用色块,未逐 affect 对照 40250 的 icon/附着资源。
|
||||
- `project/net_play.gd`
|
||||
- 本地主角消费 damage、stunned、knock_down 门控,并在攻击成功时驱动目标受击表现。
|
||||
- `project/combat_status_effect_system.gd`
|
||||
- 另有客户端本地中毒、眩晕、减速、灼烧/流血施加、计时和直接改 HP 的模拟链;它不是 40250 Windows 客户端的协议消费链。
|
||||
|
||||
## 分支等价矩阵
|
||||
|
||||
| 参考判据/副作用 | 当前实现 | 结论 |
|
||||
|---|---|---|
|
||||
| AffectFlagContainer 按位复制、边界检查和旧/新差分 | native 64-bit 字段 + `mut_affect_flags`;视图用 `AFFECT_EFFECT_MAP` 遍历 | 基本等价;未覆盖完整 bit 宽度和所有 effect resource |
|
||||
| 普通 Actor affect 变化触发设置/重置附着效果 | `NetWorld._apply_field_updates` → `set_affect_flags` | 部分等价;当前按映射表重扫,未证明逐 bit 的旧/新回调和失败清理顺序 |
|
||||
| 石头烟雾按 0/1/2/3 等级销毁并重建 | 当前通用 affect 映射,没有对应 stone smoke 分支证据 | 缺失/不等价 |
|
||||
| 建筑忽略 affect flag | 当前 `_apply_field_updates` 依赖节点是否有 setter,没有显式建筑 no-op 规则 | 未证明等价 |
|
||||
| AFFECT_INVISIBILITY 清附着效果、文字尾和可见性 | 当前有 affect effect 映射,但目标选择/文字尾的隐身门控分散在其它脚本 | 不完整 |
|
||||
| AFFECT_STUN 驱动睡眠/眩晕表现 | 当前 `mut_stun` 仅置 `Entity.stunned`;本地门控可阻止行动,远端主要靠命中回调 | 不完整;没有完整远端 `SetSleep`/清除链 |
|
||||
| GC_DAMAGE_INFO 只产生 DamageEffect/飘字,不在客户端扣血 | `net_world._on_damage` 产生飘字;基础 `damage_effect_test` 通过 | 基本等价;本地主角 `_on_damage` 额外使用通用 damage/击退锁,flag 细分未完整 |
|
||||
| GC_AFFECT_ADD/REMOVE → UI 状态图标和能量时间 | native signal → `game_scene`/HUD/quickbar,基础协议测试通过 | 基本等价;图标资源、重复/乱序/过期 duration 未证明 |
|
||||
| 客户端不本地结算 poison/stun/slow/fire 结果 | `combat_status_effect_system.gd` 本地 roll、tick 并直接改 HP | 明确不等价;数据权威错误 |
|
||||
| 死亡、击倒、技能不可取消时拒收网络状态 | `EntityStore::can_process_network_state` 和 `NetPlay._can_process_network_state` 有 dead/knockdown/stun 门 | 部分等价;native 门不含 stunned,远端动作消费也未统一 |
|
||||
| 受击动作 `HitGood/Great/Stone` 的资源选择和队列 | `hit_reaction.gd`/PlayerView/MobView 有近似队列 | 部分等价;`hit_view_test.gd` 当前有 1 项失败,前向 damage 仍停留在 combo 资源 |
|
||||
|
||||
## 已执行测试
|
||||
|
||||
- `project/test_combat_status_effect_parity.gd`:24/24 PASS;只能证明当前本地模拟器自身行为,不证明客户端权威模型正确。
|
||||
- `project/damage_effect_test.gd`:PASS;覆盖伤害飘字分类和显示门。
|
||||
- `project/combat_fx_test.gd`:PASS;覆盖命中、连击、shake、affect 和死亡主链。
|
||||
- `project/test_mob_combat_reaction_parity.gd`:17/17 PASS;覆盖怪物受击和击倒 helper。
|
||||
- `project/test_combat_death_pickup_entry_parity.gd`:PASS;覆盖死亡/动作锁相关入口。
|
||||
- `build/extension/net_entity_test`:PASS;覆盖 native stun/affect add/remove 等状态存储。
|
||||
- `build/extension/net_classic_session_test`:PASS;覆盖 classic session 状态包入口。
|
||||
- `project/hit_view_test.gd`:FAIL,1 项:`fScalar < 0` 的前向受击期望 `damage/damage_1.msa`,实际仍为 `onehand_sword/combo_01.msa`。
|
||||
|
||||
## 已确认差异和未验证项
|
||||
|
||||
1. `combat_status_effect_system.gd` 把服务端战斗状态结算复制到客户端,包含随机 roll、持续时间、毒/火跳伤害和直接改 HP;必须与协议消费链分离,否则会造成双重结算或离线假状态。
|
||||
2. native `GC_STUN` 目前只置 `stunned=true`,没有专门的解除包/清除路径;当前依赖后续实体更新/重建恢复,未证明与 40250 Actor affect 清除顺序一致。
|
||||
3. `NetWorld._apply_field_updates` 没有 40250 `__SetNormalAffectFlagContainer` 的逐 bit 差分回调,也没有 stone smoke/building/no-op 的显式分支。
|
||||
4. 远端 `stunned`/`knock_down` 没有统一进入 `SetSleep`、击倒动作和网络状态拒收链;本地 `NetPlay` 与远端 `NetWorld` 的门控职责不一致。
|
||||
5. `hit_view_test.gd` 的失败说明受击动作资源/动作锁时序仍有真实差异,当前不能将受击合同标记为通过。
|
||||
6. 需要补齐 affect bit→effect/icon 资源索引、重复/乱序 add/remove、duration 过期、死亡/切图/重连清理、隐身文字尾、stone smoke 和建筑 no-op 测试。
|
||||
|
||||
## 下一轮验证入口
|
||||
|
||||
- 先移除或隔离本地状态结算器的权威写入,只保留服务端 affect/damage 包驱动的视图与 UI。
|
||||
- 为每个 affect bit 建立 40250 资源、附着骨骼、开关、清理和石头烟雾矩阵。
|
||||
- 增加 `GC_AFFECT_ADD → duration → GC_AFFECT_REMOVE`、重复/乱序/重连/换图的 native→HUD 端到端测试。
|
||||
- 增加远端 Actor 的 stunned/knockdown/skill-cancel 状态机测试,并修复 `hit_view_test.gd` 的前向 damage 资源选择后再提升合同状态。
|
||||
@@ -0,0 +1,239 @@
|
||||
# combat.attack_combo_damage
|
||||
|
||||
## Scope
|
||||
|
||||
审计普攻、连击输入窗口、攻击动作状态包、`.msa` 命中窗口、碰撞命中、伤害包、击退/硬直、弓箭 FLY 帧和换图清理。
|
||||
|
||||
## Reference call chain
|
||||
|
||||
- `CPythonPlayer::NEW_Attack`
|
||||
- 检查 private shop、`__CanAttack`、死亡/晕眩/施法/坐骑等级/弓箭条件。
|
||||
- 按目标/方向调用 `CInstanceBase::NEW_Attack` 或 `NEW_AttackToDestInstanceDirection`。
|
||||
- `CInstanceBase::InputComboAttack` / `__ComboProcess` / `__RunNextCombo`
|
||||
- 根据职业、武器、坐骑、连击技能等级和 `ComboInputData` 推进段号;动作结束时清理 combo。
|
||||
- `CPythonPlayerEventHandler::OnAttack`
|
||||
- 每个动作段发送 `FUNC_COMBO + motion index` 状态包。
|
||||
- `CActorInstance::AttackProcess` / `__NormalAttackProcess`
|
||||
- 在 `.msa` 的 hit window 内用武器扫掠球和目标 `.msm` 防御球检测命中。
|
||||
- `CPythonPlayerEventHandler::OnHit`
|
||||
- 设置目标并发送 `CG_ATTACK`;根据受击方 owner、巨型/霸体和外力执行击退及同步位置。
|
||||
- `CNormalBowAttack_FlyEventHandler_AutoClear`
|
||||
- 起手 `OnSetFlyTarget` 发送目标,`.msa FLY` 帧 `OnShoot` 发送 `CG_SHOOT`;事件处理器随旧 Actor/动作生命周期失效。
|
||||
|
||||
## Current call chain
|
||||
|
||||
- `NetPlay._on_pick` / `_update_auto_attack` / `_reserve_process_click_actor`
|
||||
- 目标选择、攻击距离、自动攻击和预约追击。
|
||||
- `NetPlay._do_attack_swing` / `_run_next_combo` / `_combo_process`
|
||||
- 播放动作、发送 `FUNC_COMBO`、读取 `.msa` combo/hit windows 和攻击速度。
|
||||
- `NetPlay._attack_process` / `_normal_attack_process`
|
||||
- 读取攻击者采样点、目标 defending spheres,执行 Z-cylinder 几何判定和 hit dedup。
|
||||
- `NetPlay._process_attack_success` / `_on_hit`
|
||||
- 受击动画、特效、硬直、击退、目标更新和 `client.attack`。
|
||||
- `GameScene._queue_shot` / `_on_local_motion_event(type=FLY)`
|
||||
- 将弓箭技能先排队,等 `.msa FLY` 帧再发送 `CG_SHOOT`;`_on_world_reset` 现在清理旧队列。
|
||||
|
||||
## Branch matrix
|
||||
|
||||
| 场景 | 40250 | 当前端 | 结论 |
|
||||
|---|---|---|---|
|
||||
| 普攻输入 | `NEW_Attack` 由方向/目标/按键分派,并受 CanAttack gates 限制 | `_on_pick`、SPACE/auto attack 和 `_do_attack_swing` 已接入 | PARTIAL:鼠标 press/release owner 不完全相同 |
|
||||
| 连击段 | `ComboInputData` 的 start/next/end 时间和动作尾清理段号 | `.msa` 数据、combo 表和 motion_bound 清理已实现 | MAPPED + tests |
|
||||
| 状态包 | 每段 `FUNC_COMBO + motion index`,技能使用 `FUNC_SKILL|motion` | `_send_state` 发送对应 func/arg/位置 | MAPPED |
|
||||
| `.msa` 命中窗 | AttackProcess 每帧扫掠真实武器球与 `.msm` 防御球 | 仅在有效 `AttackingData/HitDataContainer` 和真实 defending sphere/bone 存在时判定 | FIXED + regression;WeaponTrace 视觉副作用仍未等价 |
|
||||
| 命中限制 | `m_HitDataMap`、`fInvisibleTime`、hit limit、combo 每窗去重 | `_hit_dedup`、invisible、limit 和 combo 分支已实现 | MAPPED + tests |
|
||||
| 受击后果 | `OnHit` 发 CG_ATTACK,按 owner/巨型/霸体决定击退和 sync position | `_process_attack_success` / `_on_hit` 已映射 | PARTIAL:服务端确认/死亡包实际闭环未 fixture 化 |
|
||||
| 弓箭 | 起手 target,FLY 帧才 shoot;旧 handler 清除 | `_pending_shots` 等 FLY 帧发送;本轮已在 world reset 清空 | MAPPED + regression |
|
||||
| 换图/断线 | 旧 Actor handler 和动作状态失效,不带旧攻击事件 | reset 已清 shot queue,其他攻击/命中窗状态仍分散 | PARTIAL |
|
||||
|
||||
## Equivalence matrix
|
||||
|
||||
| 项目 | 状态 | 证据 |
|
||||
|---|---|---|
|
||||
| Preconditions | MAPPED | `_can_attack`、safe/dead/stun/skill/mount/arrow gates 已定位 |
|
||||
| Branch structure | PARTIAL | 鼠标 press/release、方向攻击、自动攻击、弓箭和坐骑完整排列不足 |
|
||||
| Algorithms/formulas | PARTIAL | combo/hit geometry 已映射;缺失资源 fallback 已移除,WeaponTrace 和服务端伤害仍不等价 |
|
||||
| State transition order | PARTIAL | 动作绑定、hit window、CG_ATTACK、击退和 sync position 顺序需包级证明 |
|
||||
| Constants/units | PARTIAL | 攻击距离、speed ratio、hit limit、stiffen/invisible 等需从资源和 fixture 双证 |
|
||||
| Timing/event sources | PARTIAL | 参考由 Actor motion event 驱动,当前由 Godot `_process` 和 signal 驱动 |
|
||||
| Resource/data sources | PARTIAL | `.msa/.msm` 资源读取和缺失资源拒绝分支已接入;真实资源覆盖与 WeaponTrace 数据源仍未完全证明 |
|
||||
| Protocol side effects | PARTIAL | FUNC_COMBO/CG_ATTACK/CG_SHOOT 已映射,真实服务端回包闭环未验证 |
|
||||
| Interruption/failure/cleanup | PARTIAL | 本轮补了 pending shot;死亡、换图、断线、动作缺失和重复命中仍需测试 |
|
||||
|
||||
## Regression evidence
|
||||
|
||||
- `project/combat_fx_test.gd`:攻击锁定、连击段、hit window、命中去重、击退、抗击倒、弓箭 target/shoot 时序。
|
||||
- `project/combat_parity_test.gd`:攻击锁、伤害数字和受击反馈。
|
||||
- `project/gamescene_test.gd`:world reset 清理旧弓箭 FLY/CG_SHOOT 队列。
|
||||
- `extension/tests/net_state_queue_test.cpp`:远端 `FUNC_ATTACK`、`FUNC_COMBO`、`FUNC_SKILL` 到达/动作状态。
|
||||
|
||||
### Active GameLib weapon-trace review
|
||||
|
||||
- `GameLib/WeaponTrace.cpp`/`.h` 使用动态池,按武器骨骼矩阵采样时间点,构建连续刀光顶点带,并分别支持纹理/alpha、生命周期、采样间隔、reach scale、TurnOn/TurnOff 和清理;它是 `ActorInstance` 攻击动作的渲染副作用,不是一次性的命中特效。
|
||||
- 当前战斗链有 `skill_fx`、hit effect 和动作事件,但没有证明每个武器类型的骨骼采样、刀光顶点、alpha/texture 模式和跨动作清理与 `WeaponTrace` 相同;现有 `combat_fx_test` 只覆盖战斗桩和效果入口。
|
||||
- 本轮完成 `WeaponTrace` 活跃源码静态核对,攻击/连击合同继续为 `PARTIAL`。
|
||||
|
||||
## Status
|
||||
|
||||
`PARTIAL`。普攻/连击主体已经有较完整的本地模拟;当前已删除缺少有效动作攻击数据时的本地 CG_ATTACK 合成路径,但 WeaponTrace、服务端实际伤害结果和完整输入/生命周期矩阵仍未达到 40250 完全等价。
|
||||
|
||||
## Audit round 2026-09-20T21:12Z
|
||||
|
||||
本轮沿 `CActorInstance::AttackingProcess -> __NormalAttackProcess -> OnHit` 重新核对当前攻击实现,并重跑 `combat_fx_test`、`combat_parity_test`、`gamescene_test` 和 23 项 native CTest:
|
||||
|
||||
- 状态包顺序、命中窗建立、`fInvisibleTime`/hit-limit 去重、受击硬直/击退、弓箭 FLY 帧以及换图清理已有可执行实现和回归证据。
|
||||
- 明确保留差异:当前 `_emit_swing` 在缺少 `.msa` 命中窗时立即退化发送 `CG_ATTACK`,`_normal_attack_process` 在空 samples 时使用正前方/近距离兜底;40250 在没有有效 `m_pkCurRaceMotionData`/`HitDataContainer` 时不会合成这条本地命中路径。
|
||||
- `WeaponTrace` 仍只有参考端源码证据,当前没有动态武器骨骼采样、刀光顶点带、alpha/texture 模式和动作中断回收的等价实现。
|
||||
- native CTest 首次串行回归出现一次 `net.classic_encstream` 的加密头断言失败,单测复跑和全量复跑均通过;暂记为测试环境/时序抖动,不作为本轮攻击实现通过依据。
|
||||
|
||||
结论:缺失资源 fallback 已按 40250 的无有效攻击数据分支修复;WeaponTrace 和真实服务端伤害/死亡/击退包序进入下一轮修复候选,合同保持 `PARTIAL`。
|
||||
|
||||
## Audit and implementation round 2026-09-21
|
||||
|
||||
沿 `ActorInstanceBattle.cpp::ComboAttack`、`ActorInstanceMotion.cpp::__SetMotion` 和
|
||||
`PythonPlayerEventHandler::OnAttack` 重新核对事件顺序,确认参考端是:先绑定
|
||||
`InterceptOnceMotion`(同时绑定攻击数据、清理命中表,并触发弓箭 `OnSetFlyTarget`),再由
|
||||
`__OnAttack` 发送 `FUNC_COMBO + motion index`。当前端原先在 `_emit_swing` 中先发状态包、再绑定
|
||||
Godot 动作,存在服务端状态已发出但本地命中窗口尚未建立的顺序差异。
|
||||
|
||||
本轮已调整为“绑定动作/建立命中窗口 → 弓箭 SetFlyTarget → 发送 FUNC_COMBO”,并新增
|
||||
`combat_fx_test.gd` 的跨对象顺序断言;`combat_fx_test.gd`、`combat_parity_test.gd`、
|
||||
`gamescene_test.gd` 均通过。
|
||||
|
||||
仍保持 `PARTIAL`:当前没有 `WeaponTrace` 的骨骼采样/刀光生命周期实现,也没有真实服务器对
|
||||
`CG_ATTACK`、`CG_SYNC_POSITION`、死亡/伤害/击退包的端到端 fixture;弓箭和攻击状态在换图、
|
||||
断线、动作中断后的全部清理仍需要继续核对。
|
||||
|
||||
## Implementation fix round 2026-09-20T21:18Z
|
||||
|
||||
按 40250 `CActorInstance::isValidAttacking` 与
|
||||
`ActorInstanceCollisionDetection.cpp::__NormalAttackProcess` 修复攻击命中源:
|
||||
|
||||
- `NetPlay::_emit_swing` 不再在缺少当前动作 `AttackingData/HitDataContainer` 时直接发送 `CG_ATTACK`;仍只发送动作对应的 `FUNC_COMBO` 状态包,真正的 `CG_ATTACK` 只能由命中处理链触发。
|
||||
- `NetPlay::_attack_process` 现在要求当前动作存在有效 `AttackingData` 和命中窗;空命中窗不命中。
|
||||
- `_defending_spheres` 不再为缺少 `.msm` 防御球的节点合成占位胶囊;空 `m_DefendingPointInstanceList` 直接无命中。
|
||||
- `combat_fx_test.gd`、`netplay_test.gd` 增加缺少动作攻击数据和缺少防御球的反例,`combat_parity_test.gd`、`playable_combat_test.gd` 同步回归通过。
|
||||
|
||||
本轮修复了“缺失资源仍产生本地命中”的结构差异;`WeaponTrace` 骨骼采样/刀光生命周期、
|
||||
真实服务端伤害确认和死亡/击退包序仍保持 `PARTIAL`。
|
||||
|
||||
## Audit round 2026-09-20T21:24Z — WeaponTrace call-chain review
|
||||
|
||||
本轮只做差异审计,没有把现有 `has_trail` 配置字段或一次性 `.mse` 特效误判为
|
||||
`WeaponTrace` 已实现。沿 40250 的完整调用链核对结果如下:
|
||||
|
||||
- `ActorInstanceAttach.cpp::AttachWeapon` 仅对非 bell/fan/bow 武器创建
|
||||
`CWeaponTrace`,并绑定武器模型实例;当前 `equip_model.gd` 只设置
|
||||
`weapon_gr2`/`shield_gr2` 与强化 `.mse`,没有按武器类型创建轨迹对象。
|
||||
- `ActorInstance::INSTANCEBASE_Deform` 每帧执行 `TraceProcess`;当前
|
||||
`PlayerView`/`Metin2Model` 没有对应的每帧武器骨骼采样出口。
|
||||
- `CWeaponTrace::Update` 同时维护 short/long 两条世界坐标时间队列,使用默认
|
||||
`lifeTime=0.18s`、`samplingTime=0.003s`、武器 bound-box 长度和
|
||||
`__GetReachScale()` 生成刀根/刀尖采样;当前端没有时间队列、reach scale、采样间隔
|
||||
或真实武器骨骼矩阵等价物。
|
||||
- `CWeaponTrace::BuildVertex/Render` 用 cubic spline 生成 triangle strip,分别提供
|
||||
alpha/texture 渲染模式;当前端没有 WeaponTrace mesh、顶点带、材质模式或独立
|
||||
`RenderTrace` 阶段。`socket_glow_fx_system.gd` 的 `has_trail` 只是强化配置结果,
|
||||
没有消费方。
|
||||
- `ActorInstanceMotion.cpp::__SetMotion` 在绑定新动作前统一 `__HideWeaponTrace`,
|
||||
成功绑定可攻击动作后 `__ShowWeaponTrace`;`Destroy/Clear` 删除全部 trace,
|
||||
`TurnOff` 保留旧队列以便自然淡出。当前 `_bind`、播放结束、换装、换图和销毁路径
|
||||
都没有这一组开关/淡出/清理语义。
|
||||
|
||||
现有证据:`player_motion_test.gd`、`combat_fx_test.gd`、`combat_parity_test.gd` 和
|
||||
`playable_combat_test.gd` 通过,但它们只能证明动作/战斗门和命中链,不能证明刀光。
|
||||
`weapon_attach_test.gd` 当前还存在 race 0 的 03150 双手动作左手贴合断言失败并伴随
|
||||
exit 139;该失败属于武器动作绑定的独立问题,也说明不能用当前武器挂点测试替代
|
||||
WeaponTrace 证据。
|
||||
|
||||
结论:`combat.attack_combo_damage` 继续为 `PARTIAL`。下一次实现应新增独立
|
||||
`WeaponTrace` 适配层,并在真实武器模型上验证“创建过滤 → 动作开关 → 双轨迹采样 →
|
||||
刀光网格/alpha → 动作中断自然淡出 → 换装/换图销毁”的全链路;在此之前不得声称
|
||||
40250 的武器刀光已经复刻。
|
||||
|
||||
## Implementation round 2026-09-20 — WeaponTrace adapter
|
||||
|
||||
已新增 `project/ui/weapon_trace.gd`,并将它接入
|
||||
`project/ui/player_view.gd` 与 `project/ui/equip_model.gd`,对应 40250 的以下调用边界:
|
||||
|
||||
- `EquipModel._weapon_trace_enabled()` 对 bell/fan/bow 不创建刀光,普通武器才允许创建;换装和空武器路径执行 detach/clear/free。
|
||||
- `PlayerView._refresh_weapon_trace()` 在附着的 `Weapon` 模型上建立 trace;`_bind()` 在绑定新动作前 `TurnOff`,攻击/连击动作成功绑定后 `TurnOn`,播放结束保留旧点并自然淡出。
|
||||
- `WeaponTrace._process()` 每帧维护 short/long 世界坐标时间队列;默认 `0.18s` 生命周期、`0.003s` spline 采样步长,并复刻参考端三对角 cubic spline 与 triangle strip 顶点交错。
|
||||
- `StandardMaterial3D` 提供双面、透明、无光照、不写深度的 alpha 路径,并保留 texture/alpha 两种模式;独立 `weapon_trace_test.gd` 覆盖附着、采样、网格、TurnOff 尾迹、过期、重挂和 PlayerView 攻击开关。
|
||||
|
||||
当前仍有明确适配差异,不能标记为完全一致:Godot 端点由 attached weapon 的 mesh AABB 推导,尚未由 native bone matrix 直接提供
|
||||
`m_fLength * __GetReachScale()` 的同一坐标语义;远端/怪物渲染覆盖、`lot_ade10-2.tga`
|
||||
真实视觉资源和服务端动作资源驱动的截图证据仍缺;`weapon_attach_test.gd` 的 race 0 /
|
||||
03150 双手动作左手贴合断言仍失败并 exit 139。`socket_glow_fx_system.gd::has_trail`
|
||||
仍是独立强化配置,不能当作 WeaponTrace 消费方。
|
||||
|
||||
结论:本轮把 WeaponTrace 从“生产链缺失”修复为“生产链已接入、核心生命周期有证据、坐标端点和覆盖范围仍 PARTIAL”。
|
||||
|
||||
## Implementation fix round 2026-09-21T03:42Z — MainInstance damage billboard
|
||||
|
||||
沿 40250 `PythonNetworkStreamPhaseGame::RecvDamageInfoPacket` →
|
||||
`CInstanceBase::AddDamageEffect` → `CInstanceBase::ProcessDamage` 复核本地伤害显示:
|
||||
参考端只要 `CharacterManager` 找到目标实例,且目标就是 `MainInstance`,仍会按
|
||||
`bSelf` 入伤害队列并在本地主角上显示 `damage_*.dds`/MISS。
|
||||
|
||||
当前端 GameScene 为避免把本地主角重复挂入 `_by_vid`,将其单独保存为 `_local_node`。
|
||||
修复前 `NetWorld._process_damage_queue()` 只从 `_by_vid` 取节点,导致本地主角自己的
|
||||
伤害包已经入队,却在渲染前被静默丢弃。现在对本地 VID 使用 `_local_node` 回退,保持
|
||||
普通远端 actor 与 MainInstance 的同一 `ProcessDamage` 分类和渲染路径。
|
||||
|
||||
新增 `project/damage_local_main_parity_test.gd`,修复前专项回归失败,修复后通过;
|
||||
合同仍为 `PARTIAL`,因为服务端真实伤害/死亡包、WeaponTrace 坐标端点、远端/怪物覆盖和
|
||||
攻击生命周期清理尚未达到 40250 完全等价。
|
||||
|
||||
## Implementation fix round 2026-09-21T03:46Z — multi-part WeaponTrace
|
||||
|
||||
沿 `ActorInstanceAttach.cpp::__IsRightHandWeapon`、`__IsLeftHandWeapon` 和
|
||||
`CActorInstance::AttachWeapon` 继续核对:参考端不是每个角色只有一个刀光。
|
||||
匕首会同时挂 `PART_WEAPON` 与 `PART_WEAPON_LEFT`,骑马扇也可能有双部件;每个可追踪
|
||||
部件都独立创建一个 `CWeaponTrace`,并由 `m_WeaponTraceVector` 统一执行开关、纹理和
|
||||
清理操作。
|
||||
|
||||
修复前当前 `PlayerView._refresh_weapon_trace()` 只查找 `Weapon` 并维护一个对象,
|
||||
因此第二个部件的刀光不会出现。现在按 `Weapon`、`Shield` 部件顺序维护多条 trace,
|
||||
逐部件执行 `attach/TurnOn/TurnOff/detach`,并补齐对应的纹理、Texture/Alpha 模式
|
||||
控制入口。新增 `project/weapon_trace_multi_part_parity_test.gd`,修复前回归失败,
|
||||
修复后与 `weapon_trace_test.gd`、`equip_model_test.gd`、`weapon_attach_test.gd`、
|
||||
`player_motion_test.gd` 和 `combat_fx_test.gd` 一起通过。
|
||||
|
||||
仍为 `PARTIAL`:当前刀根/刀尖端点仍由 Godot mesh AABB 推导,不是参考端骨骼矩阵加
|
||||
`__GetReachScale()`;远端/怪物 trace 覆盖、真实刀光纹理视觉和服务端攻击闭环仍未证明。
|
||||
|
||||
## Implementation fix round 2026-09-21T03:55Z — WeaponTrace geometry and reach scale
|
||||
|
||||
本轮继续沿 40250 的 `GameLib/WeaponTrace.cpp`、
|
||||
`InstanceBaseEffect.cpp::__Warrior_SetGeomgyeongAffect` 和
|
||||
`ActorInstanceCollisionDetection.cpp::__NormalAttackProcess` 核对端点与攻击扫掠:
|
||||
|
||||
- 参考端在 `SetWeaponInstance` 中从武器 bound box 到骨骼原点计算
|
||||
`m_fLength`,`Update` 再沿武器局部 Z 轴使用 `m_fLength * fReachScale` 生成刀尖;
|
||||
当前端不再把任意 AABB 角点当作方向,而是取 attached mesh AABB 的最大半径作为长度,
|
||||
沿局部 Z 轴延伸,并提供可验证的 `reach_scale`。
|
||||
- 参考端 `AFFECT_GEOMGYEONG` 开启时把 reach scale 设为 `1.5`,关闭时恢复 `1.0`;
|
||||
当前 `PlayerView` 将该状态广播到所有 Weapon/Shield trace,并在重新附着时保留当前值。
|
||||
- 参考端普通攻击把 `dsiSrc.v3Position - dsiSrc.v3LastPosition` 乘以
|
||||
`__GetReachScale()` 后再参与碰撞变换;当前 `NetPlay._normal_attack_process` 通过
|
||||
`_attack_sample_positions` 对同一段位移应用该缩放,因此视觉刀光和命中扫掠共用同一
|
||||
reach scale 来源。
|
||||
|
||||
修复前专项回归分别复现了端点方向/长度错误及缺少 reach scale API、Geomgyeong 未改变
|
||||
reach scale、攻击采样没有暴露 reach-scaled endpoint;修复后以下测试通过:
|
||||
`weapon_trace_endpoint_parity_test.gd`、`weapon_trace_reach_affect_parity_test.gd`、
|
||||
`weapon_reach_scale_attack_parity_test.gd`、`weapon_trace_multi_part_parity_test.gd`、
|
||||
`weapon_trace_test.gd`、`combat_fx_test.gd`、`combat_parity_test.gd`、
|
||||
`playable_combat_test.gd`、`test_combat_status_effect_parity.gd`、`netplay_test.gd`。
|
||||
|
||||
合同仍为 `PARTIAL`:当前端点的长度输入来自 Godot attached mesh AABB,尚未直接消费
|
||||
native `CActorInstance` 的同一骨骼 composite matrix、旋转和绑定坐标语义;远端/怪物
|
||||
覆盖、真实刀光纹理视觉、服务端真实攻击/死亡包级 fixture 和跨动作/断线清理仍未闭合。
|
||||
|
||||
## Audit evidence reconciliation 2026-09-21T04:00Z
|
||||
|
||||
重新运行当前工作树的 `weapon_attach_test.gd`,加载 `00010.gr2` 和 `03150.gr2` 均正常,
|
||||
进程退出码为 0;此前记录的 race 0 / 03150 左手贴合断言失败不再是当前待办项,已从
|
||||
manifest 的 `remaining` 移除。该校正只更新证据状态,不改变 WeaponTrace native
|
||||
骨骼 composite matrix、远端/怪物覆盖和服务端攻击闭环仍未证明的结论。
|
||||
@@ -0,0 +1,280 @@
|
||||
# combat.death-exp-drop
|
||||
|
||||
## Scope
|
||||
|
||||
比较普通攻击/技能命中后的动作事件、`GC_DAMAGE_INFO`、`GC_DEAD`、主角死亡、`POINT_EXP` 增量、`GC_CREATE_FLY(FLY_EXP)`、地面掉落、归属校验、重复事件、复活和地图切换清理。
|
||||
|
||||
## Reference call chain
|
||||
|
||||
- `UserInterface/PythonNetworkStreamPhaseGame.cpp::RecvDamageInfoPacket`
|
||||
- 读取目标 VID、伤害值和 `EDamageFlag`,交给角色管理器显示伤害/受击表现;DAMAGE_FLYING 等特殊 flag 进入击飞状态。
|
||||
- `UserInterface/PythonNetworkStreamPhaseGame.cpp::RecvDeadPacket`
|
||||
- 找到 `CInstanceBase` 后调用 `Die`;若 VID 是主角,非决斗时通知 `OnGameOver`,并调用 `CPythonPlayer::NotifyDeadMainCharacter` 清除自动攻击目标。
|
||||
- `UserInterface/InstanceBaseBattle.cpp::CInstanceBase::Die`
|
||||
- 卸马鞍、清 AFFECT、取消选中/目标,再进入 `CActorInstance::Die`;底层 `m_isRealDead` 门保证死亡动作只启动一次。
|
||||
- `UserInterface/PythonNetworkStreamPhaseGame.cpp::RecvPointChange`
|
||||
- 对所有点数先 `ShowPointEffect(Type, VID)`;主角更新 `CPythonPlayer::SetStatus` 和 HUD,等级/属性/技能/金币走专门刷新;`POINT_EXP` 使用 `TPacketGCPointChange.amount` 作为本次权威增量。
|
||||
- `UserInterface/PythonNetworkStreamPhaseGame.cpp::RecvCreateFlyPacket`
|
||||
- 只按 `bType/startVID/endVID` 创建客户端飞行表现;经验数值仍来自 `POINT_EXP`,经验球不能替代点数包。
|
||||
- `UserInterface/PythonNetworkStreamPhaseGameItem.cpp::RecvItemGroundAddPacket` / `RecvItemGroundDelPacket`
|
||||
- 创建/删除地面物品、记录 vnum/坐标/owner;拾取请求由客户端发送,归属和结果由服务端裁决。
|
||||
- `UserInterface/PythonPlayer.cpp::NotifyCharacterDead`
|
||||
- 目标死亡时清除当前 target,避免死亡实体继续被攻击/显示目标框。
|
||||
- `GameLib/FlyingData.cpp` / `PythonEffectModule.cpp`
|
||||
- `FLY_EXP` 只负责经验球视觉的起点、终点和资源类型,不本地计算经验。
|
||||
|
||||
## Current call chain
|
||||
|
||||
- `extension/src/net/classic/classic_parser.cpp`
|
||||
- 解析 `GC_DAMAGE_INFO`、`GC_DEAD`、`GC_POINT_CHANGE`、`GC_CREATE_FLY`、`GC_ITEM_GROUND_ADD/DEL`,写入 `EntityStore`。
|
||||
- `extension/src/net/entity_store.cpp::mut_set_point` / `mut_dead` / `mut_ground_add`
|
||||
- 保存主角绝对点数和 `POINT_EXP.amount`,设置实体 HP/dead,维护地面掉落队列和飞行 cue;`take_exp_gain()` 只消费一次累计增量。
|
||||
- `extension/src/net/m2_client.cpp::_process` / classic mirror
|
||||
- 将 `points_changed`、`vitals_changed`、去重后的 `entity_dead`、`ground_item_added/removed`、`fly_cue` 和 `damage` 发给 GDScript。
|
||||
- `project/net_world.gd::_on_dead` / `_on_damage` / `_on_fly`
|
||||
- 隐藏 HP/名字/目标效果,标记死亡并延迟渐隐尸体;显示受击表现;对 `FLY_EXP` 生成三枚追踪经验球并指向本地主角。
|
||||
- `project/net_play.gd::_on_points` / `_trigger_exp_gain` / `_clear_target`
|
||||
- 优先使用 `exp_gain`,其次从绝对经验差计算反馈;HUD 更新经验/等级;目标死亡时清目标并记录经验球起点。
|
||||
- `project/ui/ground_items.gd`
|
||||
- 贴地生成掉落物、显示 owner,并按 40250 `GetCloseItem` / `SendClickItemPacket`
|
||||
实现 `DISTANCE_APPROX` 300cm、VID tie-break、party/anti-flag 归属检查和
|
||||
`pickup_item` 请求。
|
||||
- `project/ui/death_ui.gd` / `project/ui/quickbar.gd`
|
||||
- 主角死亡显示复活面板并禁用快捷栏;复活后的正 HP/vitals 解除面板。
|
||||
|
||||
## Audit findings (2026-09-20)
|
||||
|
||||
### 已对齐的部分
|
||||
|
||||
- `GC_POINT_CHANGE` 的 `POINT_EXP.amount` 已在 classic 和 m2dev 两条解析路径保留并累计;表现层不再只比较绝对 EXP,因此跨等级 EXP 回零仍能显示增量。
|
||||
- `GC_DEAD`、HP<=0、`GC_DAMAGE_INFO`、目标清除、死亡实体不可选/不可阻挡、地面物品添加/删除和 owner 拒绝路径已有实现。
|
||||
- `GC_CREATE_FLY` 的经验球只负责视觉;当前 `_trigger_exp_gain` 不再额外合成第二批经验球,避免一个服务端飞行包造成双倍表现。
|
||||
- 掉落物坐标转换、地面贴合、owner 独立文本、普通/骑马拾取距离以及服务端拾取请求已有回归证据。
|
||||
|
||||
### 材料差异
|
||||
|
||||
- 40250 主角收到 `GC_DEAD` 时会执行 `OnGameOver`、清除自动攻击并进入死亡 Actor 状态;当前 `entity_dead` 主要被 DeathUI/Quickbar 消费,`NetPlay` 没有统一把主角死亡传给 `PlayerController.stop`/动作锁和自动攻击清理,主角节点若正在点地/WASD 可能继续响应移动。这是和用户此前“死亡后仍有动作/控制”风险直接相关的 P0 差异。
|
||||
- 40250 `CInstanceBase::Die`/`CActorInstance::Die` 有 `m_isRealDead` 一次性门、清 affect、卸马鞍、OnUnselected/OnUntargeted;当前 `_on_dead` 依赖 `dead_seen` 和 `meta` 的外围去重,死亡视觉、NetWorld、PlayerController、技能/钓鱼/自动攻击状态没有一个共同的 death transition,重复 `GC_DEAD`/HP<=0/重建时序仍缺端到端矩阵。
|
||||
- 40250 的 `RecvPointChange` 对每个点类型先显示 point effect,并按 POINT_LEVEL/属性/技能/金币走分支刷新;当前 `_on_points` 只完整处理经验、等级、HUD 数值和少量音效,普通 POINT_* 的上浮数字/point effect、金币 amount 专门反馈和属性/技能窗口刷新没有完全等价。
|
||||
- 40250 `RecvCreateFlyPacket` 要求起点和终点 actor 均存在,否则只丢弃视觉;当前为支持主角未在 `_by_vid` 的结构做了 `_local_node` 特判,这属于合理平台适配,但远端起点/终点缺失、目标坐标 fallback、地图切换时飞行队列清理仍需要协议级对照。
|
||||
- 40250 `CInstanceBase::Die` 并不负责本地计算经验;经验只来自服务端 `POINT_EXP.amount`。当前离线/事件路径仍存在 `spawn_exp_fly` 和 `_trigger_exp_gain` 两套可调用入口,需要继续审计是否有任何非权威路径把本地掉落/经验反馈当成服务端结果。
|
||||
- 当前掉落归属检查使用名称匹配,参考端由服务端 owner/name 裁决且 `CPythonItem` 的创建、删除和拾取响应受网络包序约束;名字为空、重名账号、先拾取后 DEL、重连 catch-up 和 owner 更新的结果还没有包级记录。
|
||||
- 当前死亡尸体使用固定 2.5 秒后 fade,参考端由 `CInstanceBase`/CharacterManager 的删除与 fade 生命周期驱动;服务端提前 `GC_CHARACTER_DEL`、重新 `GC_CHARACTER_ADD`、死亡后地图切换时,旧 tween、目标效果、经验球和动作状态的统一清理还未完全证明。
|
||||
|
||||
## Equivalence matrix
|
||||
|
||||
| 项目 | 状态 | 备注 |
|
||||
|---|---|---|
|
||||
| Preconditions | PARTIAL | VID、目标、owner、点数索引和服务端权威增量已有检查;主角死亡门、起止 actor 和重连包序不完整 |
|
||||
| Branch structure | PARTIAL | DAMAGE/DEAD/POINT_EXP/FLY_EXP/ground add-del 主分支已映射;POINT_* 专门刷新、主角 GameOver、重复死亡和归属边界未闭合 |
|
||||
| Algorithms/formulas | PARTIAL | EXP amount 优先、绝对值 fallback、经验球追踪和拾取距离已对齐;死亡门、point effect 和 owner 裁决仍不同 |
|
||||
| State transition order | PARTIAL | 网络包→EntityStore→signals→NetWorld/HUD 主顺序已对齐;主角死亡→停止输入/清自动攻击→复活恢复未统一 |
|
||||
| Constants/units | PARTIAL | `POINT_EXP.amount`、经验球数量/速度和拾取范围已有实现;死亡 fade、动画持续时间和完整 point effect 常量未闭合 |
|
||||
| Timing/event sources | PARTIAL | `GC_CREATE_FLY` 与 `POINT_EXP` 分离、points dirty 单次消费已测;真实攻击命中窗→死亡→经验→掉落包序仍缺记录 |
|
||||
| Resource/data sources | PARTIAL | 伤害/死亡/经验球/掉落基础资源可用;完整死亡动作、point effect、掉落模型和 item/mob 失败回退未全等价 |
|
||||
| Protocol side effects | PARTIAL | dead/point/fly/ground packets 已解析;主角 GameOver、自动攻击清除、point effect 和拾取结果副作用未全闭合 |
|
||||
| Interruption/failure/cleanup | PARTIAL | 基本尸体 fade、删除和地图 reset 已有;重复死亡、MISS/无效目标、重连、复活、跨图和待飞行物清理仍需矩阵 |
|
||||
|
||||
## Implementation fix round 2026-09-21
|
||||
|
||||
- `NetPlay::_on_entity_dead` 现在区分目标死亡和主角死亡;主角死亡会清理自动攻击、预约动作、技能/钓鱼状态,锁住 `PlayerController` 的点地与 WASD 输入,并阻止网络状态处理。
|
||||
- `PlayerController.set_dead` 增加死亡/复活状态门;正 HP `vitals_changed` 会恢复主角控制。`EntityStore` 与 `M2Client.dead_seen` 同时修复正 HP 清除死亡边沿,避免复活后下一次死亡信号丢失。
|
||||
- `test_combat_death_pickup_entry_parity.gd`、`netplay_test.gd`、`net_entity_test.cpp` 覆盖死亡、移动清理、复活恢复和再次死亡所需的状态边沿;本轮未宣称 POINT_* 特效、真实服务端包序和跨图清理全部等价。
|
||||
|
||||
本合同继续为 `PARTIAL`:经验/掉落权威链、完整死亡动画/复活 UI、真实服务器包序和异常清理仍需后续审计。
|
||||
|
||||
## Regression evidence
|
||||
|
||||
- `extension/tests/net_state_queue_test`:`POINT_EXP.amount` 累计和一次性消费、死亡状态和网络状态队列。
|
||||
- `project/mob_death_drop_test.gd`:怪物死亡目标/HP/名字/影子清理、掉落坐标贴地、owner/拾取/删除。
|
||||
- `project/combat_parity_test.gd`:防御球、击退阻挡、攻击锁、伤害表现和死亡抗击退相关分支。
|
||||
- `project/playable_combat_test.gd`:无目标、超距、死亡目标/自身、冷却和断线时不发送错误战斗请求。
|
||||
- `project/gamescene_test.gd`:地图 reset 时待发攻击/飞行和 NetPlay 状态清理。
|
||||
|
||||
这些测试证明当前端的若干主链可工作,但没有真实 40250 服务器的同一场战斗包序、主角死亡后输入门、POINT_* effect 和重复/乱序事件记录,因此合同保持 `PARTIAL`。
|
||||
|
||||
## Status
|
||||
|
||||
`PARTIAL`。经验增量丢失、掉落归属 fixture 和双重经验球等已确认问题已有修复;死亡/经验/掉落主链可运行,但主角死亡状态门、完整 point effect、服务端包序、重复/乱序/复活清理仍未达到 40250 的统一实现。
|
||||
|
||||
## Audit round 2026-09-21T03:23Z — ground-pickup ownership and distance
|
||||
|
||||
本轮继续沿 40250 `CPythonPlayer::PickCloseItem` / `SendClickItemPacket` /
|
||||
`__OnPressItem`、`CPythonItem::GetCloseItem/SetOwnership` 与当前
|
||||
`GroundItems::try_pickup/try_pickup_vid`、`NetPlay::pick_ground_item`、
|
||||
`EntityStore::mut_ground_owner` 对照,确认已有经验/死亡主链没有回归,但掉落拾取仍有三处实现差异:
|
||||
|
||||
- 40250 的所有权预检不是“只允许同名”。`SendClickItemPacket` 对非本人 owner
|
||||
还会检查 `IsPartyMemberByName` 以及物品 `ITEM_ANTIFLAG_DROP | ITEM_ANTIFLAG_GIVE`;
|
||||
当前 `GroundItems` 只比较 owner 字符串和本地角色名,队伍成员掉落与 anti-flag
|
||||
分支没有实现,因此会把 40250 允许请求的队伍物品提前拦截在客户端。
|
||||
- 40250 `GetCloseItem` 使用 `DISTANCE_APPROX` 的厘米距离,并在该版本函数体内固定以
|
||||
300cm 判定;当前 `try_pickup` 使用 Godot 欧氏米制距离,且骑马时放宽到 5m。
|
||||
两者在对角线边界、骑马拾取和同距离物品选择上可能选出不同 VID。
|
||||
- 40250 `__OnPressItem` 在点击路径先用 `GetGroundItemPosition` 和
|
||||
`NEW_IsClickableDistanceDestPixelPosition` 判断,超距才建立预约;当前
|
||||
`NetPlay::pick_ground_item` 有同类主路径,但公开的 `try_pickup_vid` 自身不再检查
|
||||
距离,后续若被其它输入入口直接调用会绕过这个边界。
|
||||
|
||||
对照结果:`CPythonItem::SetOwnership` 和当前 `mut_ground_owner` 对未知 VID 都只忽略,
|
||||
`GC_ITEM_GROUND_ADD/DEL` 的创建/删除主顺序一致;本轮 `test_combat_death_pickup_entry_parity.gd`、
|
||||
`mob_death_drop_test.gd`、`combat_parity_test.gd`、`playable_combat_test.gd`、
|
||||
`gamescene_test.gd` 与 `net_state_queue_test` 全部通过,但现有 fixture 只覆盖本人掉落,
|
||||
没有覆盖队伍 owner、anti-flag、厘米近似距离、同距 tie-break、先拾取后 DEL 和真实服务端
|
||||
攻击→死亡→经验→掉落包序。因此合同继续为 `PARTIAL`,下一轮修复候选是补齐 party/anti-flag
|
||||
上下文并把拾取距离/选择规则收敛到 40250 的实际函数语义。
|
||||
|
||||
## Implementation fix round 2026-09-21T04:10Z — ground-pickup parity
|
||||
|
||||
本轮先在 `project/test_ground_pickup_parity.gd` 中加入回归断言,修复前实际复现
|
||||
6 项失败:队伍成员物品被错误拒绝、对角线 `DISTANCE_APPROX` 边界被欧氏距离拒绝、
|
||||
同距物品没有按最高 VID 选择,以及 `try_pickup_vid` 可绕过点击距离门。随后按 40250
|
||||
实际调用链完成修复:
|
||||
|
||||
- `GroundItems::_on_added` 保存掉落包或 item proto 的 `anti_flags`;
|
||||
`_can_pick_owned_item` 与 `CPythonPlayer::SendClickItemPacket` 一致,自己的 owner
|
||||
直接允许,队伍成员仅在没有 `ITEM_ANTIFLAG_DROP | ITEM_ANTIFLAG_GIVE` 时允许,
|
||||
其他 owner 仍触发 `cannot_pick_item`。
|
||||
- `GroundItems::try_pickup` 改为厘米整数 `DISTANCE_APPROX`,固定使用
|
||||
`CPythonItem::GetCloseItem` 实际函数体的 300cm 边界;按升序 VID 遍历并用 `<=`
|
||||
复刻同距时后者(最高 VID)覆盖前者。骑马传入的 500cm 参数不再错误放宽实际
|
||||
`GetCloseItem` 的 300cm 硬边界。
|
||||
- `GroundItems::try_pickup_vid` 增加 `NEW_IsClickableDistanceDestPixelPosition`
|
||||
对应的 150cm 自身距离门;`NetPlay::CLICK_ITEM_PICKUP_CM` 和预约到达边界同步为
|
||||
150cm,并在边界使用 `<=`。
|
||||
|
||||
修复后通过:`test_ground_pickup_parity.gd`、`netplay_test.gd`、
|
||||
`mob_death_drop_test.gd`、`test_combat_death_pickup_entry_parity.gd`。本轮仍未宣称
|
||||
真实服务端攻击→死亡→经验→掉落包序、空 owner/重名、拾取竞争和重连 catch-up 已完全
|
||||
等价,合同继续为 `PARTIAL`。
|
||||
|
||||
## 深审轮次(2026-09-20)
|
||||
|
||||
本轮重新沿 `GC_DEAD -> entity_dead -> NetWorld/NetPlay/PlayerController` 和
|
||||
`GC_PLAYER_POINT_CHANGE -> POINT_EXP.amount -> _trigger_exp_gain` 跟踪正式连接:
|
||||
|
||||
- `EntityStore` 已把 `POINT_EXP.amount` 累加为一次性 `exp_gain`,`NetPlay._on_points`
|
||||
优先消费该增量;因此“服务端确实下发 POINT_EXP 但跨等级回零后没有经验反馈”的旧问题已有防回归证据。
|
||||
- `GC_CREATE_FLY(FLY_EXP)` 仍只触发 `NetWorld._on_fly` 的视觉球,`_trigger_exp_gain` 不再生成第二批球,权威经验和表现来源没有混用。
|
||||
- 当前 `client.entity_dead` 在 `NetWorld` 中只消费 `_by_vid` 的实体;本地主角由
|
||||
`GameScene` 以 `_local_node` 单独持有,`NetPlay` 的 `entity_dead` 回调只在死亡目标等于当前目标时清目标,未统一调用
|
||||
`PlayerController.stop`、清自动攻击/预约动作、死亡 UI/GameOver 和复活恢复。该缺口仍是正式链路差异,不是测试 fixture 问题。
|
||||
- `RecvPointChange` 的参考端对所有 `POINT_*` 先执行 `ShowPointEffect`,再按等级、属性、技能、能量和金币分支刷新;当前 `_on_points` 主要更新 HUD/经验/等级,完整 point effect 与金币 amount 的专门副作用仍未闭合。
|
||||
|
||||
本轮执行 `test_combat_death_pickup_entry_parity.gd`、`mob_death_drop_test.gd`、
|
||||
`combat_parity_test.gd`、`playable_combat_test.gd`、`gamescene_test.gd` 和
|
||||
`build/extension/net_state_queue_test` 均通过;这些测试证明已有局部链路可工作,但没有把主角 `GC_DEAD` 门禁、
|
||||
真实服务端包序和完整 `POINT_*` 分支纳入断言,因此合同继续保持 `PARTIAL`。
|
||||
|
||||
## Deep audit round 2026-09-20
|
||||
|
||||
本轮重新从 40250 `RecvDeadPacket`、`CInstanceBase::Die`、`RecvPointChange` 和 `RecvCreateFlyPacket` 对到当前 parser/store/UI 消费链:
|
||||
|
||||
- 40250 主角 `GC_DEAD` 会先触发非决斗 `OnGameOver`、`NotifyDeadMainCharacter` 清自动攻击,再进入一次性 `Die`;当前 `entity_dead` 主要由 `NetWorld` 处理远端节点,主角由 `GameScene.player` 单独持有,尚未有统一入口把死亡同时传给 `PlayerController.stop`、NetPlay 自动攻击/预约清理、死亡 UI 和复活恢复。
|
||||
- 当前 `POINT_EXP.amount` 的累计与一次性消费仍然正确,`GC_CREATE_FLY(FLY_EXP)` 只做视觉,不会再造第二份经验;但 40250 对所有 `POINT_*` 先 `ShowPointEffect`,并对等级/属性/技能/能量/金币有专门刷新,当前 `_on_points` 对普通 point effect、金币 amount 和属性/技能窗口刷新仍不完整。
|
||||
- 当前地面掉落和 owner/拾取主路径通过了本轮回归,但名字匹配、重名/空 owner、先拾取后 DEL、重连 catch-up、重复死亡/重新 Add、复活和跨图时序仍没有真实包级证据。
|
||||
- 本轮 `test_combat_death_pickup_entry_parity.gd`、`mob_death_drop_test.gd`、`combat_parity_test.gd`、`playable_combat_test.gd`、`gamescene_test.gd` 和 `net_state_queue_test` 均退出码 0;通过项覆盖局部战斗门、死亡怪物、掉落、经验增量和 map reset,不覆盖主角 `GC_DEAD` 门禁、完整 `POINT_*` 副作用及服务端攻击→死亡→经验→掉落包序。
|
||||
|
||||
结论:经验增量和基础掉落/死亡表现已可回归,但主角死亡状态门、完整 point effect、重复/乱序/复活清理和真实服务器包序仍与 40250 不同,合同保持 `PARTIAL`。
|
||||
|
||||
## Audit round 2026-09-20T19:03Z
|
||||
|
||||
本轮重新核对 `RecvDeadPacket`、`CInstanceBase::Die`、`RecvPointChange`、
|
||||
`RecvCreateFlyPacket` 与当前 parser/store/bridge 的完整边界,并重跑死亡、经验和掉落回归:
|
||||
|
||||
- 上一轮补的主角死亡门现在已形成可验证闭环:`NetPlay::_on_entity_dead` 清除自动攻击、
|
||||
预约动作、技能/钓鱼状态并调用 `PlayerController.set_dead(true)`;正 HP 的 `vitals_changed`
|
||||
和 `GC_MAIN_CHARACTER` 清除死亡状态。相关测试全部通过,原先“死亡后仍继续 WASD/点地”
|
||||
的差异不再列为当前未修复项。
|
||||
- `POINT_EXP.amount` 仍是唯一经验增量来源,`GC_CREATE_FLY(FLY_EXP)` 只创建视觉飞行物;
|
||||
`_trigger_exp_gain` 不再重复生成经验球,经验增量消费测试通过。
|
||||
- 当前端仍没有 40250 `ShowPointEffect(type, vid)` 的 typed event。所有点数最终合并进
|
||||
`points_changed`,没有按 `POINT_STAT_RESET_COUNT`、四维属性、技能/坐骑技能、能量、金币
|
||||
等分支保留“先显示点效果、再刷新对应窗口”的事件来源;`POINT_GOLD.amount` 也没有
|
||||
等价的 `OnPickMoney(amount)` 信号。
|
||||
- `fly_cue` 的主角 end VID 适配和缺失 actor 丢弃已有明确分支,但当前 `NetWorld` 仍把
|
||||
服务端 FLY_EXP 组合成固定三颗 Godot 光球,真实 `.fly`/索引飞行资源、地图切换中的飞行
|
||||
队列清理以及 start/end actor 包序没有 live 服务端证据。
|
||||
- 地面物品、owner 和拾取距离测试通过,但空 owner/重名、先 PICKUP 后 DEL、重复 DEAD/ADD、
|
||||
重连 catch-up 和跨图清理仍未由同一套包序 fixture 证明;这些仍属于服务端权威边界,
|
||||
当前本地 helper 不能替代协议证据。
|
||||
|
||||
本轮执行 `test_combat_death_pickup_entry_parity.gd`、`mob_death_drop_test.gd`、
|
||||
`combat_parity_test.gd`、`playable_combat_test.gd`、`gamescene_test.gd`、
|
||||
`net_state_queue_test` 和 `net_entity_test`,均退出码 0;合同继续为 `PARTIAL`。
|
||||
|
||||
## Implementation fix round 2026-09-20T19:17Z
|
||||
|
||||
本轮按 40250 `RecvPointChange` → `CPythonCharacterManager::ShowPointEffect` →
|
||||
`CPythonPlayer::SetStatus` / 窗口刷新分支修复点数事件链:
|
||||
|
||||
- `EntityStore::mut_set_point` 统一 classic 与 m2dev 的单点变更入口,先保留
|
||||
`{vid, type, value, amount}` typed event,再更新主角点表、实体生命/等级和经验增量;
|
||||
`POINT_ENERGY == 0` 同步清除 `POINT_ENERGY_END_TIME`。
|
||||
- `M2Client` 新增 `point_changed`,两种协议镜像都在 `vitals_changed` /
|
||||
`points_changed` 前发出,保留 40250 的“先 point effect、后状态刷新”顺序;新增
|
||||
`POINT_LEVEL_STEP`、`POINT_STAT_RESET_COUNT` 及四维属性常量。
|
||||
- `NetWorld` 对 `POINT_LEVEL` / `POINT_LEVEL_STEP` 按 40250 注册的
|
||||
`level_up.mse` / `skillup_1.mse` 资源在对应 VID 上播放,并在远端等级变更后刷新
|
||||
`LevelTag`;`NetPlay` 对金币正 amount 发出 `money_picked` / HUD `on_pick_money`,
|
||||
并发出属性、技能窗口刷新请求;`SkillUI` 监听点数事件。
|
||||
- `net_entity_test.cpp`、`netplay_test.gd` 覆盖 typed event 顺序、EXP/GOLD amount、
|
||||
零能量清理、金币反馈和属性/技能刷新;同时修复 NetWorld 在未显式分离本地节点的
|
||||
headless/adaptor 场景错误跳过主实体的问题。
|
||||
|
||||
本轮仍不宣称完全等价:`LevelUp`/`SkillUp` 的真实 C++ effect manager 生命周期、
|
||||
金币浮字的完整 UI 资源、状态窗口所有旁路调用,以及真实服务器攻击→死亡→经验→掉落
|
||||
包序和跨图异常清理仍需继续审计。
|
||||
|
||||
本合同继续为 `PARTIAL`。
|
||||
|
||||
## Audit round 2026-09-20T21:20Z
|
||||
|
||||
本轮对上一轮修复后的死亡/经验/掉落链做回归复核,并重新对照
|
||||
`RecvDeadPacket -> CInstanceBase::Die -> NotifyCharacterDead/NotifyDeadMainCharacter`、
|
||||
`RecvPointChange -> POINT_EXP.amount`、`RecvCreateFlyPacket` 和地面物品包:
|
||||
|
||||
- 主角死亡的 `PlayerController` 输入门、自动攻击清理、复活正 HP 边沿、远端死亡目标清理和经验增量一次性消费均有测试证据;本轮未发现上一轮已修复分支回归。
|
||||
- `POINT_EXP.amount` 仍是经验的权威增量,`FLY_EXP` 只负责视觉;死亡、掉落坐标/贴地、owner 文本/拒绝、拾取距离和删除主路径回归通过。
|
||||
- 仍不等价的分支保持明确:真实服务端攻击→伤害→死亡→`POINT_EXP`→`CREATE_FLY`→地面掉落包序未 fixture 化;重复/乱序死亡、无效 VID、重连 catch-up、重名/空 owner 和拾取竞争未闭合。
|
||||
- 40250 的 `CInstanceBase::Die` 会清 affect、卸马鞍、取消选中/目标并由底层一次性死亡门驱动动作;当前这些动作分散在 NetWorld/NetPlay/UI,完整死亡动画、特效管理器生命周期和跨图旧队列清理仍没有同一入口证明等价。
|
||||
|
||||
结论:本轮是回归审计,没有新增实现修改;已有死亡和经验主链保持可用,但合同继续为 `PARTIAL`,下一轮优先补协议级死亡包序和重复/乱序/复活清理 fixture。
|
||||
|
||||
## Implementation fix round 2026-09-21T04:00Z — local main fly target fallback
|
||||
|
||||
沿 40250 `RecvCreateFlyPacket` 的起点/终点实例解析继续核对发现:参考端从
|
||||
`CPythonCharacterManager` 取得 `CInstanceBase`,本地主角虽然不在远端 actor 容器中,
|
||||
仍由 `GetMainInstancePtr()` 提供位置。当前端 `GameScene` 将本地主角单独放在
|
||||
`NetWorld._local_node`,但 `_entity_pos()` 原先只查 EntityStore 与 `_by_vid`;在阶段切换
|
||||
的短窗口内若本地主角 EntityStore 行尚未镜像,`_on_fly(FLY_EXP, mob_vid, main_vid)` 会因
|
||||
终点为 `Vector3.INF` 直接丢弃经验球。
|
||||
|
||||
修复前先让 `test_exp_fly_parity.gd` 删除本地 VID 的 EntityStore fixture,专项实际复现
|
||||
“经验球数量为 0”的失败。现在 `_entity_pos()` 对本地 VID 优先回退到 `_local_node`,
|
||||
`_on_fly()` 对本地主角作为起点也使用同一实例来源;因此 EntityStore 行暂缺时仍可按
|
||||
40250 的主角实例语义创建和追踪飞行物。
|
||||
|
||||
修复后通过:`test_exp_fly_parity.gd`、`fly_reset_test.gd`、`netbridge_test.gd`、
|
||||
`test_combat_death_pickup_entry_parity.gd`、`mob_death_drop_test.gd`、
|
||||
`gamescene_test.gd`。测试中 `test_exp_fly_parity.gd` 仍有 3 个 ObjectDB leak warning,
|
||||
不影响退出码和本轮断言结果,需单独做生命周期清理审计。
|
||||
|
||||
合同继续为 `PARTIAL`:真实服务器攻击→死亡→经验→掉落包序、重复/乱序/重连/拾取竞争、
|
||||
完整死亡资源生命周期和跨图清理仍未由 live packet fixture 证明。
|
||||
|
||||
## Implementation fix round 2026-09-21T04:02Z — indexed fly actor-instance gate
|
||||
|
||||
继续核对 40250 `RecvCreateFlyPacket`:参考端在 `CreateIndexedFly` 前必须同时取得
|
||||
`GetInstancePtr(startVID)` 和 `GetInstancePtr(endVID)`,任一实例不存在就直接返回,
|
||||
不会用网络层残留坐标制造一个脱离角色实例的飞行物。
|
||||
|
||||
修复前回归新增两项失败:远端终点节点不存在但 EntityStore 有坐标时错误生成飞行物;
|
||||
远端起点节点不存在但 EntityStore 有坐标时同样错误生成飞行物。当前
|
||||
`NetWorld._on_fly()` 对起点和非零终点增加实例存在门;本地主角保留上一轮的
|
||||
`_local_node` 适配,因此只在本地主角实例真实存在时允许创建。
|
||||
|
||||
修复后 `test_exp_fly_parity.gd`、`fly_reset_test.gd`、`netbridge_test.gd`、
|
||||
`test_combat_death_pickup_entry_parity.gd`、`mob_death_drop_test.gd`、
|
||||
`gamescene_test.gd` 和 `combat_fx_test.gd` 全部通过。合同仍为 `PARTIAL`,真实服务端
|
||||
攻击→死亡→经验→掉落包序、重复/乱序/重连/拾取竞争和跨图清理仍未闭合。
|
||||
@@ -0,0 +1,172 @@
|
||||
# combat.flying_projectile —— 飞行物、目标、地形碰撞、命中和回收
|
||||
|
||||
状态:`PARTIAL`
|
||||
|
||||
本合同覆盖 40250 客户端的两条飞行物来源:
|
||||
|
||||
- 普攻/弓箭的 `.msa FLY` 事件 → `OnSetFlyTarget` / `OnShoot` → `CG_FLY_TARGETING`、`CG_SHOOT`。
|
||||
- 服务端 `GC_CREATE_FLY`、`GC_FLY_TARGETING`、`GC_ADD_FLY_TARGETING` → 客户端 `CFlyingManager` 弹道、目标追踪、背景/Actor 碰撞和爆炸回调。
|
||||
|
||||
## 40250 参考调用链
|
||||
|
||||
### 客户端发射事件
|
||||
|
||||
- `GameLib/ActorInstanceFly.cpp` `OnGetFlyTargetPosition`
|
||||
- 从目标 Actor 的飞行目标包围球读取位置。
|
||||
- `UserInterface/PythonPlayerEventHandler.cpp:232` `CNormalBowAttack_FlyEventHandler_AutoClear::OnSetFlyTarget`
|
||||
- 读取主角位置/朝向并发送 `SendFlyTargetingPacket(target VID, target position)`。
|
||||
- `UserInterface/PythonPlayerEventHandler.cpp:241` `OnShoot`
|
||||
- 在动作资源的 FLY 事件帧发送 `SendShootPacket(skill)`。
|
||||
- `UserInterface/PythonPlayerEventHandler.cpp:259` `OnExplodingAtAnotherTarget`
|
||||
- 若启用对应处理,向另一 Actor 发送攻击包并调用目标 `OnShootDamage`;当前参考实现该函数主体被直接 `return`,必须保留这个可达性事实,不能凭注释臆造伤害链。
|
||||
|
||||
### 飞行数据和弹道
|
||||
|
||||
- `GameLib/FlyingData.cpp` `CFlyingData::LoadScriptFile`
|
||||
- `.fly` 资源读取 initial velocity、cone/roll、angular velocity、gravity、range、acceleration、homing、bomb effect、hit-on-background、hit-on-another-monster、pierce count、bomb range 和 attach data。
|
||||
- `GameLib/FlyingInstance.cpp` `CFlyingInstance::Create`
|
||||
- 绑定 `CFlyTarget`、`canAttack`、起点、旋转方向、局部初速/加速度和剩余射程。
|
||||
- `GameLib/FlyingInstance.cpp:347` `CFlyingInstance::Update`
|
||||
- homing → 速度/加速度推进 → 维护 attach effect → 超程 → 目标线段命中 → 穿透/爆炸 → 其它 Actor 动态碰撞 → 地形高度/静态背景碰撞。
|
||||
- `GameLib/FlyTarget.cpp` `CFlyTarget`
|
||||
- 目标可以是对象或坐标;对象被删除时转为最后目标位置,避免悬空指针。
|
||||
- `GameLib/FlyingObjectManager.cpp`
|
||||
- Update 返回 false 后从实例表删除;地图/场景销毁时删除全部实例和挂接特效。
|
||||
|
||||
### 网络收包
|
||||
|
||||
- `UserInterface/PythonNetworkStreamPhaseGame.cpp:2559` `RecvAddFlyTargetingPacket`
|
||||
- 找不到射手时只记录并返回;存在目标 VID 时绑定对象,否则通过地图高度构造坐标目标。
|
||||
- `:2599` `RecvFlyTargetingPacket`
|
||||
- 与上面相同,但替换当前目标而不是追加。
|
||||
- `:2697` `RecvCreateFlyPacket`
|
||||
- 起点或终点实例不存在时不创建;否则 `CreateIndexedFly(type, start, end)`。
|
||||
- `:2637-2695` `SendShootPacket`、`SendAddFlyTargetingPacket`、`SendFlyTargetingPacket`
|
||||
- 每个发送操作后都发送 sequence。
|
||||
|
||||
## 当前端实现映射
|
||||
|
||||
- `project/fly_object.gd`
|
||||
- `FlyData`、`FlyInstance`、`FlyManager` 对应 `CFlyingData/CFlyingInstance/CFlyingManager`。
|
||||
- 已实现 `.msf` 的 `CFlyingData::LoadScriptFile` 字段解析和 cm→m 单位适配,并保留目标对象/坐标、homing、线段到点命中、超程、地形高度/静态 Physics layer 2 碰撞、穿透计数、爆点和地图清理。
|
||||
- `project/net_world.gd`
|
||||
- `_on_fly` 按 `playersettingmodule.py` 的索引表加载 `.msf` 并消费服务端飞行 cue;`_on_fly_targeting` 暂存 shooter 的目标队列;`clear_for_map_change` 清理飞行物和爆点。
|
||||
- 服务端索引经验球通过 `FLY_EXP` 加载 `ga_piece_yellow_small2.msf`;离线 `spawn_exp_fly` 仍是独立的多球测试/表现辅助入口。
|
||||
- `project/net_play.gd`
|
||||
- `_emit_swing` 在弓箭起手时调用 `_send_fly_target`,并由 `bow_shot_fired` 进入发射链。
|
||||
- `send_fly_targeting` 处理扇形/圆形目标追加。
|
||||
- `project/world_shot.gd`、`project/fx/skill_fx.gd`
|
||||
- 提供世界投射物/技能表现层。
|
||||
- `extension/src/net/*`
|
||||
- 当前 classic parser/store/client 提供飞行事件和目标坐标的网络入口。
|
||||
|
||||
## 分支等价矩阵
|
||||
|
||||
| 参考判据/副作用 | 当前实现 | 结论 |
|
||||
|---|---|---|
|
||||
| `.fly`/`.msf` 资源读取飞行参数和 attach data | `FlyData.load_msf` 解析参考字段、相对/绝对资源路径和 cm→m 单位;`FlyInstance` 已创建 AttachFile/FlyTrace,`NetWorld._indexed_fly_data` 绑定 40250 注册资源;远端 Actor 的 MSA FLY 也进入同一实例入口 | 部分等价;精确远端 skill 上下文、相机尾迹几何和所有资源矩阵仍缺 |
|
||||
| 起点/终点实例缺失则不创建飞行物 | `_on_fly` 用 `Vector3.INF`/节点存在门控后返回 | 基本等价;完整 packet handler/sequence 仍未 live 验证 |
|
||||
| 对象目标随 Actor 移动,目标删除后转位置 | `FlyTargetAnchor.fly_target_position` 使用包围球中心;`notify_target_despawn` 在渐隐/替换前立即转为位置目标 | 基本等价;迟到目标包和 VID 重用的包级矩阵仍未完成 |
|
||||
| homing、速度/加速度、射程和线段到点命中 | `FlyInstance._adjust_homing`/`update` | 基本等价;坐标轴和单位转换需真实资源验证 |
|
||||
| `m_fCollisionSphereRadius = max(move distance*2, configured radius)` | `FlyInstance.update` 在同一顺序计算膨胀半径,并通过 `actor_provider` 查询当前可见 Actor | 基本等价;Actor 碰撞球半径的真实模型标定仍未完全证明 |
|
||||
| `hitonanothermonster` + 已命中对象集合 + `OnExplodingAtAnotherTarget` | `FlyInstance._find_another_actor`、`hitted_objects`、`at_another` 回调和穿透分支已接入;射手被排除 | 部分等价;EffectManager/真实动态 culling 仍未完全复现 |
|
||||
| hit-on-background 先地形高度后静态背景碰撞 | 当前动态 Actor 分支后仍是 `sample_height` 后 Physics layer 2 ray | 基本等价的 Godot 平台适配;真实 culling 半径/查询范围未证明相同 |
|
||||
| pierce count:命中后 bomb,减计数,耗尽才 explode | `FlyInstance.pierce`/`target_hitted` 与动态 `at_another` 分支均执行相同的 bomb/继续/爆炸顺序 | 基本等价;多种真实资源与同帧背景竞争仍未完整验证 |
|
||||
| `OnShootDamage` 仅在 `canAttack` 对象目标命中时触发 | 当前 `shoot_damage` signal 有入口,但服务端 GC 弹道使用 `can_attack=false` | 基本等价;真实本地弓箭事件到服务端回包链未完整证明 |
|
||||
| 超程 `OnExplodingOutOfRange`、背景/目标回调和删除 | `exploded` signal + manager 删除;BombEffect 优先走 EffectRegistry,资源不可用才 fallback flash | 基本等价;EffectManager 真实生命周期和全部回调包序仍未完整验证 |
|
||||
| Add/Set FlyTargeting 追加与替换 | 入站通过 shooter meta `fly_target_queue`、`append`;出站普通弓箭走独立 `fly_targeting`/`CG_FLY_TARGETING`,附加目标走 `add_fly_targeting`/`CG_ADD_FLY_TARGETING` | 基本等价;shooter 不可见/本地主角/VID 重用分支待验证 |
|
||||
| `GC_CREATE_FLY` indexed normal/firecracker/auto-fire 类型 | `INDEXED_FLY_TYPES` 与 40250 注册表一致;`_on_fly` 已分别复刻普通、随机位置烟花和 +100cm AUTO_FIRE 起点,AttachFile 优先作为实际视觉 | 基本等价;资源缺失 fallback、真实模型半径和包序仍未完全证明 |
|
||||
| 每个 outbound 飞行目标/射击包后发送 sequence | ClassicSession 字节级验证 `CG_FLY_TARGETING`、`CG_ADD_FLY_TARGETING`、`CG_SHOOT` 均追加正确 sequence;完整动作队列与真实 socket 包序仍未 live 验证 | 部分等价 |
|
||||
|
||||
## 已执行测试
|
||||
|
||||
- `project/fly_test.gd`:PASS;覆盖 `CFlyingManager/CFlyingInstance` 基础弹道、命中、超程和 `__CanShot`。
|
||||
- `project/fly_reset_test.gd`:PASS;覆盖地图切换时飞行物和爆点清理。
|
||||
- `project/test_exp_fly_parity.gd`:6/6 PASS;覆盖经验光球飞行和吸收。
|
||||
- `project/fly_target_bounds_test.gd`:0 failures;覆盖模型目标包围球中心和 CPU/GPU 一致性,但有 ObjectDB leak warning。
|
||||
- `project/test_click_target_effect_parity.gd`:0 failures;覆盖静态障碍碰撞和目标效果清理,但有 ObjectDB/RID 清理警告。
|
||||
- `project/combat_fx_test.gd`:PASS;覆盖命中/连击/效果主链。
|
||||
- `project/fly_data_msf_test.gd`:PASS;覆盖 `geompung.msf`、经验球和 `arrow_01.msf` 的字段、相对资源路径、attach data、枚举及 cm→m 转换。
|
||||
- `project/indexed_fly_type_test.gd`:PASS;覆盖 40250 NORMAL/FIRE_CRACKER/AUTO_FIRE 的索引类型、起点高度、对象/位置目标和随机距离范围。
|
||||
- `project/fly_dynamic_collision_test.gd`:PASS;覆盖动态 Actor 双目标穿透和同一 Actor 的 `HittedObjectSet` 去重。
|
||||
- `project/fly_target_lifecycle_test.gd`:PASS;覆盖包围球目标中心和 Actor 销毁时立即转位置目标。
|
||||
- `project/fly_attachment_effect_test.gd`:PASS;覆盖绝对 AttachFile 解析、EffectRegistry 实例、FlyTrace 轨迹、BombEffect 和资源视觉不重复绘制。
|
||||
- `project/remote_motion_fly_test.gd`:PASS;覆盖远端 Actor `GC_FLY_TARGETING` 队列、MSA FLY 事件、owner VID/skill 和资源飞行实例创建。
|
||||
|
||||
## Implementation fix round 2026-09-21T06:34Z
|
||||
|
||||
按 `FlyingObjectManager::CreateIndexedFly` 接入 `INDEXED_FLY_TYPES`:NORMAL 保持射手根位置和对象目标,FIRE_CRACKER 使用 +200cm 起点、-90° 基准方向与 20..50m/10..25m 随机位置目标,AUTO_FIRE 使用 +100cm 起点并保留对象目标。新增 `indexed_fly_type_test.gd`,修复前 FIRE_CRACKER/AUTO_FIRE 断言失败,修复后通过。
|
||||
|
||||
按 `FlyingInstance::Update` 接入动态 Actor 碰撞:每帧使用 `max(move_distance * 2, collision_sphere_radius)`,排除 owner,按节点实例身份维护 `hitted_objects`,触发 `at_another` 并复刻 pierce/bomb/explode 分支。按 `FlyTarget`/`ActorInstanceFly` 接入 `FlyTargetAnchor` 包围球中心,并在 NetWorld 的 despawn、同 VID 替换前调用 `notify_target_despawn`,保证渐隐节点不再继续移动飞行目标。新增 `fly_dynamic_collision_test.gd` 和 `fly_target_lifecycle_test.gd`,均通过。
|
||||
|
||||
修复后 `fly_test`、`fly_reset_test`、`fly_data_msf_test`、`motion_event_fly_test`、`test_exp_fly_parity`、`netbridge_test`、`combat_fx_test`、`netplay_test` 和静态建筑碰撞回归通过。真实 `EffectManager`/`FlyTrace` 资源表现、真实模型动态半径、包序和不可见 Actor 生命周期仍使合同保持 `PARTIAL`。
|
||||
|
||||
## Implementation fix round 2026-09-22T07:55Z
|
||||
|
||||
复核 `CFlyingManager::DeleteAllInstances` 的销毁边界时发现,当前 `FlyInstance.handler` 闭包捕获实例自身,而实例又持有该 Callable;地图清理只清数组会留下 `RefCounted` 环。现新增 `_release_instance()`,在地图清理和自然过期两个删除路径中同步解除 handler、target、world、data 引用,再移除实例;地图清理的 visual/flash 节点也按 Loading 的同步删除边界直接释放。
|
||||
|
||||
修复前 `fly_reset_test.gd` 虽 PASS 但退出报告 5 个 ObjectDB 泄漏;修复后退出无 ObjectDB 泄漏。`test_exp_fly_parity.gd` 行为断言仍为 6/6 PASS,但其旧式单函数 fixture 仍报告 3 个 ObjectDB/不在 SceneTree 的路径警告,属于测试 harness 生命周期待清理,不将其伪装成完全干净证据。动作事件 FLY 的 owner/skill/包序、真实模型半径、远端不可见 Actor 和 EffectManager 全量生命周期仍保持 `PARTIAL`。
|
||||
|
||||
## Implementation fix round 2026-09-21T06:46Z
|
||||
|
||||
继续按 40250 `FlyingData::LoadScriptFile`、`FlyTrace` 和 `FlyingObjectManager` 的资源链修复飞行特效:`TailLength` 按参考保持秒单位,`TailSize` 只做 cm→m 转换;`AttachData` 现在会实例化 `AttachFile`,并维护 line/multi-line/sine/exp 四类尾迹历史和按时间衰减的轨迹几何。`BombEffect` 优先通过 `EffectRegistry` 资源实例化,只有资源不存在时才使用兼容性 flash,避免资源特效和通用球体重复绘制。
|
||||
|
||||
同时修复 `EffectRegistry` 对 `.msf` 内绝对资源路径的解析:存在的绝对路径不再被重复拼接到资源根目录。新增 `fly_attachment_effect_test.gd` 覆盖 AttachFile 实例、FlyTrace 历史、BombEffect、资源视觉去重和地图清理;`fly_data_msf_test.gd` 增加 `TailLength` 单位回归。资源特效链已进入正式运行路径,但精确相机朝向、所有远端 FLY/SHOOT owner-skill 组合、EffectManager 的每种资源生命周期和真实包序仍未完成,因此合同继续保持 `PARTIAL`。
|
||||
|
||||
## Implementation fix round 2026-09-21T06:54Z
|
||||
|
||||
按 40250 `CActorInstance::ProcessMotionEventFly` 补齐远端 Actor 的动作事件飞行链:`NetWorld` 现在从 `GC_FLY_TARGETING/GC_ADD_FLY_TARGETING` 保存的当前目标队列取目标,在远端 MSA 的 type 6 事件帧创建同一个 `.msf` 飞行实例,使用 Actor 动作位置/挂骨骼位置作为起点,并传递 owner VID 与 skill index;对象目标、坐标目标和目标队列消费均保持分支语义。`GC_CREATE_FLY` 的服务端索引飞行仍与动作事件路径分离。
|
||||
|
||||
新增 `remote_motion_fly_test.gd` 覆盖远端目标队列、资源实例、owner/skill 和单次消费,修复后的测试无 ObjectDB 泄漏。远端动作事件没有原生 `m_kCurMotNode.uSkill` 的完整网络字段时仍只能使用事件字段或当前动作参数回退,精确 skill 上下文、包序、不可见 Actor 和全量资源矩阵继续保持 `PARTIAL`。
|
||||
|
||||
## Implementation fix round 2026-09-21T06:59Z
|
||||
|
||||
按 40250 `CNormalBowAttack_FlyEventHandler_AutoClear::OnSetFlyTarget` 与 `CPythonNetworkStream::SendFlyTargetingPacket` 修正出站主目标链:新增 `M2Client.fly_targeting`、`ClassicSession::send_fly_targeting` 和 Godot 绑定,普通弓箭不再误发 `CG_ADD_FLY_TARGETING`;扇形/圆形技能的附加目标继续使用独立的 `add_fly_targeting`。ClassicSession 回归逐字节验证两种 header 和 sequence,战斗回归验证弓起手优先调用主目标 API。
|
||||
|
||||
该修复只闭合了主/附加目标包类型和局部 sequence 证据;不可见 shooter、VID 重用、真实 socket 中乱序/重连和完整 MSA→CG_SHOOT 时序仍保持 `PARTIAL`。
|
||||
|
||||
## Implementation fix round 2026-09-21T07:03Z
|
||||
|
||||
继续核对 `FlyingInstance::UpdateAttachInstance` 后修正尾迹几何:40250 的
|
||||
`MULTI_LINE`、`SINE`、`EXP` 分支使用 `(-sin(roll) * offset, 0,
|
||||
-cos(roll) * offset)`,当前端原先把局部 Z 号写反,导致资源飞行物的尾迹/AttachFile
|
||||
偏到相反一侧;现已改为同号。`LINE` 的 effect 朝向按参考只使用基础飞行旋转,其他三种
|
||||
偏移类型才叠加 `m_qAttachRotation`。新增直接检查 `MULTI_LINE` 局部 Z 偏移的回归断言。
|
||||
|
||||
`fly_attachment_effect_test`、`fly_data_msf_test`、`fly_test`、`fly_dynamic_collision_test`、
|
||||
`fly_target_lifecycle_test`、`fly_reset_test` 均通过。`coneangle/rollangle` 初始旋转、
|
||||
相机相关 FlyTrace 顶点生成、完整资源矩阵和远端原生 skill 上下文仍未闭合,合同保持 `PARTIAL`。
|
||||
|
||||
### Active GameLib flying-core review
|
||||
|
||||
- `GameLib/FlyingData.h` 定义 `.fly` 的初速、cone/roll、角速度、重力、加速度、homing、range、bomb effect、background/another-monster 命中、pierce、collision sphere 和 attach tail;`FlyingInstance.h` 保存速度/旋转/剩余射程、目标、已命中集合和 attach effect 生命周期。
|
||||
- `FlyingObjectManager.h` 按 index/type 注册飞行资源,创建/更新/渲染/删除实例;`FlyTrace.cpp` 则用动态池记录位置时间 deque,按 tail length 衰减并生成相机相关的飞行尾迹。当前 `fly_object.gd` 已按索引读取 `.msf`,接入动态 Actor 碰撞/已命中集合、AttachFile/FlyTrace、BombEffect 资源清理和远端 MSA FLY 创建;精确相机朝向、模型半径标定、全部资源组合和远端原生 skill 上下文仍缺证据。
|
||||
- 本轮完成 `FlyTarget`/`FlyingData`/`FlyingInstance`/`FlyingObjectManager`/`FlyTrace` 的活跃源码静态核对;测试通过只证明基础 Godot 弹道,不提升合同状态。
|
||||
|
||||
## 已确认差异和未验证项
|
||||
|
||||
1. 普通 GC 飞行物已从 40250 索引注册表加载 `.msf` 建立 `FlyData`,本地主角和远端 Actor 的动作事件 type 6 都进入同一实例入口;远端原生 `m_kCurMotNode.uSkill` 上下文、动作/网络包序仍未闭合。
|
||||
2. `hitonanothermonster` 已接入动态 Actor 碰撞、按节点身份的 `HittedObjectSet` 去重、穿透后继续飞行和另一目标回调;真实客户端的 culling 范围、模型动态半径和背景/Actor 同帧竞争仍未证明一致。
|
||||
3. 正式目标命中已经使用 `FlyTargetAnchor` 包围球中心,目标销毁也会转为最后坐标;`.fly` 的 bomb range/collision sphere 与真实模型资源组合仍没有逐类标定矩阵。
|
||||
4. `_on_fly_targeting` 只把目标队列挂在可见 shooter 节点;40250 的 `CPythonCharacterManager` 实例表与当前 `NetWorld` 可见节点表不是同一生命周期,远端暂不可见、主角本地节点、VID 重用和迟到 `GC_CREATE_FLY` 尚未证明一致。
|
||||
5. `bombeffect` 已优先走 `EffectRegistry`,AttachFile/FlyTrace 也已进入飞行实例生命周期;精确相机尾迹、颜色/材质、所有资源组合和 EffectManager 的逐资源回收仍未证明一致。
|
||||
6. 已有测试覆盖 `.msf` 字段、索引类型、动态 Actor 双目标穿透/去重、目标删除、AttachFile/FlyTrace/BombEffect、主/附加目标 header+sequence 和地图清理;仍缺 Add/Set 乱序、真实服务端包序、完整资源矩阵和背景/Actor 同帧竞争。
|
||||
|
||||
## Audit round 2026-09-21T06:24Z
|
||||
|
||||
本轮沿 `FlyingObjectManager::CreateIndexedFly`、`FlyingInstance::Update`、`FlyTarget::NotifyTargetClear` 和 `CActorInstance::OnGetFlyTargetPosition` 做了源码级闭环核对,并验证了当前资源目录包含 40250 注册的 7 个 FIRE_CRACKER `.msf`、1 个 AUTO_FIRE `.msf` 以及普通索引 `.msf`。
|
||||
|
||||
- `playersettingmodule.py` 的 16 个索引资源路径已在当前 `INDEXED_FLY_FILES` 中出现,但类型元数据没有进入当前表;`NetWorld._on_fly` 所有索引都统一创建普通对象目标,未复刻 FIRE_CRACKER 的 `+200cm` 起点/随机位置目标和 AUTO_FIRE 的 `+100cm` 起点。
|
||||
- 40250 每帧按 `max(move_distance * 2, collision_sphere_radius)` 扫描动态 Actor,并用 `HittedObjectSet` 去重;当前已按同一顺序实现 provider 查询、owner 排除、去重和 `at_another` 分支,但 provider 的可见性边界与真实模型半径仍是适配层差异。
|
||||
- 40250 目标 Actor 销毁时立即 `ClearFlyTargeter()`,飞行物切换为最后目标坐标;当前 `_on_despawn` 先走渐隐/延迟释放,`FlyInstance` 仍持有旧 Node3D,目标删除、同 VID 重建和迟到 `GC_CREATE_FLY` 尚未达到同一时序。
|
||||
- 40250 对象目标位置来自 `CActorInstance::OnGetFlyTargetPosition` 的包围球中心;当前 `FlyInstance.target_position()` 读取 Node3D 根位置,`fly_target_bounds_test` 的包围球 helper 尚未接入正式飞行命中链。
|
||||
- 40250 的 `bombeffect`、attach/tail 和 FIRE_CRACKER/AUTO_FIRE 的资源表现由 `EffectManager`/`FlyTrace` 管理;当前只生成通用 SphereMesh/flash,资源路径虽能解析但尚未真正消费。
|
||||
|
||||
因此本轮没有提升合同状态;当前结论仍为 `PARTIAL`。资源消费、动态碰撞和目标生命周期已从“缺失”推进到“有运行证据的部分等价”,后续应优先补远端动作事件 FLY、包序/乱序、不可见 Actor 与完整资源生命周期矩阵。
|
||||
|
||||
## 下一轮验证入口
|
||||
|
||||
- 为动作事件 FLY 增加远端 Actor 的原生 skill 上下文和 `CG_FLY_TARGETING → CG_SHOOT → sequence` 真实状态机/包序测试。
|
||||
- 增加背景与 Actor 同帧竞争、三 Actor/多种 collision sphere 的真实可见性与半径矩阵。
|
||||
- 增加 `GC_FLY_TARGETING → GC_ADD_FLY_TARGETING → GC_CREATE_FLY` 乱序、不可见 shooter、目标删除/VID 重用和 map reset 测试。
|
||||
- 对照每个 `INDEX_FLY_TYPE` 的 AttachFile、BombEffect、尾迹材质、颜色和回收时序,补齐资源矩阵后再决定是否提升合同状态。
|
||||
@@ -0,0 +1,121 @@
|
||||
# combat.target_selection_rules —— 目标选择、可攻击判定、目标框和安全区
|
||||
|
||||
状态:`PARTIAL`
|
||||
|
||||
本合同对照 40250 Windows 客户端的三套不能合并的判据:
|
||||
|
||||
1. `CInstanceBase::IsAttackableInstance`:是否允许战斗逻辑攻击目标。
|
||||
2. `CInstanceBase::CanPickInstance` / `IsTargetableInstance`:是否允许鼠标/脚本设置目标。
|
||||
3. `CPythonNetworkStream::RecvTargetPacket` + `CanViewTargetHP`:服务端目标包到达后,目标框是否关闭、显示血量或保持。
|
||||
|
||||
## 40250 参考调用链
|
||||
|
||||
### 战斗判定
|
||||
|
||||
- `UserInterface/InstanceBase.cpp:2147` `CInstanceBase::IsAttackableInstance`
|
||||
- 主角观战模式先拒绝;自身 VID 先拒绝。
|
||||
- 石头只能被 PC 攻击。
|
||||
- PC 目标按决斗、同公会、killer、PK mode、队伍、同/异帝国、PVP/GVG、善恶冲突逐分支判断。
|
||||
- PC 可攻击敌怪、木门;敌怪可攻击 PC/建筑;变身实例按 `IsPoly` 分支判断。
|
||||
- `UserInterface/PythonPlayer.cpp:207` `CPythonPlayer::__Update_AutoAttack`
|
||||
- 自动攻击目标必须存在、未死亡、骑乘等级允许,并通过 `IsAttackableInstance`。
|
||||
- `UserInterface/PythonPlayerInput.cpp:324-370` `__OnClickActor`
|
||||
- 点击实体后先清预约;攻击目标再检查 `__CanAttack`、距离和 NPC 交互分支。
|
||||
- `UserInterface/PythonPlayerInput.cpp:336-380` `__OnPressActor`
|
||||
- 目标切换、自动攻击、弓箭射程、预约靠近和最终 `IsAttackableInstance` 的顺序固定。
|
||||
|
||||
### 目标可选中判定
|
||||
|
||||
- `UserInterface/PythonPlayerInput.cpp:112` `CPythonPlayer::SetTarget`
|
||||
- 先检查主角 `CanChangeTarget()`。
|
||||
- 已选中目标有强制/非强制切换时间窗。
|
||||
- 新目标必须通过主角 `IsTargetableInstance()`。
|
||||
- 目标成功时设置 actor targeted 状态、飞行目标和网络 `SendTargetPacket`;失败时清空本地目标。
|
||||
- `UserInterface/InstanceBase.cpp:2251` `IsTargetableInstance`
|
||||
- 直接委托目标的 `CanPickInstance()`。
|
||||
- `UserInterface/InstanceBase.cpp:2263` `CanPickInstance`
|
||||
- 视锥外拒绝。
|
||||
- 死亡门;死亡门对门对象单独处理。
|
||||
- PC 的隐身、复活隐身、普通隐身按 affect 和主角可见性拒绝。
|
||||
|
||||
### 目标框和安全区
|
||||
|
||||
- `UserInterface/PythonNetworkStreamPhaseGame.cpp:2424` `RecvTargetPacket`
|
||||
- 目标或主角实例缺失:关闭目标框。
|
||||
- 目标已死亡:保持当前目标框状态,不写入新的实例目标。
|
||||
- PC/建筑:`CloseTargetBoardIfDifferent`。
|
||||
- 石头/木门/敌怪:`SetHPTargetBoard`。
|
||||
- 其它实体:关闭目标框。
|
||||
- `UserInterface/InstanceBase.cpp:2293` `CanViewTargetHP`
|
||||
- 只有石头、木门和敌怪能显示 HP 目标框。
|
||||
- `UserInterface/InstanceBase.cpp:560` `IsInSafe`
|
||||
- 通过地图 `ATTRIBUTE_BANPK` 判定安全区;它与“能否选目标”和“目标框能否显示”是独立判据。
|
||||
- `UserInterface/PythonPlayer.cpp:496-520` `NotifyCharacterDead/Update`
|
||||
- 当前目标死亡时清目标;目标更新后不再 `CanPickInstance` 时清目标并关闭目标框。
|
||||
|
||||
## 当前端实现映射
|
||||
|
||||
- `project/entity_rules.gd`
|
||||
- `is_attackable_instance` 已覆盖观战、自身、PC/怪/石头/木门、PK mode、party、killer、duel、PVP/GVG 和 alignment 分支;`can_pick_instance` 统一镜像视锥、死亡、门死亡和三类隐身 affect 门。
|
||||
- `project/net_play.gd`
|
||||
- `_is_attackable` 拼装会话上下文并调用 `EntityRules`。
|
||||
- `_on_pick` 负责 NPC/warp 交互、战斗目标选择、预约靠近和自动攻击。
|
||||
- `_reserve_process_click_actor` 处理距离、攻击资格、死亡、安全区、弓箭和攻击顺序。
|
||||
- `_on_target_info` 镜像 `RecvTargetPacket` 的目标框分派。
|
||||
- `_change_target_to_picked_instance` 是当前统一脚本目标入口,先走 `_can_change_target`,再走重复目标时间窗和 `_is_targetable_instance`。
|
||||
- `_motion_blocks_target_change` 读取当前 `.msa` 的 WARP/FLY/EFFECT_TO_TARGET 事件,缺少动作数据时按 40250 的保守门处理。
|
||||
- `project/target_board.gd`
|
||||
- `classify`、`can_view_target_hp`、异阵营过滤、Ctrl 悄悄话和 HP 百分比边界已独立镜像。
|
||||
- `project/player_controller.gd`
|
||||
- 使用射线和可点选节点进行实体命中;射线与悬停两条路径均消费 NetWorld 写入的 `can_pick` 门。
|
||||
- `project/net_world.gd`
|
||||
- 负责目标光圈、目标/悬停名字强显和实体节点生命周期,并在实体创建/更新时同步 `can_pick`。
|
||||
- `project/alignment_pk_system.gd`
|
||||
- 提供善恶值、PK 头衔和死亡掉落辅助逻辑;这些不是 40250 客户端 `SetTarget` 的替代实现。
|
||||
|
||||
## 分支等价矩阵
|
||||
|
||||
| 参考判据/副作用 | 当前实现 | 结论 |
|
||||
|---|---|---|
|
||||
| `IsAttackableInstance` 的 PC/怪/石头/木门/PK/决斗/PVP/GVG 分支 | `EntityRules.is_attackable_instance` | 基本等价;已有 `entity_rules_test.gd` 覆盖边界 |
|
||||
| 自动攻击先检查存在、死亡、坐骑等级,再检查攻击资格 | `_on_pick`、`_reserve_process_click_actor`、自动攻击链 | 部分等价;多个调用点重复拼接门控,未由统一目标状态对象约束 |
|
||||
| `SetTarget` 先执行 `CanChangeTarget` | `_can_change_target` → `_motion_blocks_target_change`,按当前 `.msa` 的 WARP/FLY/EFFECT_TO_TARGET 事件门控 | 已补齐静态/可读取动作事件路径;真实完整动作资源仍待 live 验证 |
|
||||
| `SetTarget` 必须通过 `IsTargetableInstance`/`CanPickInstance` | `EntityRules.can_pick_instance` + NetWorld `can_pick` 元数据,脚本目标、射线点选和悬停共用 | 已补齐视锥字段、死亡、门死亡、EUNHYEONG/复活隐身/普通隐身门;真实视锥仍待 live 验证 |
|
||||
| NPC/warp 不可攻击但可在距离内交互 | `_on_pick` 分流到 `_send_click_actor_packet` | 基本等价;真实包拒绝/冷却仍缺 live 证据 |
|
||||
| `RecvTargetPacket` 的 PC/building/HP/CLOSE/NONE 分支 | `TargetBoard.classify` + `_on_target_info` | 静态分支基本等价;缺少目标死亡/更新/删除乱序时序测试 |
|
||||
| `NotifyCharacterDead/Update` 清目标并关闭目标框 | `_on_entity_dead` / `_on_entity_gone` 统一 `CG_TARGET(0)` 清理;NetWorld 更新同步 `can_pick` | 死亡/删除已补齐;隐身、出视野和 stale 包时序仍待完整生命周期测试 |
|
||||
| `IsInSafe` 地图属性判定 | 使用实体快照 `in_safe` | 数据来源不等价;当前端未证明由地图 `ATTRIBUTE_BANPK` 实时计算并在移动/换图时更新 |
|
||||
| PC 异阵营目标框开关和 Ctrl 悄悄话 | `TargetBoard` + `net_play` | helper 分支通过,真实 UI/输入时序仍需 live 验证 |
|
||||
|
||||
## 已执行测试
|
||||
|
||||
- `project/entity_rules_test.gd`:PASS,覆盖攻击资格判定矩阵。
|
||||
- `project/target_board_test.gd`:PASS,覆盖目标框分派、HP 可见性、异阵营过滤和 Ctrl 悄悄话。
|
||||
- `project/test_alignment_pk_parity.gd`:35/35 PASS,覆盖善恶值和 PK 辅助逻辑。
|
||||
- `project/test_click_target_effect_parity.gd`:0 failures,覆盖点击、目标光圈和飞行物地形碰撞;存在 ObjectDB/RID 清理警告。
|
||||
- `project/test_auto_combat_bot_parity.gd`:22/22 PASS,覆盖自动选目标、移动、技能/普攻和拾取;主要为 fake client/helper 路径。
|
||||
- `project/playable_combat_test.gd`:0 failures,覆盖可玩战斗主链。
|
||||
|
||||
## 本轮实现修复(2026-09-20T21:05Z)
|
||||
|
||||
- 按 `CPythonPlayer::SetTarget` 的顺序增加 `_can_change_target`、重复目标 1/2 秒时间窗和失败时的 `CG_TARGET(0)`;自动命中改走 `SetTarget(victim, FALSE)` 语义。
|
||||
- 按 `CInstanceBase::CanPickInstance` 增加统一 `EntityRules.can_pick_instance`:视锥字段、死亡/木门死亡、EUNHYEONG、复活隐身和普通隐身均在脚本目标入口检查。
|
||||
- NetWorld 在实体创建/更新时把同一结果写入 `can_pick`,PlayerController 的点击和悬停射线均消费该门,避免 UI/脚本两条路径出现不同目标。
|
||||
- `netplay_test.gd` 新增技能动作锁、四类不可选目标、非强制命中换目标窗口测试;`netplay_test`、`entity_rules_test`、`playable_adapter_test`、点击目标/NetWorld/自动战斗/可玩战斗回归均通过。
|
||||
|
||||
本合同仍为 `PARTIAL`:完整动作资源事件、隐身/裁剪/迟到更新生命周期、地图 `ATTRIBUTE_BANPK` 权威和真实服务端目标包顺序尚未闭合。
|
||||
|
||||
## 已确认差异和未验证项
|
||||
|
||||
1. 真实完整 `.msa` 的 `get_events()`、动作结束和缺失动作资源下的 `CanChangeTarget` live 时序仍需验证。
|
||||
2. 网络实体更新没有完全对应 40250 `NotifyCharacterUpdate` 的统一目标失效回调;隐身、出视野/淡出和迟到更新仍可能在不同入口留下目标框或光圈状态。
|
||||
3. 当前安全区来自实体字段 `in_safe`,尚未证明与地图 `ATTRIBUTE_BANPK` 在移动、跨图和服务端状态更新时保持同一数据权威。
|
||||
4. 当前端的 `_is_attackable` 在 helper 层提前检查 `dead`,而 40250 的 `IsAttackableInstance` 本身不检查死亡;虽然主要调用点另有死亡门,但该职责边界尚未统一。
|
||||
5. `OnTargeted/OnUntargeted`、飞行目标和 `SendTargetPacket` 的完整副作用顺序,以及真实服务端目标包拒绝/乱序,仍无 live fixture。
|
||||
|
||||
## 下一轮验证入口
|
||||
|
||||
- 增加目标生命周期测试:存活 → 隐身 → 普通死亡 → 删除/裁剪 → 迟到更新,逐步检查目标 VID、目标框、光圈、名字强显和 `CG_TARGET(0)`。
|
||||
- 增加完整 `.msa` 事件源的 `CanChangeTarget`/动作锁/重复点击时间窗测试,并对照 `_change_target_to_picked_instance` 的实际调用链。
|
||||
- 增加地图属性 `ATTRIBUTE_BANPK` 与 `in_safe` 的移动、换图、目标切换矩阵。
|
||||
- 只有以上差异修复并通过 native/classic 与场景回归后,才能将本合同从 `PARTIAL` 提升。
|
||||
@@ -0,0 +1,86 @@
|
||||
# 40250 全功能审计合同清单
|
||||
|
||||
## 目的与范围
|
||||
|
||||
本文件把 `ClientVS22/source` 中可达的客户端行为,按“外部可观察功能”而不是按源文件拆成稳定合同。每个合同都必须继续追踪完整调用链:触发条件、前置检查、分支、算法/单位、状态写入顺序、动作或资源事件、协议副作用、失败/中断/清理。
|
||||
|
||||
参考范围:
|
||||
|
||||
- `../40250/Server Client TMP4/ClientVS22/source/UserInterface`
|
||||
- `../40250/Server Client TMP4/ClientVS22/source/GameLib`
|
||||
- `../40250/Server Client TMP4/ClientVS22/source/EffectLib`
|
||||
- `../40250/Server Client TMP4/ClientVS22/source/EterLib`
|
||||
- `../40250/Server Client TMP4/ClientVS22/source/EterGrnLib`
|
||||
- `../40250/Server Client TMP4/ClientVS22/source/PRTerrainLib`
|
||||
- `../40250/Server Client TMP4/ClientVS22/source/MilesLib`
|
||||
- `../40250/Server Client TMP4/ClientVS22/source/EterPack`
|
||||
- `../40250/Server Client TMP4/ClientVS22/source/EterLocale`
|
||||
|
||||
当前范围:`project/`、`extension/`、`formats/`、`assets/`、`libgr2/` 和 `oracle/` 中会影响运行时行为的代码、资源读取和测试。第三方库、禁用代码、仅构建工具不作为独立 40250 行为合同;如果当前客户端存在没有 40250 对应物的扩展功能,则登记到 `extension.current_only_features`,不能静默算作已兼容。
|
||||
|
||||
`MAPPED` 只表示两边入口已经定位,`UNMAPPED` 表示尚未找到当前实现或参考映射,`PARTIAL` 表示已经发现差异。任何新合同都必须先进入 `audit/manifest.json`,再开始实现或验证。
|
||||
|
||||
## 合同总表
|
||||
|
||||
| ID | 功能 | 参考入口 | 当前入口 | 初始状态 |
|
||||
|---|---|---|---|---|
|
||||
| `lifecycle.bootstrap_phase_host` | 启动、阶段属主、窗口和应用生命周期 | `PythonApplication*`, `UserInterface.cpp`, `PythonApplicationProcedure.cpp` | `client_main.gd`, `app_flow.gd`, `app_lifecycle.gd`, `intro_*_phase.gd` | MAPPED |
|
||||
| `network.login.phase_flow` | 登录、握手、选人、加载、进入游戏阶段 | `PythonNetworkStreamPhaseLogin/HandShake/Select/Loading/Game.cpp` | `classic_session.cpp`, `m2_client.cpp`, `app_flow.gd`, `net_play.gd` | MAPPED |
|
||||
| `network.character_select.create_delete` | 角色列表、创建、删除、改名、选人 | `PythonNetworkStreamPhaseSelect.cpp`, `PythonNetworkStreamPhaseLoading.cpp`, `PythonCharacterManager.cpp` | `classic_parser.cpp`, `classic_session.cpp`, `char_select_screen.gd`, `intro_create_phase.gd` | MAPPED |
|
||||
| `network.packet_dispatch_protocol` | 包头、分帧、阶段分派、未知包和发送顺序 | `Packet.h`, `PythonNetworkStream.cpp`, `NetPacketHeaderMap.cpp`, `EterLib/NetStream.cpp` | `classic_stream.cpp`, `classic_parser.cpp`, `wire_classic.h`, `net_stream.cpp` | MAPPED |
|
||||
| `network.security_encoding` | 握手、序列号、加密、编码和定长字符串 | `EterBase/cipher.cpp`, `PythonNetworkStreamPhaseHandShake.cpp`, `EterLocale/StringCodec.cpp` | `secure_cipher.cpp`, `classic_cipher.cpp`, `text_codec.h`, `net_stream.cpp` | MAPPED |
|
||||
| `network.reconnect_warp` | 断线、换图、跨服重连和旧会话清理 | `PythonNetworkStreamPhaseGame.cpp`, `PythonNetworkStreamPhaseLoading.cpp`, `NetworkActorManager.cpp` | `m2_client.cpp`, `classic_session.cpp`, `game_scene.gd`, `net_play.gd` | MAPPED |
|
||||
| `network.error_timeout_server_state` | 登录错误、超时、频道/服务器状态和恢复 | `AccountConnector.cpp`, `ServerStateChecker.cpp`, `PythonNetworkStreamPhaseLogin.cpp` | `auth_client.h`, `channel_status.gd`, `serverinfo.gd`, `app_flow.gd` | MAPPED |
|
||||
| `world.entity_spawn_state_sync` | Actor 创建、追加信息、VID 重用、状态同步 | `NetworkActorManager.cpp`, `PythonNetworkStreamPhaseGameActor.cpp`, `InstanceBase.cpp` | `entity_store.cpp`, `net_world.gd`, `remote_player_view.gd`, `mob_view.gd` | MAPPED |
|
||||
| `world.map_load_transition` | 地图、区域、地形、加载阶段和场景重建 | `PythonBackground.cpp`, `MapManager.cpp`, `MapOutdoorLoad.cpp`, `AreaLoaderThread.cpp` | `metin2_world.cpp`, `game_scene.gd`, `environment_builder.cpp`, `atlas_ui.gd` | MAPPED |
|
||||
| `world.npc_drop_actor_spawn` | NPC、怪物、建筑、掉落物和招牌实体 | `PythonNonPlayer.cpp`, `PythonNetworkStreamPhaseGameActor.cpp`, `InstanceBase.cpp` | `entity_store.cpp`, `net_world.gd`, `mob_view.gd`, `ground_items.gd` | MAPPED |
|
||||
| `world.reset_cleanup` | 换图、死亡、退出和 UI/实体/特效清理 | `PythonApplicationProcedure.cpp`, `PythonEventManager.cpp`, `MapManager.cpp`, `NetworkActorManager.cpp` | `app_flow.gd`, `game_scene.gd`, `net_play.gd`, `entity_store.cpp` | MAPPED |
|
||||
| `movement.keyboard.motion` | WASD、方向键、停止、转向和移动动作 | `PythonPlayerInputKeyboard.cpp`, `PythonPlayerInput.cpp`, `InstanceBaseMovement.cpp` | `player_controller.gd`, `net_play.gd`, `net_world.gd` | MAPPED |
|
||||
| `movement.click_collision` | 点地、路径目标、地形/Actor 碰撞和阻挡恢复 | `PythonPlayerInputMouse.cpp`, `InstanceBaseMovement.cpp`, `ActorInstanceCollisionDetection.cpp`, `MapOutdoor.cpp` | `player_controller.gd`, `mouse_controller.gd`, `hit_collision.gd`, `metin2_world.cpp` | MAPPED |
|
||||
| `movement.remote_sync_state` | 远端移动命令队列、服务器时钟和状态释放 | `InstanceBaseSync.cpp`, `InstanceBase.cpp`, `PythonPlayerEventHandler.cpp` | `entity_store.cpp`, `net_play.gd`, `classic_parser.cpp` | MAPPED |
|
||||
| `movement.motion_resource_events` | `.msa`/`.msm` 动作、连击窗口、动作事件和速度 | `InstanceBaseMotion.cpp`, `ActorInstanceMotion.cpp`, `ActorInstanceMotionEvent.cpp`, `RaceMotionData.cpp` | `player_view.gd`, `remote_player_view.gd`, `mob_view.gd`, `msa_motion.gd`, `formats/msa.cpp`, `formats/msm.cpp` | MAPPED |
|
||||
| `combat.attack_combo_damage` | 普攻、连击、命中窗口、伤害和受击 | `PythonPlayer.cpp`, `InstanceBaseBattle.cpp`, `ActorInstanceBattle.cpp`, `ActorInstanceWeaponTrace.cpp` | `net_play.gd`, `player_controller.gd`, `hit_collision.gd`, `damage_effect.gd` | MAPPED |
|
||||
| `combat.target_selection_rules` | 目标选择、可攻击判定、目标框和安全区 | `PythonPlayerInputMouse.cpp`, `PythonNetworkStreamPhaseGame.cpp`, `InstanceBase.cpp` | `mouse_controller.gd`, `entity_rules.gd`, `target_board.gd`, `target_ui.gd`, `net_play.gd` | MAPPED |
|
||||
| `combat.death.exp_drop` | 死亡、经验、掉落归属和拾取链路 | `PythonNetworkStreamPhaseGameActor.cpp`, `InstanceBaseBattle.cpp`, `InstanceBaseMotion.cpp` | `net_play.gd`, `net_world.gd`, `entity_store.cpp`, `ground_items.gd` | PARTIAL |
|
||||
| `combat.flying_projectile` | 飞行物、目标、地形碰撞、命中和回收 | `PythonFlyModule.cpp`, `ActorInstanceFly.cpp`, `FlyingInstance.cpp`, `FlyTarget.cpp` | `fly_object.gd`, `world_shot.gd`, `skill_fx.gd`, `net_play.gd` | MAPPED |
|
||||
| `combat.affect_status` | 受击、眩晕、击倒、状态旗标和状态图标 | `AffectFlagContainer.cpp`, `PythonNetworkStreamPhaseGameActor.cpp`, `InstanceBaseEffect.cpp` | `combat_status_effect_system.gd`, `hit_reaction.gd`, `entity_store.cpp`, `char_status_ui.gd` | MAPPED |
|
||||
| `skill.cast.effect_timing` | 技能施法、动作事件、结果和特效时序 | `PythonSkill.cpp`, `PythonPlayerSkill.cpp`, `InstanceBaseEffect.cpp`, `ActorInstanceMotionEvent.cpp` | `player_skill.gd`, `net_play.gd`, `net_world.gd`, `skill_fx.gd`, `fx/effect_player.gd` | PARTIAL |
|
||||
| `skill.player_ui_cooldown` | 技能栏、快捷键、等级、冷却和禁止条件 | `PythonSkill.cpp`, `PythonPlayerSkill.cpp`, `PythonPlayer.cpp` | `player_skill.gd`, `skill_table.gd`, `ui/skill_ui.gd`, `ui/quickbar.gd` | MAPPED |
|
||||
| `item.inventory_equipment_state` | 背包、装备栏、物品状态、槽位和穿脱装备 | `PythonItem.cpp`, `ItemData.cpp`, `ItemManager.cpp`, `PythonNetworkStreamPhaseGameItem.cpp` | `inventory_ui.gd`, `item_list.gd`, `equip_model.gd`, `entity_store.cpp` | MAPPED |
|
||||
| `ui.equipment.tooltip` | 装备查看、属性面板和普通提示框 | `PythonNetworkStreamPhaseGameItem.cpp`, `PythonItem.cpp`, `PythonItemModule.cpp`, `PythonTextTail.cpp` | `view_equipment_ui.gd`, `item_tooltip_view.gd`, `ui_manager.gd`, `inventory_ui.gd`, `shop_ui.gd` | PARTIAL |
|
||||
| `item.consumable_action` | 使用、堆叠、拆分、快捷使用和消耗品反馈 | `PythonNetworkStreamPhaseGameItem.cpp`, `PythonPlayer.cpp`, `PythonItem.cpp`, `Packet.h` | `consumable_system.gd`, `item_split_system.gd`, `ui/quickbar.gd`, `net_play.gd` | MAPPED |
|
||||
| `item.drop_pickup` | 地面掉落、拾取、光标、归属和掉落名条 | `PythonNetworkStreamPhaseGameItem.cpp`, `PythonTextTail.cpp`, `PythonPlayerInputMouse.cpp` | `mob_drop_scatter.gd`, `ground_items.gd`, `mouse_controller.gd`, `net_play.gd` | MAPPED |
|
||||
| `item.refine_socket_costume` | 强化、镶嵌、属性、时装和外观换装 | `PythonItem.cpp`, `ItemData.cpp`, `PythonNetworkStreamPhaseGameItem.cpp` | `item_attr_system.gd`, `item_enchant_system.gd`, `jewelry_socket_system.gd`, `costume_system.gd`, `wardrobe_transmog_system.gd` | MAPPED |
|
||||
| `npc.dialog_shop` | NPC 交互、普通商店、个人商店和购买流程 | `PythonNonPlayer.cpp`, `PythonShop.cpp`, `PythonNetworkStreamPhaseGame.cpp`, `Packet.h` | `npc_shop_system.gd`, `private_shop_system.gd`, `ui/shop_ui.gd`, `ui/private_shop_ui.gd` | MAPPED |
|
||||
| `social.chat_text_tail` | 公聊、私聊、聊天气泡、名字尾标和文本布局 | `PythonChat.cpp`, `PythonTextTail.cpp`, `PythonMessenger.cpp` | `chat_tail.gd`, `text_tail.gd`, `text_tail_arrange.gd`, `ui/chat_ui.gd`, `whisper_chat_system.gd` | MAPPED |
|
||||
| `social.party_guild_messenger` | 组队、公会、好友、Messenger 和徽章 | `PythonGuild.cpp`, `PythonMessenger.cpp`, `PythonCharacterManager.cpp`, `GuildMarkDownloader.cpp` | `party_management_system.gd`, `guild_war_system.gd`, `guild_mark_uploader_system.gd`, `messenger_system.gd`, `ui/party_ui.gd`, `ui/guild_ui.gd`, `ui/friend_ui.gd` | MAPPED |
|
||||
| `social.exchange_safebox` | 玩家交易、仓库、安全箱和金币确认 | `PythonExchange.cpp`, `PythonSafeBox.cpp`, `PythonNetworkStreamPhaseGameItem.cpp` | `player_trade_exchange_system.gd`, `safebox_storage_keeper_system.gd`, `ui/exchange_ui.gd`, `ui/safebox_ui.gd` | MAPPED |
|
||||
| `quest.quest_event_navigation` | 任务状态、事件、对话、HUD 和导航提示 | `PythonQuest.cpp`, `PythonEventManager.cpp`, `PythonNetworkStreamPhaseGame.cpp` | `quest_log.gd`, `quest_dialog.gd`, `quest_curtain.gd`, `quest_event_system.gd` | MAPPED |
|
||||
| `world.dungeon_activity` | 地下城、地图事件、Boss、结果和排行 | `MapManager.cpp`, `GameEventManager.cpp`, `PythonNetworkStreamPhaseGame.cpp`, `PythonQuest.cpp` | `dungeon_state.gd`, `dungeon_result_ui.gd`, `world_boss_system.gd`, `dungeon_ranking_system.gd` | MAPPED |
|
||||
| `world.fishing_mining` | 钓鱼、采矿、资源采集和水域条件 | `PythonPlayerInput.cpp`, `PythonNetworkStreamPhaseGame.cpp`, `MapOutdoorWater.cpp`, `AreaTerrain.cpp` | `fishing_mining_system.gd`, `fishing_cooking_camp_system.gd`, `mining_refinery_smelt_system.gd` | MAPPED |
|
||||
| `world.mount_pet_polymorph` | 马匹、宠物、变身和骑乘战斗 | `PythonPlayer.cpp`, `InstanceBaseMotion.cpp`, `ActorInstance.cpp`, `PythonNetworkStreamPhaseGameActor.cpp` | `horse_system.gd`, `horse_combat_system.gd`, `pet_companion_system.gd`, `polymorph_system.gd`, `mounted_combat_skill_system.gd` | MAPPED |
|
||||
| `render.model_attachment` | Granny 模型、LOD、骨骼、武器、盾牌和发型挂点 | `EterGrnLib/ThingInstance.cpp/.h`, `EterGrnLib/LODController.cpp/.h`, `EterGrnLib/ModelInstance*.cpp`, `GameLib/ActorInstanceAttach.cpp`, `RaceData.cpp/.h`, `ItemData.cpp` | `metin2_model.h/.cpp`, `metin2_anim.cpp`, `gr2_bridge.cpp`, `player_view.gd`, `remote_player_view.gd`, `mob_view.gd`, `equip_model.gd`, `race_spec.gd` | PARTIAL |
|
||||
| `render.effect_particle_motion` | MDE/MSE、粒子、动作事件挂点和回收 | `RaceMotionData.cpp`, `RaceMotionDataEvent.h`, `ActorInstanceMotionEvent.cpp`, `EffectLib/EffectManager.cpp`, `EffectInstance.cpp`, `ParticleInstance.cpp`, `PythonEffectModule.cpp` | `formats/msa.cpp`, `extension/src/metin2_anim.cpp`, `fx/effect_player.gd`, `fx/mde.gd`, `fx/mse.gd`, `fx/skill_fx.gd`, `fx/effect_registry.gd`, `fx/motion_effect_anchor.gd`, `fx/target_effect_anchor.gd`, `game_scene.gd`, `ui/player_view.gd`, `ui/remote_player_view.gd`, `ui/mob_view.gd`, `net_world.gd` | PARTIAL |
|
||||
| `world.terrain_water_collision` | 高度、属性阻挡、水面、桥梁、拾取射线和地面贴合 | `MapOutdoor.cpp`, `AreaTerrain.cpp`, `MapOutdoorWater.cpp`, `TerrainDecal.cpp`, `EterLib/AttributeInstance.cpp` | `metin2_world.cpp`, `formats/area_data.cpp`, `formats/attribute_data.cpp`, `water_builder.cpp`, `hit_collision.gd` | MAPPED |
|
||||
| `world.environment_weather` | 天空、环境、光照、昼夜、雪和天气 | `MapOutdoorRender.cpp`, `MapOutdoorRenderHTP.cpp`, `MapOutdoorRenderSTP.cpp`, `SnowEnvironment.cpp`, `SnowParticle.cpp`, `EterLib/EnvironmentMap.cpp` | `environment_builder.cpp`, `formats/environment.cpp`, `metin2_world.cpp`, `game_scene.gd`, `weather_daynight_system.gd`, `fx/weather.gd` | PARTIAL |
|
||||
| `ui.input_camera_mobile` | 鼠标、相机、光标、IME、触屏和移动端窗口 | `PythonApplicationCamera.cpp`, `PythonApplicationCursor.cpp`, `PythonPlayerInputMouse.cpp`, `PythonPlayerInputKeyboard.cpp`, `PythonIME.cpp`, `EterLib/Input.cpp` | `game_camera.gd`, `player_controller.gd`, `mouse_controller.gd`, `ui/cursor_manager.gd`, `ui/mobile/*`, `ui/mobile/mobile_window_host.gd` | PARTIAL |
|
||||
| `ui.hud_target_minimap_status` | 角色状态、血条、目标框、小地图、文本尾标和快捷栏 | `PythonTextTail.cpp/.h`, `PythonMiniMap.cpp/.h`, `PythonPlayer.cpp/.h`, `PythonApplication.cpp/.h`, `PythonPlayerEventHandler.cpp`, `InstanceBase*.cpp`, `PythonNetworkStreamPhaseGame.cpp`, `uitaskbar.py`, `uicharacter.py`, `uitarget.py` | `hud.gd`, `net_play.gd`, `net_world.gd`, `ui/char_status_ui.gd`, `ui/target_ui.gd`, `target_board.gd`, `ui/minimap.gd`, `text_tail.gd`, `text_tail_arrange.gd`, `chat_tail.gd`, `ui/ground_items.gd`, `ui/quickbar.gd` | PARTIAL |
|
||||
| `resource.pack_locale_proto` | EterPack、资源解析、locale、proto、MSA/MSM 和格式转换 | `EterPack/*.cpp`, `EterLocale/*.cpp`, `PythonPackModule.cpp`, `ItemData.cpp`, `RaceDataFile.cpp` | `pack_mount.cpp`, `asset_io.cpp`, `locale.gd`, `proto.cpp`, `formats/*.cpp` | MAPPED |
|
||||
| `audio.bgm_ambience_3d` | BGM、区域环境音、角色音效、3D 监听器和前后台恢复 | `MilesLib/SoundManager*.cpp`, `PythonSoundManagerModule.cpp`, `GameLib/Area.cpp`, `MapOutdoor.cpp` | `audio.gd`, `bgm_director.gd`, `app_lifecycle.gd`, `formats/area_data.cpp`, `extension/src/metin2_world.cpp` | PARTIAL |
|
||||
| `lifecycle.platform_release` | Windows/桌面窗口、导出、资源自包含、崩溃和发布门禁 | `MSApplication.cpp`, `MSWindow.cpp`, `PythonApplication.cpp`, `PythonApplicationProcedure.cpp`, `CheckLatestFiles.cpp`, `EterPack/EterPackManager.cpp` | `build.sh`, `build-macos-client.sh`, `build-android.sh`, `export-android.sh`, `project/export_presets.cfg`, `project/client_main.gd`, `project/app_flow.gd`, `project/app_lifecycle.gd`, `project/asset_root.gd`, `project/asset_pack.gd`, `oracle/*` | PARTIAL |
|
||||
| `extension.current_only_features` | 没有 40250 参考实现的新增玩法和平台扩展 | 无对应 40250 运行时链路 | `*_system.gd` 中的新版副本、离线集市、邮件等 | EXCLUDED |
|
||||
|
||||
## 初始审计规则
|
||||
|
||||
1. 先完成 `UNMAPPED`/`MAPPED` 合同的静态调用链,再对照现有 `docs/CLIENT-GAP.md` 的历史结论;历史文档不能直接把合同提升为已验证。
|
||||
2. 每个合同都要建立完整分支表,至少覆盖成功、拒绝、空数据、重复触发、异步乱序、断线/切图、死亡/中断和清理路径。
|
||||
3. 现有 `project/test_*_parity.gd` 只能作为候选证据;测试通过不等于 40250 算法、资源来源和时序一致。
|
||||
4. `extension.current_only_features` 只用于记录没有 40250 对应行为的当前端扩展,不能把它们混入 40250 等价率,也不能因为有测试就标记为 40250 兼容。
|
||||
5. 本清单建立后,每次审计只允许改变对应合同的状态、矩阵、证据和历史;不得删除未完成合同来提高覆盖率。
|
||||
|
||||
## 活跃源码覆盖门
|
||||
|
||||
- Visual Studio 活跃项目的源码/头文件覆盖由 `.agents/skills/metin2-40250-parity-audit/scripts/audit_source_coverage.py` 生成,结果保存在 `audit/source-coverage.json` 和 `audit/reports/source-coverage.md`。
|
||||
- 本轮扫描到 524 个活跃源码/头文件:18 个 StdAfx 构建支持,142 个已在合同 manifest 中逐文件列出,364 个进入按行为合同归属的静态复核队列;当前未归属行为文件为 0。
|
||||
- `EffectLib`、`EterGrnLib`、`MilesLib` 已完成本轮活跃符号和生命周期链静态复核,但仍按实现差异保持 `PARTIAL`;其余 inferred queue 不得因为自动归属而视为已审计。
|
||||
- 下次审计必须先运行源码覆盖扫描,再从 `inferred_review_queue` 取下一组,补充对应合同的参考文件、符号、分支矩阵和测试证据;只有完成语义核对后才能转为 `reviewed_static_group`。
|
||||
@@ -0,0 +1,153 @@
|
||||
# item.consumable_action
|
||||
|
||||
## Scope
|
||||
|
||||
审计使用物品、消耗品、物品堆叠/拆分、快捷栏使用、快捷栏服务器同步、物品使用反馈和 NPC 商店出售请求。判断标准不是“本地函数能算出同样的结果”,而是从输入到 `CG_*` 请求、服务器 `GC_*` 确认、物品状态更新、点数/效果/UI 刷新的完整链路是否与 40250 一致。
|
||||
|
||||
## Reference call chain
|
||||
|
||||
- `PythonPlayer.cpp` / `PythonPlayerModule.cpp`
|
||||
- `SetQuickPage` 将页面索引按 `QUICKSLOT_MAX_LINE` 做取模并刷新背包窗口。
|
||||
- `RequestUseLocalQuickSlot` 只接受本地 8 格范围;道具槽转发 `SendItemUsePacket(TItemPos(INVENTORY, pos))`,技能槽调用 `ClickSkillSlot`,表情槽调用 `BINARY_ActEmotion`。
|
||||
- `RequestAddLocalQuickSlot`、`RequestAddToEmptyLocalQuickSlot`、`RequestDeleteGlobalQuickSlot`、`RequestMoveGlobalQuickSlotToLocalQuickSlot` 只发请求;内存快捷栏由后续 `GC_QUICKSLOT_*` 回包修改。
|
||||
- `MoveItemData` 是给 UI/脚本使用的本地槽位交换工具,不等同于服务端物品移动成功。
|
||||
- `PythonNetworkStreamPhaseGameItem.cpp`
|
||||
- `SendItemUsePacket`、`SendItemUseToItemPacket`、`SendItemMovePacket`、`SendItemDropPacket(New)` 先执行 `__CanActMainInstance`;装备相关路径还执行交易/商店/攻击中门控,必要时播放物品音效,写包后调用 `SendSequence`。
|
||||
- `SendShopSellPacket` / `SendShopSellPacketNew` 发送 NPC 商店出售请求;出售数量和价格由服务器确认,不由客户端直接增钱或清槽。
|
||||
- `RecvItemUsePacket` 只触发物品窗口刷新;`RecvItemUpdatePacket` 按 count→socket→attribute 的顺序写入后刷新。
|
||||
- `RecvQuickSlotAddPacket` / `RecvQuickSlotDelPacket` / `RecvQuickSlotMovePacket` 先修改本地快捷栏,再刷新物品窗口。
|
||||
- `Packet.h` / `PythonNetworkStreamModule.cpp`
|
||||
- 快捷栏使用、物品使用、物品对物品、物品移动、物品更新和商店出售分别有独立的请求/回包结构,不应由本地数组操作替代。
|
||||
- 消耗品具体效果(药水、日月神水、龙神药水、卷轴)主要由 40250 服务端 `char_item.cpp` 执行;40250 Windows 客户端本身负责发送 `ITEM_USE` 并消费 `GC_ITEM_USE`、`GC_ITEM_UPDATE`、`GC_SPECIAL_EFFECT`、`GC_AFFECT_*`、`GC_POINT_*` 等反馈。客户端源码中没有可证明本地独立执行这些服务端效果的等价实现。
|
||||
|
||||
## Current call chain
|
||||
|
||||
- `project/ui/quickbar.gd`
|
||||
- `assign`、`_clear`、`_swap` 调用 `M2Client::quickslot_add/del/swap`;但请求返回后立即修改 `_state`,真实客户端只有 `GC_QUICKSLOT_*` 到达 `EntityStore` 后才会产生权威状态。
|
||||
- `activate` 对 item 槽直接调用 `M2Client::use_item(1, cell)`,对 skill/emote 走各自本地链路;没有统一复现 40250 `RequestUseLocalQuickSlot` 的本地 8 格检查、服务器回包失败回滚和 `RecvItemUsePacket` 刷新时序。
|
||||
- `_page` 是 Godot UI 私有状态,`set_page` 不调用与 40250 对应的 `SetQuickPage`/统一刷新入口;36 个服务端槽位只显示前 32 个,与参考端可显示页/保留槽的关系尚未由端到端测试证明。
|
||||
- `extension/src/net/m2_client.cpp` / `extension/src/net/classic/classic_session.cpp`
|
||||
- `quickslot_add/del/swap`、`use_item`、`use_item_to_item`、`move_item` 和 `drop_item` 已能构造经典协议包,但 native 层只有游戏阶段、槽位/整数范围检查;没有 40250 的 `__CanActMainInstance`、装备类型、交易/商店/攻击中门控、物品音效和 `SendSequence` 语义。
|
||||
- `shop_sell` 已存在并发送服务端出售请求,但 `ItemSplitSystem::quick_sell_item` 没有使用它。
|
||||
- `project/consumable_system.gd`
|
||||
- 本地实现了日月神水 socket 容量、龙神药水 buff 描述和回城/地标卷轴结果字典,但没有被 `M2Client`、`EntityStore`、`GC_ITEM_USE`、`GC_ITEM_UPDATE`、`GC_AFFECT_*` 或 `GC_POINT_*` 链路调用;它不能证明消耗品已经在游戏中生效。
|
||||
- 脚本把服务端效果算法复制到了客户端,却没有处理服务端拒绝、重复包、延迟、断线、跨图和回滚。
|
||||
- `project/item_split_system.gd`
|
||||
- `split_item` 直接修改传入 Array 的 source/destination count 并发本地 signal;没有发送 40250 对应的 `SendItemMovePacket`,也没有等待两个槽位的 `GC_ITEM_*` 确认。
|
||||
- `quick_sell_item` 直接增加本地 gold、清空槽位并发 signal;没有调用 `M2Client::shop_sell`,也没有等待服务器金币/物品回包。因此它不是 40250 NPC 商店出售链路的实现。
|
||||
- `project/ui/inventory_ui.gd` / `project/ui/shop_ui.gd`
|
||||
- 普通拖动、物品对物品和正式商店出售路径已经调用 `move_item`、`use_item_to_item`、`shop_sell`;这是可复用的正确方向,但与 `ItemSplitSystem` 形成两条不同的物品权威路径。
|
||||
|
||||
## Branch matrix
|
||||
|
||||
| 场景 | 40250 | 当前端 | 结论 |
|
||||
|---|---|---|---|
|
||||
| 快捷栏添加/删除/交换 | 只发 CG 请求,GC 回包后更新本地槽 | native 请求正确;`quickbar.gd` 请求后乐观修改 `_state` | PARTIAL:拒绝/乱序时可能显示未确认状态 |
|
||||
| 快捷栏本地使用 | 本地槽 `<8`;道具发 `ITEM_USE`,技能走 `ClickSkillSlot`,表情走脚本回调 | UI 槽 `<8`;道具/技能/表情分支存在,但不是统一参考入口,死亡/忙碌/服务器拒绝回滚未形成同一链路 | PARTIAL |
|
||||
| 快捷栏页面 | 负数/超范围按 4 页取模,并刷新背包窗口 | UI 只接受 0..3,自己维护 `_page`,不复用统一 `SetQuickPage` 事件源 | PARTIAL |
|
||||
| 普通药水/消耗品 | 客户端发 `CG_ITEM_USE`,效果、扣数、点数和特效由服务端回包驱动 | 可以发 `M2Client::use_item`,但 `ConsumableSystem` 未接入反馈链 | PARTIAL |
|
||||
| 日月神水/龙神药水/卷轴 | 服务端判断 item proto/socket/状态,客户端只显示回包结果 | 本地静态脚本可计算结果,但没有 item proto、服务器确认、effect/point/warp 回包闭环 | PARTIAL |
|
||||
| 堆叠拆分 | 通过 `CG_ITEM_MOVE` 的源/目标/数量请求,服务端决定能否拆分并回包更新 | `ItemSplitSystem::split_item` 直接改 Array,不发网络请求 | GAP |
|
||||
| 快捷出售 | 通过 `CG_SHOP` `SELL/SELL2` 请求,服务器计算价格并回包金币/物品变化 | `ShopUI` 正式路径已调用 `shop_sell`;`ItemSplitSystem::quick_sell_item` 直接加金币清槽 | GAP:存在错误的旁路实现 |
|
||||
| 物品对物品使用 | 发送 `ITEM_USE_TO_ITEM`,反馈由服务端更新 | `InventoryUI` 已发送 `use_item_to_item` | PARTIAL:native action gates/反馈时序尚未对齐 |
|
||||
| 物品使用反馈 | `GC_ITEM_USE`/`GC_ITEM_UPDATE`/special effect/affect/point 触发对应刷新 | item update store 信号存在;未证明所有消耗品反馈和 UI 声音/特效/状态消费 | PARTIAL |
|
||||
| 中断/拒绝/乱序 | 不成功的服务器请求不应改变权威客户端状态 | 拆分/快捷出售本地 helper 无拒绝回滚;快捷栏和 UI 部分乐观更新 | GAP |
|
||||
|
||||
## Implementation-equivalence matrix
|
||||
|
||||
| 项目 | 状态 | 证据/限制 |
|
||||
|---|---|---|
|
||||
| Preconditions | PARTIAL | 基础 stage/cell/数量检查存在;行动状态、装备类型、交易/商店/攻击中门未在 native 统一执行 |
|
||||
| Branch structure | PARTIAL | 快捷栏三类入口和正式商店路径存在;本地消耗品、拆分、快捷出售旁路与参考协议链不一致 |
|
||||
| Algorithms/formulas | PARTIAL | 本地消耗品/价格/拆分算法可独立通过单元场景,但 40250 权威算法在服务端,当前本地复制没有被回包约束 |
|
||||
| State transition order | PARTIAL | 参考是 request→server→GC update→refresh;当前快捷栏/拆分/快捷出售有 request 后本地立即改变或直接本地改变的分支 |
|
||||
| Constants/units | PARTIAL | 快捷栏 36/4×8、item cell 和数量宽度有基础覆盖;消耗品 vnum/affect/socket 只在脚本测试出现,未证明与运行时 proto/服务端一致 |
|
||||
| Timing/event sources | PARTIAL | 当前大量依赖 Godot signal/本地函数;没有真实 ITEM_USE→ITEM_UPDATE/AFFECT/POINT/SPECIAL_EFFECT 的顺序 fixture |
|
||||
| Resource/data sources | PARTIAL | 正式 UI 可从 item_proto/item_list 获取数据;`ConsumableSystem` 使用硬编码 vnum/容量,`ItemSplitSystem` 使用默认价格,均未从 40250 权威资源和服务器结果取值 |
|
||||
| Protocol side effects | PARTIAL | native 物品、快捷栏、shop 包结构已定位;action gate、音效、sequence、失败通知和对应 GC 反馈未完整闭合 |
|
||||
| Interruption/failure/cleanup | GAP | 本地拆分/快捷出售没有服务端失败路径;快捷栏乐观状态、断线、换图和延迟 GC 期间的回滚/清理尚未证明 |
|
||||
|
||||
## Regression evidence
|
||||
|
||||
- `project/test_consumables_parity.gd`:36/36 通过,覆盖本地识别、socket 容量、恢复计算、buff 描述和卷轴结果字典;它没有启动 `M2Client`、服务器或 GC feedback。
|
||||
- `project/test_item_split_parity.gd`:19/19 通过,覆盖本地拆分、直接加金币的快捷出售和丢弃提示;由于测试正是调用本地 Array/Dictionary,它不能作为网络等价证据。
|
||||
- `project/test_taskbar_quickslot_parity.gd`:34/34 通过,覆盖 UI 页面、经验球和按钮状态;退出时仍有 91 个 CanvasItem RID、221 个 ObjectDB 对象和资源泄漏警告。
|
||||
- `project/complex_item_drop_test.gd`:退出失败,购买模式下“卖出目标应拒绝拖放”的检查失败;这说明物品窗口/商店目标的交互合同仍有回归问题。
|
||||
- `project/netplay_test.gd`:通过,但只验证 NetPlay 主循环,不覆盖消耗品或快捷出售服务器确认。
|
||||
- 需要新增的回归覆盖:快捷栏 GC add/del/swap 拒绝回滚;`ITEM_USE` 后无效/成功的 item update、affect、point、special effect 顺序;拆分请求失败不改本地状态;SELL2 服务器价格/金币/物品回包;断线/换图/重复回包和请求乱序。
|
||||
|
||||
## Deep audit round 2026-09-20
|
||||
|
||||
### Fresh regression results
|
||||
|
||||
- `godot --headless --path project --script test_consumables_parity.gd`:退出码 0,36/36 通过;仍然只是本地识别、socket、恢复计算、buff 描述和卷轴结果字典测试,没有 `ITEM_USE`/`GC_ITEM_USE`/`GC_ITEM_UPDATE` 服务器回路。
|
||||
- `godot --headless --path project --script test_item_split_parity.gd`:退出码 0,19/19 通过;测试直接检查 `Array`/`Dictionary` 的本地拆分、加金币和清槽,不能证明 `CG_ITEM_MOVE`/`CG_SHOP SELL2` 的服务器确认语义。
|
||||
- `godot --headless --path project --script test_taskbar_quickslot_parity.gd`:退出码 0,34/34 通过;退出时报告 91 个 CanvasItem RID、221 个 ObjectDB 实例、3 个资源和文本 RID 泄漏,因此只计局部 UI PASS,不计干净生命周期 PASS。
|
||||
- `godot --headless --path project --script complex_item_drop_test.gd`:退出码 1;购买模式下“卖出目标应拒绝拖放”的检查失败,说明商店目标的模式门仍有回归。
|
||||
- `godot --headless --path project --script netplay_test.gd`:退出码 0;只覆盖 NetPlay 主循环,未覆盖消耗品、快捷栏或商店服务器确认。
|
||||
|
||||
### Static parity findings
|
||||
|
||||
- 40250 的快捷栏添加/删除/交换只发送请求,`GC_QUICKSLOT_*` 到达后才修改权威槽位;当前 `Quickbar::assign`/`_clear`/`_swap` 在 native 请求返回后立即写 `_state`,拒绝、重复或乱序回包可能留下未确认 UI 状态。
|
||||
- 40250 的 `SetQuickPage` 对页码按 `QUICKSLOT_MAX_LINE` 取模并触发统一刷新;当前 `Quickbar` 只接受 0..3、维护私有 `_page`,没有统一 page 状态源和参考端刷新入口。
|
||||
- 40250 的物品使用、移动、物品对物品和出售发送前都经过 `__CanActMainInstance`,装备路径还检查交易/商店/攻击状态、播放物品音效并发送 sequence;当前 native 仅检查游戏阶段、槽位和整数范围。
|
||||
- `ConsumableSystem` 的本地效果函数没有接入 `EntityStore` 的 item/affect/point/special-effect 消费链;它不能替代服务端效果,也没有拒绝、重复、断线、跨图和回滚状态机。
|
||||
- `ItemSplitSystem::split_item` 直接修改源/目标数组,没有发 `CG_ITEM_MOVE` 并等待两个槽位的 `GC_ITEM_*`;`quick_sell_item` 直接加本地金币并清槽,没有统一走 `M2Client::shop_sell` 和服务器价格回包。
|
||||
- `InventoryUI`/`ShopUI` 已存在走 native 请求的正式路径,但与上述本地旁路并存,导致同一功能存在两套权威来源。
|
||||
|
||||
### Round conclusion
|
||||
|
||||
本轮仍为 `PARTIAL`:协议包的基础构造存在,局部 UI 和本地算法测试通过,但快捷栏确认时序、消耗品反馈闭环、堆叠拆分、快捷出售和失败清理没有与 40250 统一。未修改实现代码。
|
||||
|
||||
## Implementation round 2026-09-21T13:45Z
|
||||
|
||||
本轮修复了合同中两个会绕过 40250 服务端权威状态的实际旁路:
|
||||
|
||||
- `ItemSplitSystem::split_item` 现在只做源/目标/数量的前置验证,然后调用
|
||||
`M2Client::move_item(source, target, count)`;请求成功返回 `pending`,不再提前改
|
||||
source/destination 的本地 count,也不再提前发“已拆分”完成信号。
|
||||
- `ItemSplitSystem::quick_sell_item` 现在只调用 `shop_sell(cell, count)`,不再读取
|
||||
本地 `sale_price`、增加金币或清空物品;服务端后续 `GC_ITEM_UPDATE`/`GC_POINT_GOLD`
|
||||
才是唯一完成条件,新增 request 信号区分“已请求”和“已出售”。
|
||||
- `ShopUI::_drop_to_sell` 在购买模式下现在直接拒绝拖入出售目标,不发送
|
||||
`CG_SHOP SELL/SELL2`;只有切换到出售模式后才进入原有 anti-sell、确认框和发包路径。
|
||||
- 回归测试改为记录 `CG_ITEM_MOVE` / `CG_SHOP SELL2` 请求并断言本地数组、金币在
|
||||
服务器确认前保持不变;`test_item_split_parity` 20/20、`complex_item_drop_test`
|
||||
和 `shop_rules_test` 均通过。
|
||||
|
||||
仍保持 `PARTIAL`:快捷栏 GC add/del/swap 的拒绝回滚、消耗品真实
|
||||
`ITEM_USE→ITEM_UPDATE/AFFECT/POINT/SPECIAL_EFFECT` 包序、断线/换图/重复回包清理,
|
||||
以及物品音效和完整 sequence 证据仍需后续轮次。
|
||||
|
||||
## Implementation round 2026-09-21T14:15Z
|
||||
|
||||
本轮按 40250 `CPythonPlayer` 的快捷栏状态时序修复了另一条乐观更新旁路:
|
||||
|
||||
- `Quickbar::assign`、`_clear`、`_swap` 在线且 native 请求成功后不再直接写 `_state`;
|
||||
它们只发送 `CG_QUICKSLOT_ADD/DEL/SWAP`,等待 `EntityStore` 消费对应的
|
||||
`GC_QUICKSLOT_*` 并发出 `quickslots_changed` 后,由 `restore_from_server()` 重建界面。
|
||||
因此服务器拒绝、延迟或乱序回包不会被一次“发送成功”伪装成已确认状态。
|
||||
- `Quickbar::set_page` 改为按 `QUICKSLOT_MAX_LINE=4` 取模,`-1` 和 `4` 分别回到第 4 页和第 1 页,
|
||||
与 40250 `SetQuickPage` 的页面语义一致;本地仍保留 36 个服务端槽、显示 4×8 槽。
|
||||
- 现有测试夹具改为显式模拟 `GC_QUICKSLOT_*` 后才更新服务器快照,并新增延迟确认断言;
|
||||
`test_skill_inventory_parity` 45 项、`skill_test`、`mobile_gesture_test` 和任务栏分页回归通过。
|
||||
|
||||
仍保持 `PARTIAL`:快捷栏使用本身仍需补统一 `RequestUseLocalQuickSlot` 的失败反馈/动作门,
|
||||
消耗品 `ITEM_USE→ITEM_UPDATE/AFFECT/POINT/SPECIAL_EFFECT` 的真实包序、断线/换图清理,
|
||||
以及物品音效和完整 `SendSequence` 证据仍未闭合。
|
||||
|
||||
## Implementation round 2026-09-21T04:10Z — shared item sound source
|
||||
|
||||
本轮把消耗品/快捷使用路径依赖的物品音效分支统一到 native `M2Client`:
|
||||
|
||||
- `item_action_rules.h` 复刻 40250 `__GetUseSoundType`/`__GetDropSoundType` 的 type/subtype 分支。
|
||||
- `M2Client::use_item` 与 `move_item` 在行动门通过后、构造 transport 包前发出 `item_action_sound`;GameScene 统一交给 Audio 播放。普通 `USE_POTION` 保留参考端的无 use sound 行为。
|
||||
- `item_action_sound_parity_test.gd` 和 `net_bounds_test` 覆盖弓/箭/防具/首饰/能力提升药/普通药水/符咒/未知 vnum。
|
||||
|
||||
快捷栏的实际服务端确认、完整 `SendSequence` 包序、服务器拒绝反馈和断线重建仍保持 `PARTIAL`。
|
||||
|
||||
## Status
|
||||
|
||||
`PARTIAL`。快捷栏增删/交换现在遵循请求→`GC_QUICKSLOT_*`→重建显示的权威时序,分页也已按 4 页取模;
|
||||
但快捷栏使用的统一入口、消耗品反馈闭环、真实回包 fixture、音效/sequence 和跨生命周期清理仍未闭合,
|
||||
因此不能把整个合同标记为 40250 等价。
|
||||
@@ -0,0 +1,105 @@
|
||||
# item.drop_pickup
|
||||
|
||||
## Scope
|
||||
|
||||
审计地面掉落物从 `GC_ITEM_GROUND_ADD`、所有权更新、模型/特效/名条创建,到鼠标点选、走近、500ms 拾取节流、权限拒绝、`CG_ITEM_PICKUP`、`GC_ITEM_GROUND_DEL` 和拾取后物品回包的完整链路。
|
||||
|
||||
## Reference call chain
|
||||
|
||||
- `PythonNetworkStreamPhaseGameItem.cpp`
|
||||
- `RecvItemGroundAddPacket` 读取坐标、VID、vnum,转换全局/本地坐标后调用 `CPythonItem::CreateItem`。
|
||||
- `RecvItemOwnership` 调用 `CPythonItem::SetOwnership`;`RecvItemGroundDelPacket` 调用 `CPythonItem::DeleteItem`。
|
||||
- `PythonItem.cpp` / `ItemData.cpp` / `ItemManager.cpp`
|
||||
- `CreateItem` 先通过 `CItemManager::GetItemDataPointer` 验证 vnum,使用 `CItemData::GetDropModelThing` 创建实际掉落模型,按地形高度/法线处理位置,并挂接 drop effect、掉落声音和掉落动画状态;名条使用 item data 名称。
|
||||
- `SetOwnership` 只更新已有 VID 的 ground instance 和名条;未知 VID 不创建对象。
|
||||
- `DeleteItem` 同时删除模型实例和 `CPythonTextTail` 名条;`DeleteAllItems` 用于清理整张地图。
|
||||
- `PythonPlayer.cpp` / `PythonPlayerInputMouse.cpp`
|
||||
- `SendClickItemPacket` 在观战模式直接拒绝,500ms 节流;读取地面物品 owner、vnum 和 ItemManager anti-flag,非本人且不满足组队/可掉落可赠送条件时触发 `OnCannotPickItem`,否则发送 `SendItemPickUpPacket`。
|
||||
- `__OnPressItem` 清理旧预约和自动攻击;距离足够时发送 WAIT 状态和拾取请求,否则建立 `MODE_CLICK_ITEM` 预约并持续走近。
|
||||
- `__ProcessClickItem` 到达约 20 像素内时发送 WAIT、`SendClickItemPacket`、停止并清理预约;物品消失则清理预约。
|
||||
- `PythonTextTail.cpp` / `CPythonItem::__Pick`
|
||||
- 鼠标拾取先测试实际掉落模型的相交,再回退到文本尾标拾取;owner 名条独立显示并参加尾标排布。
|
||||
|
||||
## Current call chain
|
||||
|
||||
- `extension/src/net/classic/classic_parser.cpp` / `EntityStore`
|
||||
- 解析 `GC_ITEM_GROUND_ADD`、`GC_ITEM_GROUND_DEL`、`GC_ITEM_OWNERSHIP`,写入 `GroundItem{vid,vnum,x,y,z,owner}` 并通过 `M2Client` 信号同步。
|
||||
- 当前 parser/store 不查询 ItemManager 以拒绝未知 vnum;未知掉落仍可进入当前状态。
|
||||
- `project/ui/ground_items.gd`
|
||||
- `_on_added` 生成通用 `item_bag.gr2` 或 BoxMesh,占位模型;按 MapCoord 和地形采样设置位置,创建名字/owner Label3D、稀有光柱和名条防重叠。
|
||||
- `_on_removed`/`clear_for_map_change` 删除 Godot 节点;`_process` 旋转占位网格、按距离改名字颜色并重新排布尾标。
|
||||
- `try_pickup`/`try_pickup_vid` 有 3m 步行、5m 骑马、最近优先、500ms 节流和 owner 名字拦截;然后直接调用 `client.pickup_item(vid)`。
|
||||
- `pick_at` 用投影后的模型点、名字标签和 owner 标签的 64px 最近点选择,不是参考端的实际模型相交 + TextTail Pick。
|
||||
- `project/net_play.gd` / `player_controller.gd`
|
||||
- `pick_ground_item` 在约 20m?(代码使用 `CLICK_ITEM_PICKUP_CM` 换算)内立即发 WAIT 并尝试拾取,否则建立走近预约;有观战拦截和自动攻击清理。
|
||||
- `extension/src/net/m2_client.cpp` / `classic_session.cpp`
|
||||
- `pickup_item` 只检查 `is_in_game`、正 VID 和 u32 范围,再发送 `CG_ITEM_PICKUP`;没有参考端的 observer、item proto、owner/party、anti-flag 或 action gate。
|
||||
- `ground_item_added`/`ground_item_removed`/`item_picked_up` 信号存在,但未证明 `GC_ITEM_GROUND_DEL` 后 inventory item set/update 的严格反馈顺序。
|
||||
|
||||
## Branch matrix
|
||||
|
||||
| 场景 | 40250 | 当前端 | 结论 |
|
||||
|---|---|---|---|
|
||||
| 掉落创建 | ItemManager 验证 vnum,按 item proto 的 drop model、地形/法线、drop effect、声音和动画创建 | parser/store 建立记录;UI 通用 bag/BoxMesh,无实际 item drop model、effect、sound、drop animation | GAP |
|
||||
| 坐标/地形 | 全局坐标转换后按 Background height/normal 设置模型位置 | MapCoord 转换并 `sample_height + 0.1`,没有法线、模型包围盒和掉落动画落点 | PARTIAL |
|
||||
| 所有权更新 | 只更新已存在 VID;owner 进入独立 TextTail owner 实例 | `mut_ground_owner` 更新并重新发 ground added;UI 重建/更新 owner tag | MAPPED + tests |
|
||||
| 名称与颜色 | 名称取 `CItemData::GetName`,颜色/尾标来自 item data 和 TextTail | 名称优先 proto/item_list;grade/color 有适配,但 `_process` 近距离颜色会覆盖原品阶色 | PARTIAL |
|
||||
| 名条点击 | 实际模型 `Intersect`,再 TextTail `Pick` | 64px 投影点近邻,可能选中不可见/非相交对象或错误标签 | PARTIAL |
|
||||
| 近距离拾取 | 距离约 20 像素内发送 WAIT+pickup;远距离建立 MODE_CLICK_ITEM 走近 | `pick_ground_item` 有走近预约和 WAIT;`GroundItems` 又有 3m/5m 阈值,两个距离层未统一证明 | PARTIAL |
|
||||
| 最近拾取/按键 | `GetCloseItem`/预留动作按参考距离和对象选择 | 最近优先、3m/5m 和 500ms 已有本地测试 | MAPPED + tests |
|
||||
| 所有权拒绝 | owner 不同且非 party/anti-flag 允许时本地拒绝并回调 | 只比较 owner 字符串与本地名字;没有 ItemManager anti-flag 和 party member 分支 | PARTIAL |
|
||||
| 观战/行动门 | `IsObserverMode` 和主角行动状态阻止拾取 | NetPlay 有 observer;native `pickup_item` 未有统一行动门 | PARTIAL |
|
||||
| 拾取协议 | `SendItemPickUpPacket` 写包并 `SendSequence` | ClassicSession 构造 VID 包,但没有序列/副作用等价保证 | PARTIAL |
|
||||
| 拾取成功/失败清理 | server GC ground del 删除模型和名条;后续 item get/set/update 刷新背包 | ground del 删除 Node,item event/物品信号存在;失败、超时、重复回包和预约清理缺少包级测试 | PARTIAL |
|
||||
| 换图/断线 | `DeleteAllItems` 和阶段清理释放全部 ground instance/text tail | `clear_for_map_change` queue_free;延迟释放/generation/断线顺序未完全证明 | PARTIAL |
|
||||
|
||||
## Implementation-equivalence matrix
|
||||
|
||||
| 项目 | 状态 | 证据/限制 |
|
||||
|---|---|---|
|
||||
| Preconditions | PARTIAL | stage/VID、距离、owner 名和 observer 有部分检查;ItemManager vnum/anti-flag/party/action gate 缺失 |
|
||||
| Branch structure | PARTIAL | add/owner/del、最近拾取、走近预约和移动端入口存在;实际模型拾取、权限分支和 drop animation 分支不等价 |
|
||||
| Algorithms/formulas | PARTIAL | 坐标转换、距离、500ms、尾标布局已覆盖;模型包围盒/法线/近似拾取和颜色更新不同 |
|
||||
| State transition order | PARTIAL | reference 是 add→model/text tail→ownership→pickup→server del→inventory feedback;当前 signal→Node 与延迟 queue_free/双距离层顺序未端到端证明 |
|
||||
| Constants/units | PARTIAL | 3m/5m/500ms 有测试;参考 20 像素、真实 item model 尺寸和服务端 cm/m 换算还需统一 fixture |
|
||||
| Timing/event sources | PARTIAL | 当前依赖 Godot `_process`、signals、预约;参考由网络收包、输入状态机、Item/Actor/TextTail 事件驱动 |
|
||||
| Resource/data sources | GAP | 当前 ground model 使用通用 item bag/BoxMesh,没有使用 `CItemData::GetDropModelThing`、drop effect、drop sound、法线等资源链 |
|
||||
| Protocol side effects | PARTIAL | add/del/ownership/pickup packet 基础结构已解析;SendSequence、OnCannotPickItem 全分支和拾取后回包顺序未闭合 |
|
||||
| Interruption/failure/cleanup | PARTIAL | map change 清理有实现;server 拒绝、重复 ownership、延迟删除、断线和旧 VID generation 尚未完整覆盖 |
|
||||
|
||||
## Regression evidence
|
||||
|
||||
- `project/test_ground_pickup_parity.gd`:拾取距离、骑马距离、最近优先、500ms、owner 名拦截、稀有光柱和基础名条通过。
|
||||
- `project/mob_death_drop_test.gd`:掉落坐标/地形高度、Yang 名称、拾取调用和死亡清理通过。
|
||||
- `project/p2b_test.gd`:掉落节点、proto 名称、多条名条去重、owner 独立名条和颜色通过,但退出仍有 2 个 ObjectDB 泄漏。
|
||||
- `project/text_tail_arrange_test.gd`:尾标 AABB 和 owner 排布通过。
|
||||
- `extension/tests/net_entity_test.cpp`、`extension/tests/net_classic_session_test.cpp`:ground add/del/owner 与经典物品包基础布局通过。
|
||||
- 未覆盖:未知 vnum、真实 drop model/effect/sound、模型相交拾取、party + anti-flag 权限矩阵、observer/native action gate、CG_ITEM_PICKUP 后 GC_DEL→ITEM_GET/ITEM_SET 顺序、失败/重复/断线/换图竞态。
|
||||
|
||||
## Deep audit round 2026-09-20
|
||||
|
||||
### Fresh regression results
|
||||
|
||||
- `godot --headless --path project --script test_ground_pickup_parity.gd`:退出码 0;距离、骑马距离、最近优先、500ms、owner 拦截和基础名条通过。
|
||||
- `godot --headless --path project --script mob_death_drop_test.gd`:退出码 0;死亡脱离、地面坐标/高度、Yang 命名和拾取调用通过。
|
||||
- `godot --headless --path project --script p2b_test.gd`:退出码 0;地面掉落、名称、名条去重、owner 名条和颜色通过,但退出仍有 2 个 ObjectDB 实例泄漏。
|
||||
- `godot --headless --path project --script text_tail_arrange_test.gd`:退出码 0;尾标 AABB 与 owner 排布通过。
|
||||
- `godot --headless --path project --script test_drop_and_cursor_parity.gd`:退出码 0;MapCoord heading、cursor 注册、占位模型创建和点选通过。
|
||||
- `./build/extension/net_entity_test` 与 `./build/extension/net_classic_session_test`:均退出码 0;只证明 ground add/del/owner 和 pickup 包的基础 native 处理。
|
||||
|
||||
### Static parity findings
|
||||
|
||||
- 40250 `CPythonItem::CreateItem` 先用 `ItemManager` 校验 vnum,再加载 `GetDropModelThing`,创建 drop effect、drop sound、drop animation,并按地形法线调整实例;当前 parser/store 接受未知 vnum,`GroundItems` 使用通用 `item_bag.gr2` 或 BoxMesh,未接 item-specific model/effect/sound/animation。
|
||||
- 40250 的 `SetOwnership` 只更新已存在 VID,`DeleteItem` 同时回收 ground instance 与 TextTail;当前 owner 更新通过 `ground_item_added` 信号重建/更新 Godot 节点,仍没有未知 vnum/未知 VID 的严格资源语义和 generation 保护。
|
||||
- 40250 鼠标拾取优先测试真实掉落模型相交,再回退到 TextTail;当前 `pick_at` 只比较模型/名条/owner 标签的屏幕投影点距离 64px,可能选中不可见或非实际相交对象。
|
||||
- 40250 `SendClickItemPacket` 有 observer、owner、party/anti-flag 和 action gate;当前 `GroundItems` 只比较 owner 字符串,`M2Client::pickup_item` 只检查游戏阶段、VID 和整数范围,缺少 ItemManager anti-flag、party 分支、native action gate 与 SendSequence 等价语义。
|
||||
- 当前 `NetPlay` 的 20m 走近/等待路径与 `GroundItems` 的 3m/5m 拾取路径同时存在;它们都能发 `CG_ITEM_PICKUP`,但距离、预约清理、拒绝和重复/延迟回包尚未由同一状态机统一证明。
|
||||
- 现有 `ground_item_removed` 和 `item_picked_up` 信号存在,但没有包级 fixture 证明 `CG_ITEM_PICKUP → GC_ITEM_GROUND_DEL → GC_ITEM_GET/SET/UPDATE` 的成功、失败、重复、断线和换图时序。
|
||||
|
||||
### Round conclusion
|
||||
|
||||
本轮仍为 `PARTIAL`:基础 ground add/del/owner/pickup 协议和局部 UI 回归通过,但真实掉落资源链、模型相交点选、权限矩阵、native action gate 与拾取后的权威回包链仍未与 40250 统一;本轮未修改实现代码。
|
||||
|
||||
## Status
|
||||
|
||||
`PARTIAL`。当前端已具备掉落状态、坐标、owner 名条、最近拾取和基础协议骨架,但通用占位模型替代了 40250 的 item proto drop model/effect/sound/animation,拾取权限和模型点选分支也不完整;现有本地回归不能标记为完整 1:1,本轮未修改实现代码。
|
||||
@@ -0,0 +1,229 @@
|
||||
# item.inventory_equipment_state
|
||||
|
||||
## Scope
|
||||
|
||||
审计 40250 的本地物品状态模型:背包/装备/腰带槽位、完整物品数据、socket/属性更新、穿脱装备前置判断、网络刷新顺序,以及换图/断线时状态的边界。
|
||||
|
||||
## Reference call chain
|
||||
|
||||
- `PythonNetworkStreamPhaseGameItem.cpp`
|
||||
- `RecvItemSetPacket` / `RecvItemSetPacket2` 读取 vnum、count、flags、anti-flags、socket、attribute,并调用 `CPythonPlayer::SetItemData`。
|
||||
- `RecvItemUpdatePacket` 按顺序调用 `SetItemCount`、`SetItemMetinSocket`、`SetItemAttribute`,随后 `__RefreshInventoryWindow`。
|
||||
- `RecvItemUsePacket` 只刷新背包窗口;`RecvQuickSlot*Packet` 更新快捷栏后也刷新背包窗口。
|
||||
- `RecvItemSetPacket2` 在 `highlight` 为真时先发 `BINARY_Highlight_Item(window, cell)`,再刷新背包;旧 `RecvItemSetPacket` 的 flags/anti-flags 取零。
|
||||
- `PythonPlayer.cpp` / `GameType.h`
|
||||
- `TItemPos::IsValidCell` 限制窗口与 cell 域;`SetItemData` 对非零 vnum 先通过 `CItemManager::GetItemDataPointer` 验证。
|
||||
- `aItem[c_Inventory_Count]` 是统一的全局槽位空间;启用新装备系统时背包有效范围覆盖普通背包、保留装备位、龙魂位和腰带背包。
|
||||
- `IsEquipCell` 使用全局装备区间;`IsEquipItemInSlot` 再通过 `CItemData::IsEquipment` 判断物品是否真的是装备。
|
||||
- `PythonNetworkStreamPhaseGameItem.cpp`
|
||||
- `SendItemUsePacket` / `SendItemMovePacket` 先检查主角是否可行动;装备操作还受交易、商店、攻击中状态门控,并播放物品音效,最后发送协议包和 sequence。
|
||||
- `GameLib/ItemData.cpp` / `ItemManager.cpp`
|
||||
- item_proto/item_list 是类型、装备性、模型、图标和物品数据的权威来源,不由槽位编号推断。
|
||||
|
||||
### Reference action gates
|
||||
|
||||
- `CPythonNetworkStream::SendItemUsePacket`、`SendItemMovePacket`、`SendItemDropPacket`、`SendItemDropPacketNew`、`SendItemUseToItemPacket` 均先调用 `__CanActMainInstance`。
|
||||
- 穿脱装备路径额外调用 `IsEquipItemInSlot`;交易、商店、攻击中分别阻止装备移动/使用,并向游戏窗口发对应通知。
|
||||
- 通过前置门后,使用和移动分别调用 `PlayUseSound` / `PlayDropSound`,写入固定 packet,再调用 `SendSequence`。
|
||||
|
||||
## Current call chain
|
||||
|
||||
- `extension/src/net/classic/classic_parser.cpp`
|
||||
- 解析 40250 经典 21/20/25 号物品包;20 号 deprecated empty 包按参考端的 42 字节布局处理,vnum 为零时清槽,否则按 Set 写入。
|
||||
- `extension/src/net/entity_store.cpp`
|
||||
- `EntityStore::apply` 处理 m2dev 物品包;`mut_item_set`、`mut_item_del`、`mut_item_update` 写入 inventory/equipment/belt/dragon-soul 存储并生成 `m_inv_changes`。
|
||||
- `mut_slot` 同时兼容全局 equipment/belt cell 与旧 raw wear/belt cell,避免经典包和当前 UI 的窗口表示互相破坏。
|
||||
- `extension/src/net/m2_client.cpp`
|
||||
- `get_inventory` / `get_equipment` / `get_belt_inventory` 暴露状态;`inventory_changed` 逐 cell 发出刷新信号。
|
||||
- `move_item` / `use_item` / `drop_item` / `use_item_to_item` / `give_item` 统一通过 `valid_item_pos` 校验后转发到 Classic 或 m2dev 会话。
|
||||
- `project/ui/inventory_ui.gd`
|
||||
- `_to_wire` 将旧 11 个装备位映射到 equipment 窗,将 costume/ring/belt 映射到全局 inventory cell;刷新时将 `get_inventory`、`get_equipment` 和 belt inventory 投影到 UI。
|
||||
- 右键装备先走 `EquipRules.can_equip`;服务器仍是最终仲裁。`equip_model.gd` 监听装备变化并从 item_proto/item_list/parts 更新模型。
|
||||
|
||||
### Current action path
|
||||
|
||||
- `M2Client::move_item/use_item/drop_item/drop_item_count/use_item_to_item/give_item`
|
||||
- 统一检查 `is_in_game`、整数宽度、`bounds::item_cell` 和 40250 主角/装备行动门;use/move 在发包前从 item_proto 选择并发出物品音效事件,然后转发到 session/game client。
|
||||
- `ClassicSession::send_item_*`
|
||||
- 只检查 `Stage::InGame`、目标/数量的基础范围并构造 packet;行动门与物品音效由共享 `M2Client` 入口完成。
|
||||
- `EntityStore::mut_item_set/update/del`
|
||||
- 不查询 `ItemManager`/item_proto;未知 vnum 仍可进入 store,`get_inventory` 只返回 vnum 非零项。
|
||||
|
||||
## Branch matrix
|
||||
|
||||
| 场景 | 40250 | 当前端 | 结论 |
|
||||
|---|---|---|---|
|
||||
| 完整 Set | 非零 vnum 先查 ItemManager,再覆盖槽位全部字段并刷新 | Classic/m2dev 均覆盖 vnum/count/flags/anti-flags/socket/attr 并发 `inventory_changed`,但未知 vnum 不拒绝,且旧 Set/Set2 的 flags 语义被统一 | PARTIAL:资源拒绝和 21/22 的字段分支不等价 |
|
||||
| deprecated empty Set/Del | 20 号包固定按完整 42 字节读;vnum=0 清空 | Classic parser 保留固定帧长并分流为 `mut_item_del` | STATIC 部分成立;m2dev 使用独立 GC_ITEM_DEL |
|
||||
| Update 非空槽 | 更新 count、socket、attr,不改 vnum;`SetItemCount` 会先触发一次 `RefreshInventory`,包处理末尾再刷新 | `mut_item_update` 一次性覆盖并只发一个 `inventory_changed` | PARTIAL:最终字段接近,但刷新次数和中间状态事件源不同 |
|
||||
| Update 有效但当前 vnum=0 的槽 | `GetItemData` 仍返回有效槽指针,更新包字段 | 已移除“空槽直接丢弃”分支,保持 count/socket/attr 和刷新事件 | FIXED + regression |
|
||||
| 无效 window/cell | `TItemPos::IsValidCell` 直接拒绝,不发包 | 原先只做 u8/u16 宽度检查;现在 `bounds::item_cell` 限制 168/134/180/16 域 | FIXED + regression |
|
||||
| 装备使用/穿脱 | `__CanActMainInstance`、交易/商店/攻击中门控、声音、包序列 | UI 有 EquipRules 前置校验;native 已统一行动门、装备性和 item_proto 音效事件;完整 m2dev/经典 sequence 与服务端拒绝边界仍未完全证明 | PARTIAL |
|
||||
| 装备类型判定 | `IsEquipCell` + ItemManager 的 `CItemData::IsEquipment` | UI 使用 `EquipRules`;native 发送路径不重新读取 item_proto,且 store 可接受未知/非装备 vnum | PARTIAL |
|
||||
| UI 刷新 | Set/Update/QuickSlot 后统一刷新窗口,Set2 可能先高亮;Update 的 `SetItemCount` 与末尾刷新有明确时序 | native 发逐 cell 信号,UI 全量刷新;不含 Set2 highlight,Update 只有一次回调,listener 顺序未与 Python window callback 对齐 | PARTIAL |
|
||||
| 换图/断线 | `CPythonPlayer::Clear` 清战斗/UI 状态;物品由新阶段包重新建立 | `EntityStore::reset_for_map_change` 清物品扩展窗/仓库/龙魂,但普通背包/装备按注释保留 | PARTIAL:需继续核对经典 Loading 包的实际重建边界 |
|
||||
|
||||
## Implementation-equivalence matrix
|
||||
|
||||
| 项目 | 状态 | 证据/限制 |
|
||||
|---|---|---|
|
||||
| Preconditions | PARTIAL | cell 域已对齐;可行动、交易/商店/攻击中门控尚未由 native 统一执行 |
|
||||
| Branch structure | PARTIAL | Set/Del/Update、经典 deprecated 帧和旧/扩展槽已覆盖;未知 item_proto 和装备操作拒绝分支未闭合 |
|
||||
| Algorithms/formulas | MAPPED | 当前没有改写物品属性公式;装备模型映射仍由 Godot adapter 承担 |
|
||||
| State transition order | PARTIAL | 收包→store→信号→UI 顺序已定位,但与 `__RefreshInventoryWindow` 的全部监听副作用尚未逐项证明 |
|
||||
| Constants/units | PARTIAL | 槽位域已按 GameType.h 固定值;新装备/龙魂/腰带实际编译分支仍需 fixture 覆盖 |
|
||||
| Timing/event sources | PARTIAL | 当前使用 Godot signal + 全量 refresh,不是原端 Python window callback 的同一事件源 |
|
||||
| Resource/data sources | PARTIAL | UI 读取 item_proto/item_list;native Set 尚未验证 vnum 必须存在于 ItemManager,`get_inventory` 还隐藏 vnum=0 但 count/socket/attr 已更新的槽 |
|
||||
| Protocol side effects | PARTIAL | 包结构、经典 20/21/25 解析、行动门、Set2 highlight 和 use/move 音效已有证据;每个操作的 `SendSequence` 及服务端拒绝副作用仍不完整 |
|
||||
| Interruption/failure/cleanup | PARTIAL | 非法 cell 已拒绝;空槽 Update 已保持;断线、换图、服务器拒绝后的 UI/拖拽回滚仍需继续审计 |
|
||||
|
||||
## Regression evidence
|
||||
|
||||
- `extension/tests/net_bounds_test.cpp`:验证 inventory/equipment/dragon-soul/belt 有效范围及非法窗口拒绝。
|
||||
- `extension/tests/net_entity_test.cpp`:验证 Set、Update、Del、socket/attribute、旧装备、扩展 belt,以及空槽 Update 不被错误丢弃。
|
||||
- `extension/tests/net_classic_session_test.cpp`:验证经典 40250 物品包固定长度、20 号 deprecated 清槽/设物品和 25 号 Update。
|
||||
- `project/inventory_ui_test.gd`、`project/equip_model_test.gd`:验证 UI 槽位映射、数量显示、拖放和装备模型刷新。
|
||||
|
||||
本轮实际执行的自动测试全部退出码通过:`net_bounds_test`、`net_entity_test`、`net_classic_session_test`、`inventory_ui_test.gd`、`equip_model_test.gd`、`item_tooltip_test.gd`、`view_equipment_ui_test.gd`。其中 `view_equipment_ui_test.gd` 仍报告 5 个 ObjectDB 泄漏;这些测试没有覆盖未知 vnum、交易/商店/攻击中门、物品音效、Set2 highlight、Update 双刷新、空槽 Update 的 API 可见性和换图/断线重建。
|
||||
|
||||
## Status
|
||||
|
||||
`PARTIAL`。前轮修复了两个可复现的结构差异:非法物品 cell 仍可发包,以及有效空槽的 GC_ITEM_UPDATE 被丢弃。本轮确认未知 vnum 仍可进入 native store,装备操作仍缺 40250 的行动状态/交易/商店/攻击中门和物品音效,Set2 highlight 与 Update 刷新事件时序也未对齐;完整 Loading 重建顺序仍未达到 40250 的 1:1 证据标准。
|
||||
|
||||
## Deep audit round 2026-09-20
|
||||
|
||||
本轮重新执行 `net_bounds_test`、`net_entity_test`、`net_classic_session_test`、`inventory_ui_test.gd`、`equip_model_test.gd`、`item_tooltip_test.gd` 和 `view_equipment_ui_test.gd`,并把操作前置门与收包副作用逐项对回 40250:
|
||||
|
||||
- 七项回归均退出码 0;证明槽位边界、Set/Update/Del、经典 20/21/25 号物品帧、UI 填充、装备模型、tooltip 和单属性面板没有新增功能回退。
|
||||
- `RecvItemSetPacket2` 的 `highlight` 在 40250 会先调用 `BINARY_Highlight_Item` 再刷新窗口;当前 native parser/store 只发逐 cell `inventory_changed`,没有等价高亮事件。
|
||||
- 40250 `RecvItemUpdatePacket` 明确先 `SetItemCount`、socket、attribute,并在中间/末尾触发窗口刷新;当前 `mut_item_update` 合并为一次覆盖和一次信号,最终数据接近但监听副作用时序不同。
|
||||
- `CPythonNetworkStream::SendItemUse/Move/Drop/UseToItem` 先走 `__CanActMainInstance`,装备操作还检查交易、私店、攻击中并播放物品音效;当前 `M2Client`/`ClassicSession` 只做阶段、范围和数值校验,UI 的 `EquipRules` 不能替代 native 发送路径的统一门控。
|
||||
- 当前 `EntityStore` 不通过 ItemManager/item_proto 拒绝未知 vnum;`get_inventory` 也只暴露非零 vnum,因而无法证明参考端对有效空槽的 count/socket/attribute 字段可见性完全一致。`view_equipment_ui_test.gd` 虽退出码 0,但退出时泄漏 5 个 ObjectDB 实例,不能作为干净生命周期证据。
|
||||
|
||||
结论:物品状态和装备面板基础链可用,但协议副作用、操作前置门、资源校验、空槽 API 和换图/断线重建仍未与 40250 统一,合同保持 `PARTIAL`。
|
||||
|
||||
## Implementation round 2026-09-21
|
||||
|
||||
本轮按 40250 `PythonNetworkStreamPhaseGameActor::__CanActMainInstance` 和
|
||||
`PythonNetworkStreamPhaseGameItem.cpp` 的入口顺序修复了可在当前 native 架构中闭合的部分:
|
||||
|
||||
- 新增 `item_action_rules.h`,Classic 与 m2dev 共用同一套主角存在、死亡、眩晕、击倒门;有效参数但当前不可行动时,`M2Client` 按 40250 语义消费调用并返回成功,不向 session 发包。
|
||||
- `M2Client::move_item/use_item/use_item_to_item/drop_item/drop_item_count/give_item` 已在 transport 前统一检查;独立 equipment window 的操作还检查交易、私店和攻击中状态。攻击/技能/移动状态通过 native 入口维护,主角死亡时清除本地攻击锁存。
|
||||
- 经典 21 号包和 m2dev `GC_ITEM_SET` 现在保留 `highlight`,`EntityStore` 生成独立 `ItemHighlight` 队列;M2Client 在 `inventory_changed` 前发 `item_highlighted`,InventoryUI 消费该信号并对对应格子做 40250 风格短暂高亮。
|
||||
- 增加了 action-rule、经典 Set2 highlight、m2dev Set2 highlight 和 InventoryUI 高亮回归断言;C++ 全量构建、CTest 以及 `inventory_ui_test.gd` 均通过。
|
||||
|
||||
仍保持 `PARTIAL`:当前端还没有将 item_proto/ItemManager 的装备性判定下沉到 native,因而 inventory 全局装备位无法在所有协议入口上复刻 `IsEquipItemInSlot`;物品使用/丢弃音效与 sequence 仍没有 40250 的等价资源/事件源;`RecvItemUpdatePacket` 的 count→socket→attribute→刷新中间副作用仍被 store 合并为一次最终状态事件;未知 vnum 拒绝、空槽字段 API、服务器拒绝后的拖拽回滚以及换图/断线重建仍需后续合同轮次验证。
|
||||
|
||||
## Next audit round 2026-09-20T18:19Z
|
||||
|
||||
本轮把“客户端是否能直接执行物品操作”与“查看装备属性面板”分开核对,避免只因 UI 测试通过就把 native 操作链视为等价:
|
||||
|
||||
- `CPythonNetworkStream::SendItemUsePacket` 的顺序是 `__CanActMainInstance` → 装备槽的交易/商店/攻击中门 → `PlayUseSound` → 发包 → `SendSequence`;当前 `M2Client::use_item`(`extension/src/net/m2_client.cpp:2688`)只检查 in-game 和 cell 范围,直接进入 `ClassicSession::send_item_use`。`move_item`、`drop_item`、`use_item_to_item` 也没有同一主角动作门,因而 UI 层 `EquipRules` 不能覆盖脚本/快捷栏/native 调用入口。
|
||||
- `RecvItemSetPacket2` 的 `highlight` 是 40250 明确的额外副作用;当前 `EntityStore` 只产生 `inventory_changed`,本轮没有发现 `item_highlighted` 等价事件。该差异不会被 `net_entity_test` 的最终槽位数据断言捕获。
|
||||
- `view_equipment_ui_test.gd`、`inventory_ui_test.gd` 和 `item_tooltip_test.gd` 均通过,但它们只证明最终 UI 数据和局部窗口装配;未证明交易/商店/攻击中拒绝、物品音效、Set2 高亮和 Update 中间刷新顺序。
|
||||
|
||||
本轮没有修改实现代码;合同继续为 `PARTIAL`。下一步应先把动作门收敛到 native 发送层,再用 fake session 记录“拒绝不发包 / 允许按顺序发包”的分支测试,之后再处理 Set2 高亮与 Update 刷新事件顺序。
|
||||
|
||||
## Next audit round 2026-09-20T18:31Z
|
||||
|
||||
本轮沿 `PythonNetworkStreamPhaseGameItem.cpp` 的收包和发包函数逐项复核到当前 native 入口:
|
||||
|
||||
- 40250 的 `RecvItemSetPacket2` 在写入物品后,若 `highlight` 为真会额外触发 `BINARY_Highlight_Item(window, cell)`,再刷新背包。当前 classic parser 和 `EntityStore::mut_item_set` 都读取完整物品字段但丢弃 `highlight`,没有对应信号或 UI 消费路径。
|
||||
- 40250 的 `RecvItemUpdatePacket` 依次调用 `SetItemCount`、全部 socket、全部 attribute,最后 `__RefreshInventoryWindow`;`SetItemCount` 自身还触发一次 `RefreshInventory`。当前 `mut_item_update` 合并成一次覆盖和一次 `inventory_changed`,最终槽位数据接近,但监听者看不到同一刷新时序。
|
||||
- 40250 的 `SendItemUsePacket`/`SendItemMovePacket`/`SendItemDropPacket`/`SendItemUseToItemPacket` 统一先经过 `__CanActMainInstance`;装备移动/使用还检查交易、商店、攻击中并播放使用/丢弃音效。当前 `M2Client` 只检查 `is_in_game`、cell/count 宽度并直接调用 `ClassicSession`/m2dev session,native 层没有对应主角行动门、交易/商店/攻击状态或音效副作用。
|
||||
- 因此已有槽位、空槽 Update、非法 cell 和 UI 测试不能证明“死亡/攻击/交易/商店期间不发物品包”以及 Set2 高亮行为一致;本轮未修改实现代码,合同继续为 `PARTIAL`。
|
||||
|
||||
## Implementation round 2026-09-20T19:03Z
|
||||
|
||||
本轮按 `RecvItemUpdatePacket` → `SetItemCount` → `SetItemMetinSocket`/
|
||||
`SetItemAttribute` → `__RefreshInventoryWindow` 的顺序修复 Update 的状态事件:
|
||||
|
||||
- `InvChange` 现在携带写入时的 `Item` 快照,而不是只有 window/cell。
|
||||
- `mut_item_update` 在 count 写入后先记录一次快照,再写 sockets/attributes 并记录最终快照;
|
||||
m2dev `GC_ITEM_UPDATE` 也复用同一入口,避免两条协议语义分叉。
|
||||
- native 新增 `inventory_state_changed(window, cell, item)`,InventoryUI 优先消费快照;
|
||||
因此第一次刷新看到“新数量+旧 socket/属性”,第二次刷新看到完整最终状态。原有
|
||||
`inventory_changed` 保留给旧监听者兼容。
|
||||
- `net_entity_test` 增加普通槽和空槽的两阶段快照断言;C++ 全量构建、CTest、
|
||||
`inventory_ui_test.gd` 和移动时间线回归均通过。
|
||||
|
||||
这是一层保持 Godot pump 架构的事件适配,不代表所有旧 `inventory_changed` 监听者都已经
|
||||
改成快照消费;ItemManager 未知 vnum/装备分类、物品音效和 sequence、服务器拒绝回滚、
|
||||
换图重建及新装备/腰带乱序矩阵仍保持 `PARTIAL`。
|
||||
|
||||
## Implementation round 2026-09-21T13:00Z
|
||||
|
||||
本轮继续对照 `GameType.h::TItemPos::IsEquipCell`、
|
||||
`CPythonPlayer::IsEquipItemInSlot` 和 `CItemData::IsEquipment`,修复了 native
|
||||
发送入口把“装备窗口”直接等同于“装备物品”的差异:
|
||||
|
||||
- `M2Client` 新增 `set_item_proto`,复用 GameScene 已加载的同一份 item_proto;
|
||||
`is_equip_item_in_slot` 现在先验证全局装备区/旧 raw wear 坐标,再通过 item_proto
|
||||
的 type 仅把 `ITEM_TYPE_WEAPON(1)`、`ITEM_TYPE_ARMOR(2)` 视为 40250 的
|
||||
`CItemData::IsEquipment()`。
|
||||
- `move_item` / `use_item` 的 native 前置门改为检查源槽的真实装备性,不再因空的
|
||||
dedicated equipment window、时装、戒指或腰带槽位而误触发交易/商店/攻击限制;
|
||||
无 proto 的启动过渡期保留装备位的 fail-closed 限制,避免绕过门控。
|
||||
- 新增纯规则回归断言,明确 use、costume、ring 不属于 40250 的
|
||||
`IsEquipment()` 分支;C++ 23 项 CTest、`inventory_ui_test.gd`、
|
||||
`equip_model_test.gd`、`netbridge_test.gd` 均通过。
|
||||
|
||||
仍保持 `PARTIAL`:`SetItemData` 对未知 vnum 的拒绝尚未下沉到 EntityStore,完整
|
||||
sequence、旧 `inventory_changed` 监听者的中间快照消费、服务器拒绝回滚以及换图/断线
|
||||
重建仍未达到 40250 的完整证据标准。
|
||||
|
||||
## Implementation round 2026-09-21T13:20Z
|
||||
|
||||
本轮继续闭合 `CPythonPlayer::SetItemData` 的资源校验分支:
|
||||
|
||||
- `EntityStore` 新增可选的 item vnum validator;validator 生效后,非零未知 vnum
|
||||
在 `mut_item_set` 中保持原槽位不变,不产生刷新或高亮副作用。
|
||||
- `M2Client::set_item_proto` 在 GameScene 的 item_proto 加载成功后把同一资源表接入
|
||||
active world;由于真实加载顺序是先收 loading burst、后建 proto,接入时还会清理
|
||||
之前已进入 store 的非法物品,并为换图/重连后的新 world 重新绑定一次。
|
||||
- `net_entity_test` 新增“未知 vnum 拒绝、已知 vnum 接受、迟到 validator 清理”的
|
||||
回归断言;C++ 全量 CTest、背包 UI、装备模型和 NetWorld bridge 回归均通过。
|
||||
|
||||
仍保持 `PARTIAL`:物品使用/丢弃音效和 sequence 的资源侧副作用、旧
|
||||
`inventory_changed` 监听者的中间快照消费、服务器拒绝后的拖拽回滚,以及新装备/龙魂/
|
||||
腰带乱序和换图重建的完整矩阵仍需后续审计。
|
||||
|
||||
## Audit round 2026-09-20T21:28Z
|
||||
|
||||
本轮重新从 `TItemPos::IsValidCell/IsEquipCell`、`SetItemData`、
|
||||
`RecvItemSetPacket2/RecvItemUpdatePacket` 和 `SendItemUse/Move/Drop` 对照到
|
||||
`EntityStore -> M2Client -> InventoryUI/EquipModel`,并重跑 3 项 native 与 5 项 Godot 物品回归:
|
||||
|
||||
- 槽位边界、经典 20/21/25 号包、Set/Update/Del、socket/attribute、空槽 Update、Set2 highlight、ItemManager 装备分类、未知 vnum 校验以及装备模型/tooltip/单属性面板均保持通过。
|
||||
- 当前 native 入口已统一主角行动门、真实装备类型判定和未知 vnum validator,但仍没有 40250 `PlayUseSound/PlayDropSound` 与 `SendSequence` 的等价资源/事件源。
|
||||
- `RecvItemUpdatePacket` 的 count→socket→attribute 两次可观察快照已适配;旧 `inventory_changed` 监听者是否都按中间状态消费、空槽 count/socket/attribute 的公开读取语义仍未完全证明。
|
||||
- 新装备/龙魂/腰带在 classic 与 m2dev 双协议下的乱序、服务端拒绝/拖拽回滚、断线或换图重建仍缺协议级 fixture。`view_equipment_ui_test` 和 `netbridge_test` 通过,但报告 5 个 ObjectDB 泄漏,因此不能作为干净生命周期证据。
|
||||
|
||||
结论:本轮没有新增实现修改;物品主数据和装备判定链可回归,但操作副作用、异常回滚和扩展槽位生命周期仍与 40250 不完全统一,合同保持 `PARTIAL`。
|
||||
|
||||
## Audit round 2026-09-21T03:48Z — inventory/equipment regression reconciliation
|
||||
|
||||
本轮没有重复修改物品实现,而是重新执行 P0 物品链回归并核对上一轮修复的证据边界:
|
||||
|
||||
- `net_bounds_test`、`net_entity_test`、`net_classic_session_test`、
|
||||
`inventory_ui_test.gd`、`equip_model_test.gd`、`item_tooltip_test.gd` 和
|
||||
`view_equipment_ui_test.gd` 均返回 0;非法 cell、Set/Update/Del、未知 vnum、装备
|
||||
分类、Set2 高亮、双阶段 Update 快照、UI 映射和装备模型没有回归。
|
||||
- `view_equipment_ui_test.gd` 仍报告 5 个 ObjectDB 泄漏。功能断言通过,但该结果不能
|
||||
作为 40250 生命周期/退出清理的干净证据,保留为后续清理项。
|
||||
- 对照 40250 `PlayUseSound/PlayDropSound`、`SendSequence`、服务器拒绝后的窗口回滚及
|
||||
新装备/龙魂/腰带乱序边界,当前没有新的等价实现或协议 fixture,因此不升级合同状态。
|
||||
|
||||
结论:本轮确认已实现部分保持通过,未发现新的可安全闭合差异;合同继续为 `PARTIAL`,
|
||||
下一次物品实现应优先处理音效/sequence 或拒绝回滚,并先补失败回归。
|
||||
|
||||
## Implementation round 2026-09-21T04:10Z — item action sound parity
|
||||
|
||||
本轮按 40250 `CPythonItem::__GetUseSoundType`、`__GetDropSoundType`、
|
||||
`CPythonNetworkStream::SendItemUsePacket` 和 `SendItemMovePacket` 修复物品操作音效:
|
||||
|
||||
- 新增共享 `item_action_rules.h` 音效枚举与 type/subtype 分支,严格保留武器/弓/箭、身体/首饰防具、能力提升药、普通药水无音效、符咒和默认音效的 40250 分支。
|
||||
- `M2Client` 从同一份 item_proto 解析音效,只有通过主角行动门后才发 `item_action_sound`;使用和移动分别在发送包前发出 use/drop 音效。GameScene 将该事件交给 Audio 播放,避免 UI 和两种 transport 各自推断。
|
||||
- 新增 native 规则断言与 `item_action_sound_parity_test.gd`,覆盖分类、资源文件名、未知 vnum 和普通药水无音效;背包、装备模型、tooltip、查看装备面板和 NetWorld 回归继续通过。
|
||||
|
||||
仍保持 `PARTIAL`:直接丢弃包在 40250 入口本身不额外播放 inventory drop sound;完整 `SendSequence`/m2dev 包序、服务器拒绝回滚、旧监听者中间快照、扩展槽位乱序及换图/断线重建仍未完成。测试中的 `view_equipment_ui_test`/`netbridge_test` ObjectDB 泄漏也尚未清理。
|
||||
@@ -0,0 +1,122 @@
|
||||
# item.refine_socket_costume
|
||||
|
||||
## Scope
|
||||
|
||||
审计强化窗口、强化请求与回包、普通/魔石/首饰镶嵌、属性添加与洗炼、时装栏位与外观更新,以及当前端额外的幻化/衣柜逻辑。判断标准不是本地字典能否算出相同结果,而是是否复现 40250 的物品原型校验、请求协议、服务器权威结果、`GC_ITEM_*` 状态更新、角色部件刷新、特效/资源和失败清理链。
|
||||
|
||||
## Reference call chain
|
||||
|
||||
- `UserInterface/PythonNetworkStreamPhaseGame.cpp` / `PythonNetworkStreamModule.cpp`
|
||||
- `SendRefinePacket` 发送 `CG_REFINE(pos,type)`;40250 取消强化使用 `(255,255)`,写包后进入序列发送。
|
||||
- `RecvRefineInformationPacket` 解析旧版 `GC_REFINE_INFORMATION(95)`;`RecvRefineInformationPacketNew` 解析新版 `GC_REFINE_INFORMATION_NEW(119)`,两者都把服务端给出的源物品、结果 vnum、费用、概率和材料交给 `OpenRefineDialog`。
|
||||
- 强化信息由服务器提供,客户端不拥有强化成功率、结果 vnum、费用或材料公式。
|
||||
- `UserInterface/Packet.h` / `GameLib/ItemData.cpp` / `PythonItemModule.cpp`
|
||||
- 旧、新强化包的 wire layout 不同;`ITEM_USE` 子类型包括 `USE_TUNING`、`USE_CLEAN_SOCKET`、`USE_CHANGE_ATTRIBUTE`、`USE_ADD_ATTRIBUTE`、`USE_ADD_ATTRIBUTE2`、`USE_ADD_ACCESSORY_SOCKET`、`USE_PUT_INTO_ACCESSORY_SOCKET` 等。
|
||||
- ItemData 提供 `GetSocket`、`GetSocketCount`、`GetRefine`、`GetRefinedVnum`、`GetValue`、flags/anti-flags、装备类型和外观资源;客户端用这些信息做 UI/目标校验,不替代服务端物品变更。
|
||||
- `UserInterface/PythonPlayer.cpp` / `PythonPlayerModule.cpp` / `PythonNetworkStreamPhaseGameItem.cpp`
|
||||
- `SetItemData` 对非零 vnum 先验证 ItemManager;`SetItemCount`、socket、attribute 按服务器回包写入本地物品,并刷新窗口。
|
||||
- `CanRefine`、`CanAttachMetin` 等只依据 ItemManager 的真实 type/subtype/flag/socket 数据判断允许与否,然后由物品使用协议请求服务端执行。
|
||||
- 物品更新顺序是服务器回包驱动的 count、socket、attribute 与窗口刷新,不是 UI 或脚本直接扣除材料。
|
||||
- `UserInterface/InstanceBase.cpp` / `PythonNetworkStreamPhaseGameActor.cpp`
|
||||
- 角色的 armor、weapon、hair 部件来自角色同步包;`SetArmor` 通过 ItemData 映射模型、specular 和强化特效,`SetHair`/weapon attachment 刷新实际角色模型。
|
||||
- `__GetRefinedEffect` 按 ItemData 的 refine/socket/type/subtype 选择并挂接/清理强化特效。
|
||||
- `Client/Eternexus/root/uirefine.py` / `uiinventory.py`
|
||||
- 强化窗口只负责显示服务器给出的 refine table、材料库存和警告;确认/取消均发送网络请求。属性、清孔、首饰孔和物品使用入口同样依赖 `item_proto` 的 use type 与服务器 `GC_ITEM_*` 回包。
|
||||
- 服务器权威位于 `Server/metin2/src/server/game/src/char_item.cpp`、`item.cpp`、`item_attribute.cpp` 及 `item_proto`。这些文件决定属性池、概率、socket/强化限制、消耗和最终 item data;客户端源码没有对应的独立权威算法。
|
||||
|
||||
## Current call chain
|
||||
|
||||
- `project/ui/refine_ui.gd` 已接收 `M2Client.refine_ask`,显示材料/概率/费用并调用 `M2Client.refine`;`extension/src/net/classic/classic_session.cpp` 已能构造 `CGRefine`,`EntityStore` 已有 `RefineCue`。
|
||||
- `extension/src/net/m2_client.cpp::refine` 只允许 `0 <= pos < INVENTORY_MAX_NUM`,所以 `RefineUI.cancel_refine()` 发出的 40250 取消哨兵 `255,255` 在 native 层直接返回 false,未到达服务器。
|
||||
- `extension/src/net/classic/classic_parser.cpp` 已分别解析旧版 `GCRefineInfoOld`(95,59 字节,无 type)和新版 `GCRefineInfo`(119,60 字节,有 type),并将两者送入 `EntityStore::mut_refine`;旧版 wire layout 本身已补齐,但旧版真实服务器回包和类型默认值仍缺端到端 fixture。
|
||||
- `project/item_attr_system.gd`、`item_enchant_system.gd` 直接在传入 `Dictionary/Array` 上生成/重置普通和稀有属性并扣本地卷轴;没有调用 `M2Client::use_item_to_item`、等待 `GC_ITEM_UPDATE` 或从 ItemManager 读取真实 use subtype/attribute set。
|
||||
- `project/jewelry_socket_system.gd` 直接用硬编码 vnum、首饰 vnum 前缀、成功率、寿命和 bonus tier 修改 sockets/gem_times;没有通过 40250 的 `ITEM_USE`/`USE_ADD_ACCESSORY_SOCKET`/`USE_PUT_INTO_ACCESSORY_SOCKET` 请求和服务器更新闭环。
|
||||
- `project/costume_system.gd` 维护独立的 `equipped_costumes`,复制传入时装、默认本地 7 天计时并计算 bonus/model_id;未发现它接入 `M2Client`、EntityStore 的 equipment 回包或 `project/ui/equip_model.gd` 的角色部件更新。
|
||||
- `project/costume_attr_transfer_system.gd` 和 `project/wardrobe_transmog_system.gd` 直接修改库存、金币、属性和 `transmog_*` 字段。现有 40250 客户端源码中没有与 `wardrobe_transmog_system.gd` 对应的幻化协议/角色字段,因此它是当前端自定义扩展,不应标记为 40250 已实现。
|
||||
- `project/refine_effect_system.gd` 有本地强化光效规格和粒子测试,但尚未证明由权威装备回包触发,且当前模型/角色路径与 40250 的 `InstanceBase` 部件、ItemData 和 effect attach 链未形成端到端测试。
|
||||
|
||||
## Branch matrix
|
||||
|
||||
| 场景 | 40250 | 当前端 | 结论 |
|
||||
|---|---|---|---|
|
||||
| 打开强化窗口 | 服务器发送旧/新版 refine info;客户端按 packet layout 交给 UI | native parser 已分别接受 95/119 并生成 cue | PARTIAL:旧版真实回包与 UI 行为缺 fixture |
|
||||
| 强化确认 | UI 发 `CG_REFINE(pos,type)`,结果和消耗由服务器回包决定 | 正常 pos/type 可发 `CGRefine` | PARTIAL:缺完整成功/失败后的 item/point/effect 顺序证明 |
|
||||
| 强化取消/超距/ESC | 发送 `CG_REFINE(255,255)` | `RefineUI` 调用 `refine(255,255)`,native 范围校验先拒绝 | GAP |
|
||||
| 强化数据来源 | `src_vnum/result_vnum/cost/prob/materials` 全部服务端提供 | `RefineCue` 能保存字段,窗口能显示 | MAPPED + tests:旧包与真实回包覆盖不足 |
|
||||
| 普通/魔石/首饰镶嵌 | ItemManager use subtype/flag/socket 校验后发物品使用请求,服务器改 item data | 本地系统按字典直接改 socket/attrs | GAP |
|
||||
| 属性添加/洗炼/第五属性 | 客户端显示 ItemData/回包,成功率和属性生成在服务器 `char_item.cpp/item_attribute.cpp` | 本地硬编码池/概率并直接写 attrs | GAP:算法复制不等于客户端协议实现 |
|
||||
| 稀有属性 6/7 条 | 在当前 40250 客户端引用中未找到与当前 71151/71152 本地系统相同的完整客户端链 | `ItemEnchantSystem` 本地新增稀有池并直接扣卷轴 | CUSTOM/PARTIAL,不能宣称 40250 1:1 |
|
||||
| 时装穿戴/卸下 | 角色同步包给 armor/weapon/hair 部件,ItemData/InstanceBase 刷新模型与强化效果 | `CostumeSystem` 只更新本地字典和信号;未接 actor/model 回包 | GAP |
|
||||
| 时装属性/限时 | 服务器 item data/affect/point 回包决定状态 | 本地 `bonuses` 与 `remaining_time` 逐帧扣减 | PARTIAL:缺权威计时、point/affect 和过期回包 |
|
||||
| 幻化/衣柜 | 40250 客户端源码未找到相同协议或 `transmog_*` 角色部件链 | 本地直接扣金币/卷轴/皮肤并改字段 | CUSTOM,不属于已验证的 40250 parity |
|
||||
| 强化外观效果 | `InstanceBase::__GetRefinedEffect` 依 ItemData 的 refine/socket/type/subtype attach/clear effect | 本地 `RefineEffectSystem` 可计算规格并创建粒子 | PARTIAL:资源、部件更新和清理未闭环 |
|
||||
|
||||
## Implementation-equivalence matrix
|
||||
|
||||
| 项目 | 状态 | 证据/限制 |
|
||||
|---|---|---|
|
||||
| Preconditions | PARTIAL | refine 基础 pos 校验存在;取消哨兵被错误拒绝;属性/socket/costume helper 不统一使用 ItemManager 的 type/subtype/flag/anti-flag/equipment slot 校验 |
|
||||
| Branch structure | PARTIAL | 强化 UI 分支和 native 请求存在;本地属性、镶嵌、时装操作绕过 `CG_ITEM_USE`/`GC_ITEM_UPDATE`,并增加了参考端不存在的幻化分支 |
|
||||
| Algorithms/formulas | PARTIAL | 40250 的强化、属性、socket 结果由服务器权威计算;当前本地池/概率/寿命只通过孤立测试,不能证明与运行时服务端一致 |
|
||||
| State transition order | GAP | 参考是请求→服务器判定→`GC_ITEM_*`/point/affect/actor update→刷新;当前多个 helper 是本地立即写入和发 signal |
|
||||
| Constants/units | PARTIAL | refine packet header/字段和部分 vnum 已定位;socket 前缀、gem tier、时装 7 天、幻化 1000 万等硬编码未由当前 40250 运行时数据链证明 |
|
||||
| Timing/event sources | PARTIAL | RefineCue/signal/UI 事件存在;旧包、取消包、真实 item update/actor update 顺序和过期竞态未覆盖 |
|
||||
| Resource/data sources | PARTIAL | 当前可读部分 item proto/模型数据;属性池、socket 规则和 costume model_id 多来自脚本常量,未接 40250 ItemData/资源选择链 |
|
||||
| Protocol side effects | PARTIAL | `CGRefine` 和基础 refine cue 存在;取消 sentinel、旧 refine header、item-use/socket/costume request、SendSequence/失败反馈未完整闭合 |
|
||||
| Interruption/failure/cleanup | GAP | 本地操作成功前就能改变库存/金币/attrs/sockets;网络拒绝、重复回包、断线、换图、时装过期和 actor model 清理没有同一权威回滚路径 |
|
||||
|
||||
## Regression evidence
|
||||
|
||||
- `project/test_refine_parity.gd`:通过,覆盖窗口显示、材料颜色、确认/取消 UI 规则和超距行为;使用 fake client,未证明 native `255,255` 取消包真实发出,也未覆盖旧版 95 包。
|
||||
- `project/test_refine_effect_parity.gd`:25/25 通过,覆盖本地强化等级和粒子规格;未覆盖 40250 `InstanceBase` 真实 ItemData 部件更新、attach/clear 和资源加载失败。
|
||||
- `project/test_item_attr_parity.gd`:28/28 通过;`project/test_item_enchant_manager_parity.gd`:36/36 通过。两者都直接对 `Dictionary` 做本地生成/扣除,未启动服务器或消费 `GC_ITEM_UPDATE`。
|
||||
- `project/test_jewelry_socket_parity.gd`:23/23 通过;覆盖本地打孔/镶嵌/寿命计算,但没有 40250 物品使用协议、ItemData socket count、服务器拒绝或回包。
|
||||
- `project/test_costume_system_parity.gd`:23/23 通过;`project/test_costume_attr_transfer_parity.gd`:23/23 通过;`project/test_wardrobe_transmog_parity.gd`:22/22 通过。它们验证本地 helper 的字典和 signal 行为,不能证明角色模型、服务端库存/金币或 40250 客户端协议等价。
|
||||
- native/refine 现有测试覆盖 `CGRefine`/cue 基础布局,但未覆盖 `GC_REFINE_INFORMATION(95)` 与 `(119)` 双版本解析、`255,255` 取消发送和 refine 后 item/point/affect/actor 更新顺序。
|
||||
|
||||
## Deep audit round 2026-09-21
|
||||
|
||||
### Fresh regression results
|
||||
|
||||
- `godot --headless --path project --script test_refine_parity.gd`:退出码 0;强化窗口/材料/确认/取消 UI 规则通过,但 fake client 不能证明 native 取消包真正发出。
|
||||
- `godot --headless --path project --script test_refine_effect_parity.gd`:退出码 0,25/25 通过;只证明本地强化等级到粒子规格的映射。
|
||||
- `godot --headless --path project --script test_item_attr_parity.gd`:退出码 0,28/28 通过;只证明本地属性字典算法。
|
||||
- `godot --headless --path project --script test_item_enchant_manager_parity.gd`:退出码 0,36/36 通过;只证明本地普通/稀有属性管理器的直接修改路径。
|
||||
- `godot --headless --path project --script test_jewelry_socket_parity.gd`:退出码 0,23/23 通过;只证明本地首饰打孔、镶嵌和寿命计算。
|
||||
- `godot --headless --path project --script test_costume_system_parity.gd`、`test_costume_attr_transfer_parity.gd`、`test_wardrobe_transmog_parity.gd`:均退出码 0,分别 23/23、23/23、22/22 通过;验证的是本地时装/属性转移/幻化字典状态,不是服务器权威链路。
|
||||
- `godot --headless --path project --script guild_refine_test.gd`:退出码 0,确认 UI 取消会调用 `client.refine(255,255)`。
|
||||
- `./build/extension/net_classic_session_test`、`./build/extension/net_entity_test`:均退出码 0;native 基础 refine/DS refine 包和新版 cue 通过。源码复核确认 parser 已分别支持旧版 95(59 字节)与新版 119(60 字节)。
|
||||
|
||||
### Static parity findings
|
||||
|
||||
- 当前明确的协议缺陷仍是 `M2Client::refine` 要求 `0 <= pos < INVENTORY_MAX_NUM`;因此 `RefineUI.cancel_refine()` 发出的 40250 取消哨兵 `(255,255)` 在 native 层被拒绝,`ClassicSession::send_refine` 虽可编码该字节,但 UI 入口到不了它。
|
||||
- 旧/新版 refine parser 已经分别存在,先前“旧版 95 包被统一结构丢弃”的判断已纠正;尚缺旧版实际字节 fixture、旧包 `type=0` 对 UI 分支的行为和成功/失败后的 item/point/actor 更新时序。
|
||||
- 40250 的属性、socket、装备外观和时装穿戴依赖 ItemManager/ItemData + `CG_ITEM_USE`/`CG_ITEM_USE_TO_ITEM` + 服务器 `GC_ITEM_*`/affect/point/character parts;当前 `ItemAttrSystem`、`ItemEnchantSystem`、`JewelrySocketSystem`、`CostumeSystem` 仍能直接修改传入字典、金币、socket、attributes 或本地计时。
|
||||
- 当前 `CostumeSystem` 的 bonus/model/7 天过期以及 `wardrobe_transmog_system.gd` 的金币/卷轴扣除都没有连接 EntityStore equipment/point/affect 回包;幻化分支在 40250 源码中没有对应协议,应继续标记为 CUSTOM,而不是 parity 已实现。
|
||||
- `EquipModel` 存在由 equipment 状态刷新强化光效的路径,但本轮没有证明 refine 后 `GC_ITEM_UPDATE` 与 character part/模型资源更新按 40250 顺序触发,资源失败和清理也未闭合。
|
||||
|
||||
### Round conclusion
|
||||
|
||||
本轮仍为 `PARTIAL`:旧/新版 refine 解析已确认存在,强化/DS 基础包和局部 UI 测试通过;但取消 sentinel 是明确失败点,本地属性/镶嵌/时装/幻化旁路仍绕过服务器权威结果,真实 item/point/affect/actor 更新和失败清理尚未与 40250 统一。本轮未修改实现代码。
|
||||
|
||||
## Next audit round 2026-09-20T18:19Z
|
||||
|
||||
本轮对强化/镶嵌/时装链进行一次入口级复核,重点验证 UI 认为“已发出”的请求是否真的能穿过 native 层:
|
||||
|
||||
- `RefineUI.cancel_refine()` 按 40250 发送 `client.refine(255, 255)`,但 `M2Client::refine`(`extension/src/net/m2_client.cpp:2351-2356`)要求 `pos >= 0 && pos < INVENTORY_MAX_NUM`,因此取消请求在 native 入口直接返回 `false`;`ClassicSession::send_refine` 的字节编码能力无法弥补这个前置门。现有 `test_refine_parity.gd` 使用 fake client,不能捕获该失败。
|
||||
- `RecvRefineInformationPacket`(95)和 `RecvRefineInformationPacketNew`(119)均已在 parser 中存在,本轮没有把此前的“旧包完全缺失”继续列为事实;但旧版真实字节 fixture、`type=0` 的 UI 分支和强化后 `GC_ITEM_UPDATE`/point/affect/actor 更新顺序仍没有证据。
|
||||
- `ItemAttrSystem`、`ItemEnchantSystem`、`JewelrySocketSystem`、`CostumeSystem` 仍直接修改传入字典、属性、socket、金币或本地剩余时间;40250 客户端对应入口只做 ItemData 校验并发 `CG_ITEM_USE`/`CG_ITEM_USE_TO_ITEM`,结果由服务器回包驱动。局部脚本测试通过不能证明这两条链统一。
|
||||
|
||||
本轮没有修改实现代码。合同继续为 `PARTIAL`,其中取消 sentinel 是可直接复现的协议级差异,优先级高于继续增加本地属性公式测试。
|
||||
|
||||
## Status
|
||||
|
||||
`PARTIAL`。强化窗口已有可用的新版协议骨架和本地 UI 回归;属性、socket、时装和幻化的本地脚本测试通过,实际实现仍主要是本地字典状态机,尚未复现 40250 的 ItemManager→CG 请求→服务器权威结果→GC item/actor/model 更新完整链路。
|
||||
|
||||
## Implementation fix round 2026-09-21
|
||||
|
||||
- `mtnet::bounds::refine_request` 与 `M2Client::refine` 现在保留普通库存位置校验,同时按 40250 接受强化取消哨兵 `(255,255)`;`net_bounds_test` 覆盖普通位置、哨兵和非法混合值。
|
||||
- `test_refine_parity.gd`/`guild_refine_test.gd` 仍证明 UI 会发出取消调用;native 回归现在证明该调用不会在入口被错误拦截。真实服务器回包、旧版 95 字节 fixture 和强化后权威 item/point/actor 顺序仍待补齐。
|
||||
- `EntityStore` 的正 HP 点数更新会清除 `dead`,`M2Client` 会重置 `dead_seen`,保证复活后下一次死亡可以再次发出 `entity_dead`。
|
||||
|
||||
本合同继续为 `PARTIAL`:本轮只修复了可直接验证的协议边界和死亡状态边沿,没有把本地属性/镶嵌/时装旁路误标为 40250 等价实现。
|
||||
@@ -0,0 +1,181 @@
|
||||
# lifecycle.bootstrap_phase_host
|
||||
|
||||
## Scope
|
||||
|
||||
对比 40250 的应用启动、单帧处理顺序、网络阶段属主、Loading 入口清理、DEAD/CLOSE、窗口焦点/后台和最终销毁,与当前 Godot `client_main`、`AppFlow`、`AppLifecycle`、`M2Client` 和场景树行为。
|
||||
|
||||
## Reference call chain
|
||||
|
||||
### 启动与每帧
|
||||
|
||||
- `UserInterface/PythonApplication.cpp::Run`
|
||||
- `UserInterface/PythonApplication.cpp::Process`
|
||||
1. `ELTimer_SetFrameMSec` / `CTimer::Advance`
|
||||
2. `CPythonNetworkStream::Process`
|
||||
3. guild mark uploader/downloader、account connector
|
||||
4. keyboard/mouse input
|
||||
5. camera、resource manager、camera update、mouse update、UI update
|
||||
- `UserInterface/PythonApplicationEvent.cpp`
|
||||
- `UserInterface/PythonApplicationProcedure.cpp`
|
||||
|
||||
### 阶段属主与清理
|
||||
|
||||
- `UserInterface/PythonNetworkStream.cpp::RecvPhasePacket`
|
||||
- `PHASE_CLOSE -> ClosePhase -> SetLoginPhase`
|
||||
- `PHASE_HANDSHAKE -> SetHandShakePhase`
|
||||
- `PHASE_LOGIN -> SetLoginPhase`
|
||||
- `PHASE_SELECT -> SetSelectPhase`
|
||||
- `PHASE_LOADING -> SetLoadingPhase`
|
||||
- `PHASE_GAME -> SetGamePhase`
|
||||
- `PHASE_DEAD` 只消费阶段包,不改变网络阶段
|
||||
- `UserInterface/PythonNetworkStreamPhaseLogin.cpp::SetLoginPhase`
|
||||
- `UserInterface/PythonNetworkStreamPhaseSelect.cpp::SetSelectPhase`
|
||||
- `UserInterface/PythonNetworkStreamPhaseLoading.cpp::SetLoadingPhase`
|
||||
- `CPythonPlayer::Clear`
|
||||
- `CFlyingManager::DeleteAllInstances`
|
||||
- `CEffectManager::DeleteAllInstances`
|
||||
- direct-enter 状态初始化
|
||||
- `UserInterface/PythonNetworkStreamPhaseGame.cpp::SetGamePhase`
|
||||
- `UserInterface/PythonNetworkStreamPhaseGame.cpp::__LeaveGamePhase`
|
||||
- PVP key、network actor、combo flag、character manager、item manager
|
||||
- `UserInterface/PythonNetworkStreamPhaseGameActor.cpp::__ClearNetworkActorManager`
|
||||
|
||||
### 关闭与销毁
|
||||
|
||||
- `EterLib/MSApplication.cpp::MessageProcess`
|
||||
- `EterLib/MSWindow.cpp::WindowProcedure/Destroy`
|
||||
- `UserInterface/PythonApplicationProcedure.cpp::WindowProcedure`
|
||||
- `WM_ACTIVATEAPP`: 保存/恢复音量;全屏窗口最小化/恢复
|
||||
- `WM_CLOSE`: debug 退出,release 走 `RunPressExitKey`
|
||||
- `UserInterface/PythonApplication.cpp::Destroy`
|
||||
- UI、事件、飞行物、角色、物品、背景、特效、网络、资源、声音、设备按显式顺序销毁
|
||||
|
||||
## Current call chain
|
||||
|
||||
### 启动与每帧
|
||||
|
||||
- `project/client_main.gd::_ready`
|
||||
- `AssetPack.ensure`
|
||||
- 创建 `AppFlow`
|
||||
- `project/app_flow.gd::start`
|
||||
- 创建并挂载一个 `M2Client`
|
||||
- 创建 `LoadingScreen`、`AppLifecycle`、`ReconnectUI`
|
||||
- 进入 Login
|
||||
- `extension/src/net/m2_client.cpp::_process/net_poll`
|
||||
- 依次 pump classic/auth/game/mark 连接
|
||||
- classic 侧再转发 `EntityStore` 变化和 Godot signals
|
||||
- `project/game_scene.gd::_process`
|
||||
- 主角同步、诊断、音频 listener/环境音
|
||||
- `project/app_flow.gd::_goto_game`
|
||||
- 以协程构造 GameScene;等待模型时只让出帧,不夺取 `M2Client._process` 的网络 pump owner
|
||||
- `project/login.gd`
|
||||
- 保留一条独立的 SceneTree 入口:自行创建 `AppLifecycle`、`M2Client`、登录层和 `GameScene`
|
||||
- 不经过 `client_main.gd -> AppFlow`,因此与主入口不是同一套阶段、关闭和销毁属主
|
||||
|
||||
### 阶段属主与清理
|
||||
|
||||
- `extension/src/net/classic/classic_session.cpp::on_phase`
|
||||
- LOGIN/SELECT/LOADING/GAME 映射到 `INetSession::Stage`
|
||||
- `PHASE_CLOSE` 直接映射为 `Stage::Failed`
|
||||
- `PHASE_DEAD` 没有 case,仅被前置的 time-sync 判断覆盖,未改变 stage
|
||||
- `extension/src/net/m2_client.cpp::pump_classic`
|
||||
- Stage 变化才发 `phase_changed`
|
||||
- stage 表只有 close/handshake/login/select/loading/in_game/failed,没有 dead
|
||||
- `EntityStore` 的实体、死亡、掉落、地图 warp 变化在同一 pump 中派发
|
||||
- `project/ui/loading_screen.gd`
|
||||
- 只按 `phase_changed` 显示/隐藏 Loading 遮罩
|
||||
- `project/game_scene.gd::_on_world_reset`
|
||||
- 清 NetPlay、NetWorld、掉落、弹道、临时 UI 和 BGM
|
||||
- 由 `M2Client` 的 classic `on_loading_phase` 在保留旧 world 时触发;空 world 或已由 GC_WARP 预清理的连接不重复触发
|
||||
- `project/net_play.gd::clear_for_map_change`
|
||||
- `project/net_world.gd::clear_for_map_change`
|
||||
- `extension/src/net/entity_store.cpp::reset_for_map_change`
|
||||
|
||||
### 窗口与后台
|
||||
|
||||
- `project/app_lifecycle.gd::_notification`
|
||||
- application paused/resumed 时保存/静音/恢复音频、暂停/恢复 `M2Client` 和 SceneTree
|
||||
- focus 只发 `focus_changed`,没有 40250 的音量/全屏处理
|
||||
- `NOTIFICATION_WM_CLOSE_REQUEST` 只发 `close_requested`
|
||||
- `AppFlow` 与旧 `login.gd` 分别消费各自 `AppLifecycle.close_requested`,关闭时按网络→音频解绑→场景释放→进程退出顺序协调;两条入口仍不是同一个启动拓扑。
|
||||
|
||||
### 销毁
|
||||
|
||||
- `AppFlow::_goto_login` 对 GameScene/UI 使用 `queue_free`
|
||||
- `M2Client::~M2Client` 为 default;连接由成员 RAII 析构关闭
|
||||
- 未发现与 40250 `CPythonApplication::Destroy` 对应的、可观测且有顺序保证的网络/实体/特效/UI/音频总清理函数
|
||||
|
||||
## Branch matrix
|
||||
|
||||
| 场景 | 40250 | 当前端 | 结论 |
|
||||
|---|---|---|---|
|
||||
| 启动/正常帧 | 固定在 `Process` 中先计时、网络,再输入、资源、相机和 UI | Godot 由 `M2Client._process` 持有网络 pump;`_goto_game` 等待模型只 yield 并同步视图 | PARTIAL:跨节点顺序和模型构建期间的完整事件时序仍未证明一致 |
|
||||
| 主入口/旧入口 | `CPythonApplication` 只有一个启动和销毁拓扑 | `client_main.gd -> AppFlow` 与旧的 `login.gd` 各自创建生命周期、客户端和游戏场景 | GAP:主入口测试不能证明旧入口也遵守单一生命周期属主 |
|
||||
| LOGIN/SELECT | `RecvPhasePacket` 作为唯一阶段 owner,阶段切换先执行旧 phase leave,再安装新 process/leave 回调 | `ClassicSession::Stage`、`M2Client` signals、`AppFlow` 三层分担 | PARTIAL:状态 owner 不唯一,乱序和重复 phase 规则未统一 |
|
||||
| SELECT -> LOADING | `SetLoadingPhase` 先切 phase,再清 player、飞行物、特效和 direct-enter 状态 | classic `on_loading_phase` 在阶段回调内先清旧 `EntityStore` 并发 `world_reset`,再继续 Loading;空 world 直接跳过 | PARTIAL:NetPlay/NetWorld/特效的完整 owner 顺序和真实 server 包序仍待补齐 |
|
||||
| GAME -> DEAD | 40250 的 `PHASE_DEAD` 本身不改 phase,死亡包在 Game phase 内走 GameOver/角色死亡链 | `ClassicSession` 不发 dead phase;`DeathUI` 现在只从 `GC_DEAD -> entity_dead` 进入,和 40250 的死亡包入口一致 | PARTIAL:死亡入口已闭环,GameOver/决斗例外、死亡动作和真实包序仍待补齐 |
|
||||
| PHASE_CLOSE | 回调 `SetLoginPhase`,由登录阶段继续处理 | `PHASE_CLOSE -> Stage::Failed`,再发 `login_failed` 或 `disconnected` | GAP:状态和 UI 入口不同 |
|
||||
| GC_WARP | 进入 Loading,显式清旧 actor/物品/特效,再连接/加载新地图 | `EntityStore::reset_for_map_change -> world_reset -> connect_warp`,并由 Loading 回调的空 world guard 避免重复 reset | PARTIAL:NetPlay/NetWorld/特效的完整 owner 顺序和真实 server 包序仍待补齐 |
|
||||
| 断线重连 | old phase leave、连接状态和角色数据按网络 owner 协调 | `ReconnectUI` 负责重试,`M2Client` 重新建/切 session;场景可能保留,world reset 不是统一前置 | PARTIAL;与 `network.reconnect_warp` 交叉记录 |
|
||||
| 窗口失焦/后台 | Windows `WM_ACTIVATEAPP` 保存/恢复音量并处理全屏,主循环仍由应用控制 | focus 仅广播;application pause 会暂停 socket/SceneTree 并静音 | PARTIAL / platform adaptation:行为不是 1:1 |
|
||||
| 窗口关闭 | `WM_CLOSE` 经退出键/退出流程,最终 `Destroy` | `AppFlow`/旧 `login.gd` 唯一消费 `close_requested`,先断开并解绑资源后退出;Windows 退出确认/保存细节仍未证明 | PARTIAL |
|
||||
| 最终销毁 | 明确 manager 顺序、网络/声音/设备显式 Destroy | Godot `queue_free` + C++ default destructor/RAII | PARTIAL:资源最终可能释放,但副作用顺序与可观测状态未等价 |
|
||||
|
||||
## Equivalence matrix
|
||||
|
||||
| 项目 | 状态 | 证据 |
|
||||
|---|---|---|
|
||||
| Preconditions | PARTIAL | 当前入口有资源、扩展和 serverlist 检查,但没有统一阶段/关闭前置条件 |
|
||||
| Branch structure | PARTIAL | LOGIN/SELECT/LOADING/GAME 已映射;DEAD、CLOSE、窗口关闭和重复启动/关闭缺统一分支 |
|
||||
| Algorithms/formulas | MAPPED | 主循环没有业务公式;计时和网络 pump owner 不同 |
|
||||
| State transition order | PARTIAL | 参考由 `RecvPhasePacket + phaseLeaveFunc` 单 owner 串行;当前由 session/stage/signal/AppFlow 分散推进 |
|
||||
| Constants/units | PARTIAL | 1500ms enter-game 等已有实现,但 frame/timer、Loading 清理和重连窗口未证明相同 |
|
||||
| Timing/event sources | PARTIAL | 当前有 Godot `_process`、signal、协程和模型同步;网络 pump 已收敛到 M2Client owner,参考是 Windows message + phase callback |
|
||||
| Resource/data sources | PARTIAL | 当前资源由 scene/RAII 管理;参考是各 manager 显式拥有,且正常 Loading 与 close 清理源不同;`client_main` 与旧 `login.gd` 还存在两套资源/生命周期装配源 |
|
||||
| Protocol side effects | PARTIAL | LOGIN/SELECT/ENTERGAME/warp 已有路径;CLOSE、DEAD 和 loading reset 的副作用不等价 |
|
||||
| Interruption/failure/cleanup | PARTIAL | 断线/warp/关闭消费者已有部分回归;普通重连 reset、重复关闭、socket 失败和 final destroy 的完整顺序仍缺证据 |
|
||||
|
||||
## Evidence and regression boundary
|
||||
|
||||
`project/app_flow_lifecycle_test.gd` 已验证:AppFlow 只创建一个 AppLifecycle、登录/游戏切换不重复创建、游戏断线保留场景并按原槽位重进、显式槽位不存在时不错误自动选人,并且桌面关闭信号有唯一消费者。`project/test_death_restart_parity.gd` 另外验证 `PHASE_DEAD` 不作为死亡面板入口,`GC_DEAD -> entity_dead` 才打开 RestartDialog。`project/gamescene_test.gd` 和 `project/channel_status_test.gd` 只提供场景装配/频道状态旁证。它们没有证明 40250 的固定每帧顺序、Loading 清理、DEAD/CLOSE 的完整副作用、旧 `login.gd` 入口或最终销毁等价,因此本合同不能升级为 MAPPED。
|
||||
|
||||
## Required tests
|
||||
|
||||
- 记录并断言 `M2Client._process`、GameScene/UI `_process` 和 loading 协程的同帧顺序,禁止等待模型时重入 pump 造成重复 phase/entered_game。
|
||||
- LOGIN/SELECT/LOADING/GAME/DEAD/CLOSE 的正常、乱序、重复和断线状态矩阵;特别断言 `PHASE_DEAD` 的消费和 `PHASE_CLOSE` 的目标状态。
|
||||
- SELECT -> LOADING、GC_WARP、普通重连三条路径分别验证 actor、飞行物、特效、NetPlay 动作、掉落、临时 UI、BGM 和 `EntityStore` 的清理顺序。
|
||||
- focus out/in、application pause/resume、重复 pause/resume、窗口 close、网络失败和重复 close 的音频、socket、SceneTree、退出状态测试。
|
||||
- GameScene queue_free、M2Client 析构、旧 session 关闭和再次启动的资源/信号泄漏测试。
|
||||
- 分别从 `client_main.tscn` 和旧 `login.gd` 启动,验证 AppLifecycle、M2Client、GameScene、音频和 close 信号没有重复属主或悬空连接。
|
||||
|
||||
### Active UserInterface application/security review
|
||||
|
||||
- `UserInterface/AbstractApplication.h` 把鼠标、全局时间、服务器时间、中心位置、事件/默认相机和 IME update/tab/return/code-page/candidate/reading callbacks 作为应用宿主接口;这与当前仅由 Godot notification/signal 分散处理不是同一 host contract。
|
||||
- `UserInterface/PythonApplicationModule.cpp` 暴露应用 process/update/render FPS、相机参数、硬件/软件光标、连接数据、文本 loader 和 pack 读取等 Python 边界;`PythonApplicationLogo.cpp`、`PythonApplicationWebPage.cpp`、`MovieMan.cpp` 管理视频/网页/媒体 surface 的创建、更新、跳过、淡出和释放。
|
||||
- `PythonSystem.cpp`/`PythonSystemModule.cpp` 以 D3D adapter/resolution/frequency、窗口状态、音量、阴影级别、保存 ID、配置文件和 interface status 为权威;当前 `AppLifecycle`/系统设置只覆盖 Godot 的一部分状态,Windows display mode、旧配置格式和媒体资源清理没有等价证据。
|
||||
- `ProcessCRC.cpp`/`CheckLatestFiles.h`、`ProcessScanner.cpp`、`PythonExceptionSender.cpp`、`HackShield`/`NProtectGameGuard`/`WiseLogicXTrap` 构成参考端完整性、进程扫描、安全组件和异常上报链。当前客户端没有对应 Windows 安全/CRC/异常服务链;这些是明确的部署能力差异,不应被普通启动测试掩盖。
|
||||
- 本轮完成上述 UserInterface 应用/安全活跃文件静态核对,并修正 DeathUI 对不可达 `phase_changed("dead")` 的错误依赖;当前端基础 AppFlow 可运行,但 bootstrap/关闭/安全/发布合同继续为 `PARTIAL`。
|
||||
|
||||
## Status
|
||||
|
||||
`PARTIAL`。本轮完成了 40250 参考调用链与当前实现调用链的逐分支审计,并修正了 DeathUI 对不可达 `phase_changed("dead")` 的错误依赖、classic Loading 阶段缺少旧 world 清理入口、主入口/旧入口遗漏关闭消费者,以及 `_goto_game` 等待模型时重复夺取网络 pump 的问题;DEAD/CLOSE 其余状态语义、focus/后台、旧入口分叉和显式销毁链仍存在未闭合差异,因此不能宣称已全部按 40250 修复。
|
||||
|
||||
## Implementation round 2026-09-21T04:20Z — close request ownership
|
||||
|
||||
本轮针对 40250 `PythonApplicationProcedure::WindowProcedure(WM_CLOSE)` 和
|
||||
`CPythonApplication::Destroy` 的关闭入口补齐 Godot 两条启动拓扑:
|
||||
|
||||
- `AppFlow.start` 将唯一的 `AppLifecycle.close_requested` 连接到 `_on_close_requested`;处理器具备幂等锁,先调用 `M2Client.disconnect_from_server()`,再解绑生命周期持有的音频、释放 GameScene,最后退出 SceneTree。
|
||||
- 旧 `login.gd` 自己创建 `AppLifecycle`,因此也接入同样的断开/解绑/场景释放/退出顺序,避免只修 client_main 主入口。
|
||||
- `app_flow_lifecycle_test.gd` 新增失败回归:没有消费者时失败,修复后验证 AppFlow 连接了唯一关闭处理器;Godot editor/headless 脚本加载检查通过。
|
||||
|
||||
仍保持 `PARTIAL`:Windows 退出确认与保存细节、真实 socket close 回调顺序、重复 close 与最终 native manager 销毁尚未由运行时 fixture 完整证明。
|
||||
|
||||
## Implementation round 2026-09-21T04:25Z — frame pump ownership
|
||||
|
||||
本轮按 40250 `CPythonApplication::Process` 的单一网络处理 owner 修复进入游戏等待段:
|
||||
|
||||
- 删除 `AppFlow::_goto_game` 等待 `_model_built` 时的直接 `client.net_poll()`;每次循环只同步模型并让出一帧,由 `M2Client._process` 继续负责唯一网络 pump。
|
||||
- 失败回归在 `app_flow_lifecycle_test.gd` 中检查 `_goto_game` 源码段不得重新调用 `client.net_poll()`;修复后生命周期、GameScene 和 world reset 回归均通过。
|
||||
|
||||
仍保持 `PARTIAL`:GameScene 内部模型构建仍有传入 pump callable 的资源加载适配,完整跨节点同帧顺序、阶段重复/乱序包和 entered_game 重入矩阵仍需运行时 fixture。
|
||||
@@ -0,0 +1,108 @@
|
||||
# lifecycle.platform_release —— Windows/桌面窗口、导出、资源自包含、崩溃和发布门禁
|
||||
|
||||
状态:`PARTIAL`
|
||||
|
||||
本合同检查 40250 的 Win32 应用/窗口消息循环、全屏与窗口状态、资源自包含、版本/CRC 检查、异常退出、前后台处理和发布产物。判断标准是目标平台上是否具备相同的启动、窗口、资源、错误处理和退出副作用;“某个平台能导出”不能代替 Windows 40250 等价实现。
|
||||
|
||||
## 40250 参考调用链
|
||||
|
||||
- `EterLib/MSApplication.cpp` / `MSWindow.cpp`
|
||||
- `CMSApplication::Initialize` 保存 HINSTANCE,`MessageLoop` 通过 `GetMessage/TranslateMessage/DispatchMessage` 驱动窗口;`WM_CLOSE` 转为退出消息。
|
||||
- `CMSWindow` 注册 Win32 窗口类,创建 HWND,处理 `WM_SIZE`、`WM_ACTIVATEAPP`、显示/隐藏、窗口/客户区尺寸、鼠标坐标、居中、位置和全屏窗口尺寸。
|
||||
- `UserInterface/PythonApplication.cpp` / `PythonApplicationProcedure.cpp`
|
||||
- 初始化 800×600、60 FPS、相机/渲染/网络/音频/Granny 共享资源,并在非 Debug 模式安装 Eter 异常处理器。
|
||||
- `WM_ACTIVATEAPP` 负责音量 Save/Restore 和全屏显示模式切换;`WM_SIZE`/`WM_EXITSIZEMOVE` 重建 back buffer;`WM_CLOSE` 按构建配置退出或进入退出流程。
|
||||
- 统一处理 IME 消息、鼠标 capture、输入和窗口激活状态,主循环按固定帧/网络/场景更新顺序执行。
|
||||
- `UserInterface/CheckLatestFiles.cpp`
|
||||
- 可选编译 `CHECK_LATEST_DATA_FILES` 时启动低优先级线程,对 `CRC32_inc.h` 中的文件逐个检查可读性和 CRC;错误通过 Application error string 触发退出。
|
||||
- `EterPack/*` / `Locale*`
|
||||
- 运行时从 EterPack/locale 读取资源,并通过版本/资源链保证客户端数据完整;坏资源和版本不匹配有明确失败路径。
|
||||
|
||||
## 当前端实现映射
|
||||
|
||||
- `project/client_main.gd` / `project/app_flow.gd`
|
||||
- 已有 Godot 主场景、AppFlow 状态机、登录→选人→游戏切换、断线重连和测试模式;客户端启动时配置协议、AssetPack 和资源根。
|
||||
- Godot MainLoop 取代 Win32 `CMSApplication`,没有 40250 的 HWND 类注册、Win32 消息分派、back buffer resize 和全屏显示模式切换等价代码。
|
||||
- `project/app_lifecycle.gd`
|
||||
- 已处理 Godot 的暂停/恢复、窗口焦点、内存警告、Android 返回和桌面关闭信号,后台降 FPS 并绑定客户端/音频。
|
||||
- 窗口失焦/重新获得焦点现在复用后台入口,分别执行音量保存、BGM/SFX 静音和恢复;关闭请求目前主要发信号,仍未形成与 40250 `WM_CLOSE`/`PostQuitMessage`/退出清理一致的统一出口。
|
||||
- `project/asset_root.gd` / `asset_pack.gd` / `build-macos-client.sh` / `build-android.sh` / `project/export_presets.cfg`
|
||||
- macOS 脚本将 assets/bgm 复制到 app Resources,Android 使用 `assets.zip`/`asset_index.txt`,AssetRoot 支持环境变量、散文件、包内资源和可执行文件旁资源。
|
||||
- 当前没有 Windows 导出预设或 Windows GDExtension 构建脚本;资源链也不是 EterPack 的原生 eix/epk/CRC/更新流程,移动端还依赖外部 zip。
|
||||
- `oracle/*`
|
||||
- 有 Windows/Wine Granny 对拍工具和文档,但它是测试 oracle,不是客户端发布/崩溃/版本链。
|
||||
|
||||
## 分支等价矩阵
|
||||
|
||||
| 40250 判据/副作用 | 当前实现 | 结论 |
|
||||
|---|---|---|
|
||||
| Win32 HINSTANCE/HWND、窗口类、消息循环和 WM_CLOSE | Godot MainLoop/Window | 平台适配;没有 Windows 40250 同链实现,Windows 发布未闭合 |
|
||||
| WM_SIZE/EXITSIZEMOVE、back buffer resize、窗口/客户区尺寸 | Godot 窗口系统自动处理,脚本未提供等价回调证据 | 部分等价;缺 Windows 原生尺寸/渲染设备恢复测试 |
|
||||
| 前后台激活、全屏显示模式切换和音量保存恢复 | AppLifecycle 对 focus/pause 统一进入/退出后台,保存并恢复 BGM/SFX;全屏切换由 Godot 配置承担 | 部分等价;焦点音量副作用已有回归,Win32 全屏显示模式分支仍缺失 |
|
||||
| 初始化/固定帧/网络/渲染/退出顺序 | AppFlow/GameScene/Godot scene tree 已有状态机 | 部分等价;没有与 `CPythonApplication::Process` 更新顺序和异常中止路径的生产级证据 |
|
||||
| Eter 异常处理、Abort/Exit 和错误退出 | Godot 日志/进程退出,未发现专用异常处理器和 crash report | GAP;崩溃收集、错误字符串和资源回收链未对齐 |
|
||||
| CheckLatestFiles CRC/可读性/低优先级线程更新检查 | 没有对应版本/CRC 更新线程 | GAP |
|
||||
| EterPack/locale 自包含和坏资源失败回退 | AssetRoot/AssetPack/外部 assets.zip/手工复制 | 部分等价;资源来源可用,但原生 pack、版本校验、坏包/缺包门禁和 Windows 路径未闭合 |
|
||||
| Windows 导出、GDExtension、资源自包含和启动验证 | 只有 macOS/Android 导出预设;macOS 导出探针通过 | GAP(Windows);其它平台基础导出通过 |
|
||||
| 应用关闭、场景释放、音频/网络/模型清理 | queue_free、AppFlow 状态切换和局部生命周期测试 | 部分等价;没有 Windows 关闭消息、崩溃中止和整包资源释放门禁 |
|
||||
|
||||
## 已执行测试
|
||||
|
||||
- `project/app_flow_lifecycle_test.gd`:通过;覆盖 AppFlow 单一生命周期属主、登录/游戏断线重连和场景状态摘要。
|
||||
- `project/bgm_test.gd`:通过;覆盖窗口失焦/获得焦点时 BGM/SFX 保存、静音和恢复;修复前该回归有 2 项失败。
|
||||
- `project/dds_lifecycle_test.gd`:通过;20 次 DDS 解码后图像仍有效且节点计数不变。
|
||||
- `godot --headless --path project --editor --quit`:退出码 0,脚本/GDExtension/项目结构校验完成;过程自动补建若干缺失 `.uid` 缓存文件,未发现阻断错误。
|
||||
- `godot --headless --path project --export-release macOS /tmp/mtgodot-platform-audit.app`:退出码 0,macOS 导出流程通过;该结果不代表 Windows 导出可用。
|
||||
|
||||
## Deep audit round 2026-09-21
|
||||
|
||||
### Fresh regression results
|
||||
|
||||
- `app_flow_lifecycle_test.gd`、`dds_lifecycle_test.gd` 均退出码 0;覆盖生命周期属主和 DDS 资源解码后对象稳定性。
|
||||
- `godot --headless --path project --editor --quit` 退出码 0,但编辑器自动重建了缺失的 `view_equipment_ui_test.gd.uid`,且在无 Android daemon 时出现连接拒绝提示;这不是 Windows 发布验证。
|
||||
- 当前没有 Windows export preset/GDExtension 构建脚本;本轮没有把 macOS/Android 结果替代 Windows 发布通过。
|
||||
|
||||
### Static parity conclusion
|
||||
|
||||
- Godot MainLoop/AppLifecycle 能覆盖跨平台基础生命周期,且焦点音量保存/恢复已对齐 `WM_ACTIVATEAPP` 的可观察副作用,但没有 40250 Win32 HWND/WM_SIZE/WM_CLOSE、Eter 异常处理、CRC 更新检查和统一崩溃/退出链。
|
||||
- 资源合同确认的 PCK/zip/loose 入口和当前自定义 pack 不能直接证明 Windows 自包含资源、坏包/缺包阻断、版本校验和路径大小写行为。
|
||||
- 未发现可执行的 Windows 导出、Windows GDExtension 和启动/连接/地图/音频/退出冒烟门禁,因此平台发布合同继续保持 PARTIAL。
|
||||
|
||||
本轮仍为 `PARTIAL`,完成焦点激活链的实现和第三轮平台生命周期证据登记。
|
||||
|
||||
### Implementation fix round 2026-09-22T09:10Z
|
||||
|
||||
- 修复前新增的 `bgm_test.gd` 失焦回归出现 2 项失败:`WM_WINDOW_FOCUS_OUT` 不会静音 BGM/SFX,重新获得焦点也没有恢复备份音量。
|
||||
- 参考 `CPythonApplication::WindowProcedure` 的 `WM_ACTIVATEAPP` 分支,`AppLifecycle._notification` 现在在 `APPLICATION_FOCUS_OUT`/`WM_WINDOW_FOCUS_OUT` 调用 `_enter_background()`,在对应 `*_FOCUS_IN` 调用 `_exit_background()`;`_bg` 守卫避免同时收到 pause/focus 通知时重复保存或恢复。
|
||||
- 修复后 `bgm_test.gd`、`app_flow_lifecycle_test.gd`、`mouse_controller_test.gd`、`audio_miles_test.gd` 通过。全屏显示模式、WM_SIZE/back-buffer、Windows 导出和 Win32 异常/CRC 链仍为平台级未闭合项。
|
||||
|
||||
### Active EterLib device-core review
|
||||
|
||||
- `EterLib/GrpDevice.cpp`、`GrpScreen.cpp`、`GrpShadowTexture.cpp` 负责 D3D 设备/交换链创建、back buffer、lost/reset、屏幕尺寸和阴影纹理资源;这些不是单纯由窗口系统自动替代的 UI 行为,而是和渲染资源、全屏切换、设备丢失恢复绑定的状态机。
|
||||
- 当前 Godot renderer 由引擎托管,`AppLifecycle` 只收到焦点/暂停/窗口通知,没有对应的设备创建、lost-device、reset、back-buffer 和 shadow texture 重建回调,也没有 Windows 目标上的验证矩阵。
|
||||
- 因此本轮完成上述活跃 EterLib 图形设备源码静态核对;Godot editor/macOS 导出通过不能证明 Windows D3D/窗口/设备恢复一致,平台合同继续为 `PARTIAL`。
|
||||
|
||||
### Active EterLib graphics/runtime utility review
|
||||
|
||||
- `EterLib/GrpDetector.cpp` 枚举 adapter/display mode/pixel format/capability;`GrpBase.cpp` 维护 back buffer、camera/projection/view、default index buffer、dynamic vertex stream 和 screen effects;`StateManager.cpp` 保存/恢复 D3D render/texture/shader/transform/stream/index/material 状态。
|
||||
- `GrpVertexBuffer*`、`GrpIndexBuffer*`、`GrpD3DXBuffer`、`GrpPixelShader`/`GrpVertexShader`、`GrpLightManager`、`GrpObjectInstance` 和 `GrpColorInstance` 分别管理 GPU buffer/shader/light/object transform/color transition;当前 Godot renderer 将这些责任下沉给引擎,没有对应 Windows lost/reset 和状态恢复 oracle。
|
||||
- `Pool.h`、`Dynamic.h`、`Ref.h`/`ReferenceObject.cpp`、`Thread.cpp`、`Event.h`、`Mutex.h` 形成参考端池化、引用计数、线程、事件排序和同步清理基础;当前端虽有 queue_free/RefCounted/SceneTree,但池容量、引用自毁、线程 shutdown 和按时间排序事件没有同一可观察契约。
|
||||
- 本轮完成 EterLib 图形/runtime utility 活跃文件静态核对,合同仍为 `PARTIAL`。
|
||||
|
||||
### Active UserInterface platform-host review
|
||||
|
||||
- `PythonSystem.cpp` 的配置文件、分辨率/频率枚举、窗口状态、音量/阴影/光标和 D3D adapter 查询,与当前 Godot `project/system_options.gd`/`app_lifecycle.gd` 的配置来源和窗口能力不同。
|
||||
- `PythonApplicationLogo.cpp`/`MovieMan.cpp` 通过 DirectShow/DirectDraw surface 驱动开场视频并恢复 D3D 状态;当前项目没有等价的 Windows media surface/skip/fade 生命周期。
|
||||
- `ProcessCRC`、`ProcessScanner`、`PythonExceptionSender` 与 GameGuard/HackShield/XTrap 提供参考端完整性/异常/安全发布门禁;当前没有相应的 Windows 组件和可执行验证。静态核对完成,但这些平台能力仍明确为 `PARTIAL/GAP`。
|
||||
|
||||
## 已确认差异和未验证项
|
||||
|
||||
1. 增加 Windows 导出预设、Windows GDExtension 构建和资源自包含脚本,至少验证启动、连接、地图/模型/音频资源加载和退出。
|
||||
2. 建立平台窗口合同:窗口大小/客户区、resize、全屏/窗口化、激活/失焦、关闭、鼠标坐标和输入法消息的运行时快照。
|
||||
3. 建立发布资源门禁:资源清单、哈希/版本、缺包/坏包/权限失败、路径大小写和外部 zip/PCK 解包失败均必须可诊断并阻断启动。
|
||||
4. 补充崩溃/异常/退出测试和诊断日志;区分正常关闭、网络断开、资源失败、GDExtension 加载失败和 native crash。
|
||||
5. 将 Windows、macOS、Android 的导出产物、资源来源、版本号和回归测试结果写入发布报告,避免把单平台导出通过误记为完整发布通过。
|
||||
|
||||
## 下一轮验证入口
|
||||
|
||||
- `world.environment_weather` 是当前唯一尚未逐分支审计的映射域;完成后生成全清单覆盖报告,再进入按优先级修复阶段。
|
||||
@@ -0,0 +1,109 @@
|
||||
# movement.click_collision
|
||||
|
||||
## Scope
|
||||
|
||||
审计鼠标点地、点实体、掉落物优先级、预约移动、地形 ATTRIBUTE_BLOCK、Actor 碰撞、滑动/停止和输入门控。
|
||||
|
||||
## Reference call chain
|
||||
|
||||
- `CPythonPlayer::NEW_SetMouseState`
|
||||
- 按绑定分派 MOVE、SMART、CAMERA、AUTO、ATTACK、SKILL。
|
||||
- `__OnPressSmart` / `__OnClickSmart`
|
||||
- 优先级为 item -> actor -> ground -> screen;按住和点击分别进入移动/攻击/交互分支。
|
||||
- `__OnPressGround` / `__OnClickGround`
|
||||
- 清预约和自动攻击,取消钓鱼,检查 `MOVABLE_GROUND_DISTANCE`,调用 `NEW_MoveToDestPixelPositionDirection`;被 gate 拒绝时使用 `__ReserveClickGround` 延迟重试。
|
||||
- `CInstanceBase::NEW_Goto` / `NEW_Stop`
|
||||
- 检查 syncing、moving skill、lock;设置 Src/Dst/rotation/`m_isGoing`,到达或停止时结束 walking。
|
||||
- `CInstanceBase::CheckAdvancing`
|
||||
- Actor collision 使用真实 collision sphere;逐个 `TestActorCollision` 并 `AdjustDynamicCollisionMovement`,第二个碰撞或调整后仍相交时 `BlockMovement`。
|
||||
- 对移动段和下一位置检查 `ATTRIBUTE_BLOCK`。
|
||||
|
||||
## Current call chain
|
||||
|
||||
- `PlayerController._on_click`
|
||||
- `ground_items.pick_at` 优先,其次对 pickables 做射线/AABB 点选,最后做地面射线求交。
|
||||
- `PlayerController._goto` / `_process`
|
||||
- 检查 active/frozen/locked/moving_skill 和近距离门;被 gate 拒绝时保存目标并在 0.1 秒后重试。
|
||||
- 点地使用 Src/Dst;WASD/移动端输入取消点地目标。
|
||||
- `PlayerController._get_blocking_actor` / `_actor_blocked`
|
||||
- 使用固定 `ACTOR_BODY_RADIUS_M`、距离门和趋近条件;静态 Actor 阻挡,其他 Actor 尝试切线滑移。
|
||||
- `PlayerController._blocked`
|
||||
- 通过 `Metin2World.is_blocked` 查询地形/静态阻挡;阻挡时尝试 x/z 分轴推进。
|
||||
- `GameScene._on_ground_clicked`
|
||||
- 生成 40250 `EFFECT_PICK` / `click.mse` 地面点击特效。
|
||||
|
||||
## Field-level findings
|
||||
|
||||
- 40250 的 `NEW_SetMouseState` 区分 `MBS_PRESS` 和 `MBS_CLICK`:Smart 按住会持续执行 `__OnPressSmart`,可持续预约地面/Actor/物品;松开才执行 `__OnClickSmart`。当前 `PlayerController._unhandled_input` 只处理左键按下,未实现同一条鼠标 Smart press/hold 状态机,因此按住鼠标连续移动、按住点 Actor/物品和 screen-direction 分支不能证明与参考一致。
|
||||
- 40250 的 Smart 优先级是 item -> actor -> ground -> screen,并且 actor/item 在按住与点击时走不同的预约/攻击/拾取逻辑。当前单击优先级已映射,但 actor 分支直接 `target_selected`,item 通过 signal 进入 NetPlay;没有统一记录 press/click/auto 三种组合的发送包序列。
|
||||
- 40250 `NEW_CanMoveToDestPixelPosition` 只拒绝当前坐标相同;当前 `_goto` 以固定 1m 门提前成功返回,门值来自近似常量,不是从 `CPythonPlayer::SetMovableGroundDistance` 的运行时状态读取。
|
||||
- 40250 Actor 碰撞使用当前运动模式选择 body/defending sphere,并在每个真实碰撞球对之间做距离门、趋近判定;当前 `_get_blocking_actor` 固定 0.55m 半径,且 `_actor_blocked` 只把部分 meta kind 当作静态阻挡,远端骨骼/坐骑/多个 collision sphere 的分支未复刻。
|
||||
- 40250 的物体碰撞在 `__TestObjectCollision` 命中后走 `AvoidObject`/`AdjustCollisionMovement`,若仍阻挡则 `BlockMovement` 清除运动;当前对 terrain/Actor 采用一次 frame 的 direct、切线和 x/z 分轴推进,可能留下 `_is_going` 或继续发移动状态,需单独验证被阻挡终止时序。
|
||||
- 当前 `_ray_ground` 以每 1m 射线采样、最多 80m,再二分地表;40250 `GetPickingPointWithRay` 先对象 picking,再 terrain/object height,且射线范围由相机 ray 配置,采样边界和桥/台阶优先级不同。
|
||||
|
||||
## Branch matrix
|
||||
|
||||
| 场景 | 40250 | 当前端 | 结论 |
|
||||
|---|---|---|---|
|
||||
| 点选优先级 | item -> actor -> ground;点击和按住路径不同 | item -> actor -> ground;左键 click 已接入 | MAPPED,按住/自动模式仍需完整 fixture |
|
||||
| 近距离点地 | 小于 `MOVABLE_GROUND_DISTANCE` 直接不动 | 固定 1m `MOVABLE_GROUND_DISTANCE_M` | PARTIAL:默认值/运行时设置未完全证明 |
|
||||
| lock/sync/moving skill | 拒绝平移;moving skill 可转向;失败点地预约 0.1s | 三门和预约重试已实现 | MAPPED + tests |
|
||||
| 地形阻挡 | 对 movement segment 分段检查 ATTRIBUTE_BLOCK,下一位置命中则 BlockMovement | `is_blocked` + direct/分轴推进 | PARTIAL:采样步长和 BlockMovement 时序不同 |
|
||||
| Actor 碰撞 | 使用每个 MSM 的真实 collision sphere;动态调整;第二次碰撞直接停 | 固定 body 半径近似,按 static/动态尝试滑移 | GAP:碰撞几何和多碰撞分支不 1:1 |
|
||||
| 点地特效 | 成功 `NEW_Goto` 后显示 `EFFECT_PICK` | `ground_clicked` 连接 `click.mse` | MAPPED |
|
||||
| 空白点击停止 | 无 item/actor/ground 时 `__OnClickSmart -> NEW_Stop` | `_ray_ground` 未命中时 `_on_click -> stop()` | MAPPED + regression |
|
||||
| 输入中断 | 失焦/停止/攻击/技能清除预约或按键状态 | `_notification`、`stop`、NetPlay 入口已接入 | PARTIAL:窗口/网络中断组合待测 |
|
||||
|
||||
## Equivalence matrix
|
||||
|
||||
| 项目 | 状态 | 证据 |
|
||||
|---|---|---|
|
||||
| Preconditions | MAPPED | active、player、camera、world 和三道移动 gate 已定位 |
|
||||
| Branch structure | PARTIAL | Smart/Auto/Camera/Attack/Skill 绑定和两种鼠标状态尚未全部覆盖 |
|
||||
| Algorithms/formulas | PARTIAL | 转向/距离/到达算法已映射;collision sphere 与 segment sampling 是近似 |
|
||||
| State transition order | PARTIAL | 预约、输入、碰撞、动画和网络状态的交错顺序需真实包/场景 fixture |
|
||||
| Constants/units | PARTIAL | 1m 近距离门、固定 0.55m body 半径、8m distance gate 未完全由资源证明 |
|
||||
| Timing/event sources | PARTIAL | 参考端 frame collision/update 与当前 Godot `_process` 同步点不同 |
|
||||
| Resource/data sources | PARTIAL | 参考端 `.msm` sphere/bone collision,当前移动层没有直接消费全部骨骼碰撞数据 |
|
||||
| Protocol side effects | MAPPED | ground/actor/item/skill 入口已接入 NetPlay;完整发送包序列仍需 fixture |
|
||||
| Interruption/failure/cleanup | PARTIAL | 点地被阻挡、实体删除、失焦、死亡、换图中断和连续点击待测 |
|
||||
|
||||
## Regression evidence
|
||||
|
||||
- `project/player_move_test.gd`:Src/Dst、三道 gate、预约、Actor 阻挡/死亡/skip collision、坐骑转向、空白点击停止旧目标。
|
||||
- `project/movement_parity_test.gd`:转向阻尼、到达精度和切线碰撞入口。
|
||||
- `project/netplay_test.gd`:CLICK_ACTOR、CLICK_ITEM、CLICK_POSITION、USE_SKILL 预约分支。
|
||||
- `project/test_click_target_effect_parity.gd`:点地、目标光圈和飞行物静态障碍测试。
|
||||
- 本轮四项回归均退出码通过;其中 click/target/fly 测试仍报告 3 个 ObjectDB 泄漏和 1 个资源退出时仍在使用,说明效果节点的清理虽满足断言,但退出生命周期尚不干净。
|
||||
|
||||
## Deep audit round 2026-09-20
|
||||
|
||||
本轮按 40250 鼠标输入、移动门、碰撞和地面 picking 的真实调用链复核:
|
||||
|
||||
1. 40250 `NEW_SetMouseState` 读取鼠标绑定后,对 Smart 分支在 `MBS_PRESS` 和 `MBS_CLICK` 分别调用 `__OnPressSmart`/`__OnClickSmart`;按住期间每帧可持续执行 item/actor/ground/screen 分支,并维护 `m_isSmtMov`。当前 `PlayerController::_unhandled_input` 只处理 `InputEventMouseButton.pressed`,没有左键 release、hold tick 或 `MBS_PRESS` 状态,因此“按住点地持续走”“按住实体持续攻击/交互”“Smart release 停止”不能视为已实现。
|
||||
2. 参考 `__OnClickSmart` 在没有拾取对象/地面时明确 `NEW_Stop()`;当前 `_on_click` 已在 `_ray_ground` 未命中时调用 `stop()`,清除旧的 `_is_going`、预约目标和按键状态,避免 click miss 后继续移动。`player_move_test.gd` 已增加回归。
|
||||
3. 40250 `NEW_CanMoveToDestPixelPosition`/`CheckAdvancing` 使用当前 Actor motion 的真实 collision sphere、对象碰撞 `AvoidObject`/`AdjustCollisionMovement`,第二次碰撞或调整后仍相交时 `BlockMovement`;当前只用固定 `ACTOR_BODY_RADIUS_M`、静态 kind 判断、一次切线再 x/z 分轴推进,没有第二碰撞停止状态,也没有从 MSM 读取每个骨骼碰撞球。
|
||||
4. 当前 `_ray_ground` 以每 1m、最多 800m 的高度采样后二分;参考 `GetPickingPointWithRay` 由 terrain/object picking 和相机 ray 边界决定,桥、台阶和对象 height 的优先级不是同一算法。现有测试只用假 world 高度和静态 obstacle,未覆盖真实 bridge/object/attribute 边界。
|
||||
5. 四组回归均通过:`player_move_test.gd`、`movement_parity_test.gd`、`netplay_test.gd`、`test_click_target_effect_parity.gd`;最后一项退出时仍有 3 个 ObjectDB 泄漏和 1 个资源使用警告,说明效果断言通过不等价于退出/中断生命周期已清理。
|
||||
|
||||
### Active EterLib picking-core review
|
||||
|
||||
- `EterLib/GrpCollisionObject.cpp` 支持 bound box、cube、indexed mesh、mesh triangle、sphere 和 cylinder 的 ray intersection;`Ray.h` 固定 start/direction/range/end 的单位与边界,`lineintersect_utils.cpp` 还提供线段/平行线最近点和调整算法。
|
||||
- 当前端点击/弹道分别使用 Godot ray/AABB/胶囊和简化地面采样;虽然 `test_click_target_effect_parity` 断言通过,但没有复用参考端同一 mesh-triangle/球柱体/线段最近点优先级,不能标记 picking 一致。
|
||||
- 本轮完成 GrpCollisionObject/Ray/line intersection 静态核对,点击碰撞合同继续为 `PARTIAL`。
|
||||
|
||||
## Status
|
||||
|
||||
`PARTIAL`。当前实现的输入/预约主链已建立,但碰撞几何、逐 Actor 调整和多碰撞停止条件与 40250 仍有结构差异;本轮不做无证据的算法替换。
|
||||
|
||||
## Audit round 2026-09-21T04:35Z — collision geometry boundary
|
||||
|
||||
本轮重新沿 `CActorInstance::CheckAdvancing`、`TestActorCollision`、
|
||||
`AvoidObject`/`BlockMovement`、`CMapOutdoor::GetPickingPointWithRay` 对照
|
||||
`PlayerController::_process`、`_get_blocking_actor`、`_blocked` 和 `_ray_ground`:
|
||||
|
||||
- 40250 的阻挡判定来自 `.msm` 每个 Actor 的真实 collision sphere,并且在移动段/下一位置分别检查,动态调整后再次相交会进入 `BlockMovement`;当前端仍是固定 0.55m body 半径、单次切线和 x/z 分轴近似。
|
||||
- 40250 的点地射线由 terrain/object picking 和相机 ray range 决定;当前端仍是最多 800m、每 1m 高度采样后二分,桥/台阶/object height 的优先级没有 1:1 数据来源。
|
||||
- 现有 `player_move_test.gd`、`movement_parity_test.gd`、`netplay_test.gd`、`test_click_target_effect_parity.gd` 全部通过;最后一项仍有 3 个 ObjectDB 泄漏和 1 个资源退出警告,因此只证明当前 fixture 的行为,不证明真实碰撞资源和退出清理等价。
|
||||
|
||||
结论:本轮没有实施替换;合同继续 `PARTIAL`。下一轮应优先建立真实 `.msm` sphere/object picking fixture,再决定是否下沉共享碰撞几何,而不是继续调固定半径。
|
||||
@@ -0,0 +1,155 @@
|
||||
# movement.keyboard-motion
|
||||
|
||||
## Scope
|
||||
|
||||
比较键盘输入、WASD 快速切换、移动请求、方向/速度计算、动作选择和停止清理。
|
||||
|
||||
## Reference call chain
|
||||
|
||||
- `UserInterface/PythonPlayerInputKeyboard.cpp`
|
||||
- `UserInterface/PythonPlayerInput.cpp`
|
||||
- `UserInterface/InstanceBaseMovement.cpp`
|
||||
- `UserInterface/InstanceBaseMotion.cpp`
|
||||
- `GameLib/ActorInstanceMotion.cpp`
|
||||
- `GameLib/ActorInstanceMotionEvent.cpp`
|
||||
|
||||
## Current call chain
|
||||
|
||||
- `project/player_controller.gd`
|
||||
- `project/net_play.gd`
|
||||
- `project/net_world.gd`
|
||||
- `project/test_wasd_steering_parity.gd`
|
||||
- `project/test_no_auto_move_regression.gd`
|
||||
- `project/player_motion_test.gd`
|
||||
- `project/keyboard_motion_timeline_test.gd`
|
||||
|
||||
## Audit findings (2026-09-20)
|
||||
|
||||
40250 的键盘路径是事件驱动的:`NEW_SetSingleDIKKeyState` → `NEW_SetSingleDirKeyState` 更新四个方向位 → `NEW_SetMultiDirKeyState`。无方向立即 `NEW_Stop`;有方向按 `Up > Down`、`Left > Right` 计算八方向角,再调用 `NEW_MoveToDirection`。`NEW_MoveToDirection` 依次执行取消点地预约、主角存在/私店/锁定/移动技能/同步门、设置 `m_isGoing = FALSE`、设置 advancing rotation、启动 walk/run motion,并把当前位置向量加 300 像素目标。
|
||||
|
||||
当前端已按事件维护方向位,W/S 优先级、释放切换、失焦、进场残留和直立旋转均有回归证据;本轮新增 `keyboard_motion_timeline_test.gd`,实际驱动 `InputEventKey` 和 `_process`,覆盖“加载期按键不继承 → W+S 优先 → 释放 W 暴露 S → 释放最后方向停止”的时序。
|
||||
|
||||
仍存在明确的实现层差异,不能升级为 STATIC_VERIFIED:
|
||||
|
||||
- 40250 在按键事件中设置固定 300 像素 `Dst`,随后由 `.msa`/`CActorInstance::AccumulationMovement` 按动作根运动和 `m_fMovSpd` 推进;当前端在 `_process` 中按米制连续速度推进,模型可用时读取 `get_move_motion_speeds`,否则回退 `SPEED_WALK/SPEED_RUN`。方向角可一致,但目标距离、速度、到达时刻和阻挡后的状态不同。
|
||||
- 40250 的 `CInstanceBase::Update → AccumulationMovement → Transform` 才会触发 `OnMove/OnMoving/OnWaiting/OnStop`;当前端 `moved` 和 `anim_state` 来自每帧预测位置,`NetPlay` 再按 0/300/100ms 节流发 `FUNC_MOVE/FUNC_WAIT`。已有节流测试,但没有证明每个动作资源停止/切换事件与网络状态事件一一对应。
|
||||
- 40250 `NEW_MoveToDirection` 的 `__IsSyncing`、`isLock`、`IsUsingMovingSkill` 分支是“忽略移动但可能保留原动作”的同步门;当前端 `frozen`/`locked`/`moving_skill` 在 `_process` 早返回,`_goto` 被门挡时只对点地目标建立预约,键盘方向本身不建立预约。被击退、情绪、技能锁和解锁恢复的顺序尚未全部对齐。
|
||||
- 40250 的相机旋转可在 `NEW_MoveToDirection` 内先改变相机再叠加角色方向;当前端读取 `camera.heading()` 后直接构造前后左右向量,没有实现同一套相机旋转副作用。
|
||||
- 当前端用 Euler 分量强制保持 `rotation.x/z = 0`,解决了 W/S 快切翻跟头;这是有效的 Godot 平台适配,但还缺“角色根节点/骨骼动画旋转不被姿态资源覆盖”的真实模型测试。
|
||||
|
||||
因此现有测试证明的是输入回归和网络节流的已覆盖路径,不是完整 40250 移动/动作/资源链等价。
|
||||
|
||||
## Baseline tests
|
||||
|
||||
`test_wasd_steering_parity.gd`、`test_no_auto_move_regression.gd`、`netplay_test.gd` 和 `keyboard_motion_timeline_test.gd` 当前通过,证明已覆盖的回归路径没有复现自动移动、W/S 粘键、翻滚和 0/300/100ms 网络节流问题;这不替代完整 40250 分支等价审计。
|
||||
|
||||
## Deep audit round 2026-09-20
|
||||
|
||||
本轮重新对照 `NEW_SetSingleDirKeyState`、`NEW_MoveToDirection`、`AccumulationMovement`、`RotationProcess` 和当前 `PlayerController`,并重跑输入与动作测试:
|
||||
|
||||
- 当前 `active` 切换、W/S 优先级、KeyUp 全局接收、失焦清键、`rotation.x/z=0` 和跨 `±PI` 的单向转向均有可重复测试;这解释了此前 W/S 快切翻跟头已经被当前平台适配压住。
|
||||
- 40250 方向键移动把目标设置为当前位置前方固定 300 像素,再由动作根运动和 `m_fMovSpd` 推进;当前端仍用 `_process` 的米制连续速度推进,并在模型缺失时使用固定速度 fallback。因此“方向一样”不代表到达时刻、动作结束事件和服务端 MOVE/WAIT 边沿一致。
|
||||
- 当前 `frozen/locked/moving_skill` 在 `_process` 中早返回或只转向;40250 的同步、私店、受击、情绪、移动技能和 `NEW_Stop` 分支会分别决定是否清 `m_isGoing`、是否保留动作及何时恢复。本轮没有端到端证明这些门在解锁后与 40250 同一帧恢复。
|
||||
- 当前摄像机方向只参与构造移动向量,没有复刻 40250 `NEW_MoveToDirection` 内相机旋转与角色 advancing rotation 的完整副作用;真实角色模型测试 `player_motion_test.gd` 虽打印 PASS,但本轮退出码为 139 并报告 6 个 ObjectDB 泄漏,不能作为干净证据。
|
||||
- `test_wasd_steering_parity.gd`、`test_no_auto_move_regression.gd`、`keyboard_motion_timeline_test.gd` 本轮退出码 0,覆盖的输入回归通过;因此只保留“输入问题已回归通过”,不把根运动、资源事件、锁门和退出生命周期标记为等价。
|
||||
|
||||
结论:WASD 输入状态和翻转回归已稳定,但 40250 的固定目标/根运动、动作事件源、锁门恢复、相机副作用和真实模型退出安全仍有差异,合同保持 `PARTIAL`。
|
||||
|
||||
### Active UserInterface actor-transform review
|
||||
|
||||
- `InstanceBaseEvent.cpp` 将 `CInstanceBase` 的 event handler 直接绑定到 `CActorInstance`;`InstanceBaseTransform.cpp` 提供 cm 像素位置、rotation/blend direction、LookAt fly target/destination 和当前/advancing rotation 访问。
|
||||
- `PythonPlayerEventHandler.h` 明确消费 `OnSyncing/Waiting/Moving/Move/Stop/Warp/ClearAffects/SetAffect/ResetAffect/Attack/UseSkill/Hit`,并在 bow fly handler 中把 `OnSetFlyTarget` 与动作帧 `OnShoot` 转成网络副作用。当前端 `PlayerController`/`NetPlay` 有移动/技能信号,但未证明远端/本地都按同一 event handler 顺序消费,自动移动和快速 WASD 仍需状态时序 oracle。
|
||||
- 本轮完成 InstanceBase/PlayerEventHandler 静态核对,键盘合同继续为 `PARTIAL`。
|
||||
|
||||
## Implementation round 2026-09-20T19:03Z
|
||||
|
||||
本轮将键盘方向目标按 `InstanceBaseMovement.cpp::NEW_MoveToDirection` 的固定距离
|
||||
规则收敛:方向事件现在记录当前角色位置加 300 像素(当前 Godot 单位为 3.0m)的
|
||||
`_direction_dst`,后续帧只向该目标推进,目标到达后停止,不再每帧从当前位置延长
|
||||
路径;移动技能仍保留只转向不平移的分支。死亡、受击/锁定门和方向释放会清理该目标。
|
||||
|
||||
新增的时间线断言验证了固定目标、到达停止、W/S 优先级、KeyUp 暴露、最后方向释放和
|
||||
进入游戏不继承输入;`keyboard_motion_timeline_test.gd`、`player_move_test.gd`、
|
||||
`test_wasd_steering_parity.gd`、`test_no_auto_move_regression.gd` 均退出码 0。
|
||||
|
||||
这只修复了目标创建/消费语义,不宣称完整根运动等价:当前端仍由 Godot `_process`
|
||||
按速度推进,尚未接入 40250 `.msa`/`AccumulationMovement` 的真实动作事件、相机副作用、
|
||||
同步/情绪/受击解锁恢复和完整 MOVE/WAIT 时间线,合同继续为 `PARTIAL`。
|
||||
|
||||
## Equivalence matrix
|
||||
|
||||
| 项目 | 状态 | 备注 |
|
||||
|---|---|---|
|
||||
| Preconditions | PARTIAL | active/失焦/按键残留、锁门、私店、情绪和锁门 held WASD 已覆盖;同步、受击、死亡和传送门未全覆盖 |
|
||||
| Branch structure | PARTIAL | W/S、A/D、八方向和释放顺序已覆盖;相机旋转与所有禁止移动分支未闭合 |
|
||||
| Algorithms/formulas | PARTIAL | 方向优先级和归一化一致;固定 300 像素 Dst 与根运动被连续速度替代 |
|
||||
| State transition order | PARTIAL | 输入状态、锁门/移动技能恢复、PlayerController、NetPlay 入口已定位;资源事件与网络事件的完整顺序未证实 |
|
||||
| Constants/units | PARTIAL | 角度/旋转阈值和 0/300/100ms 节流已记录;米/厘米、移动速度和目标距离不同 |
|
||||
| Timing/event sources | PARTIAL | 当前是 `_process` 预测 + signal;参考是 Actor motion event 驱动 |
|
||||
| Resource/data sources | PARTIAL | 当前模型速度可从动作资源读取但有 fallback;`.msa` event/AccumulationMovement 尚未一一对照 |
|
||||
| Protocol side effects | PARTIAL | `FUNC_MOVE/FUNC_WAIT` 发送和节流有测试;阻止/重复/相机副作用仍未闭合 |
|
||||
| Interruption/failure/cleanup | PARTIAL | stop、失焦、换图、锁定/移动技能恢复已覆盖;受击/情绪/传送和完整解锁矩阵仍需测试 |
|
||||
|
||||
## Remaining
|
||||
|
||||
- 用真实 `camera.heading()`、模型动作速度和网络 fake 记录同一方向序列的输入事件、移动目标、停止请求、动画状态和包时间线。
|
||||
- 补充死亡/传送/受击以及私店、情绪、移动技能和普通锁定的多门叠加顺序;各单门 held WASD 恢复已覆盖基础路径。
|
||||
- 继续对照 `.msa` 的 motion event、`AccumulationMovement` 根运动和当前 `get_move_motion_speeds` fallback,决定是否需要根运动适配层。
|
||||
- 核对相机旋转副作用与角色方向叠加,以及真实角色模型在 W/S 快切时根节点/骨骼姿态不翻转。
|
||||
|
||||
### Audit round 2026-09-21
|
||||
|
||||
本轮重新执行 `keyboard_motion_timeline_test.gd`、`test_wasd_steering_parity.gd` 和
|
||||
`test_no_auto_move_regression.gd`,三项均通过;W/S 优先级、KeyUp 暴露被按住方向、最后
|
||||
方向释放停止、进入游戏不继承加载期按键、角色 X/Z 姿态归零、跨 `±PI` 转向和技能结束后
|
||||
不自动恢复移动均有回归证据。
|
||||
|
||||
沿 `ActorInstanceBattle.cpp::ComboAttack`、`ActorInstanceMotion.cpp::AccumulationMovement`
|
||||
和 `PythonPlayerEventHandler::OnMove/OnMoving/OnWaiting/OnStop` 再次核对后,没有把这些
|
||||
输入测试升级为完整等价:当前端仍用连续速度预测替代固定 300 像素 Dst + 根运动,动作资源
|
||||
事件与移动网络事件也不是同一来源;同步/受击/情绪/移动技能解锁恢复、相机旋转副作用和
|
||||
真实模型根节点测试仍是下一轮差异项。合同保持 `PARTIAL`,本轮未改动实现。
|
||||
|
||||
### Implementation fix round 2026-09-21T11:45Z
|
||||
|
||||
本轮按 40250 `CPythonPlayer::Update` 在 `m_isDirKey` 下每帧重试
|
||||
`NEW_SetMultiDirKeyState(m_isLeft,m_isRight,m_isUp,m_isDown)` 的语义修复了当前端的
|
||||
锁门恢复差异:方向键仍按住时,`frozen/locked` 不再丢弃方向状态;移动技能接管移动时
|
||||
会清除旧的平移目标但保留 held WASD,技能结束或锁定解除后的首个可移动更新重新创建
|
||||
固定 300cm 目标。方向释放、死亡和停止仍会清除 pending 状态,避免旧目标在技能结束后
|
||||
莫名续走。
|
||||
|
||||
`keyboard_motion_timeline_test.gd` 新增锁定恢复和移动技能恢复断言;与
|
||||
`test_wasd_steering_parity.gd`、`test_no_auto_move_regression.gd`、
|
||||
`player_motion_test.gd` 一起退出码均为 0。真实模型测试同时改为清理 native model/animation
|
||||
节点并清空测试用的 GR2/MSA 缓存,确认本轮测试不再出现 ObjectDB 泄漏和 exit 139。
|
||||
|
||||
这轮只闭合了 held WASD 在锁门/移动技能边沿的状态恢复,不宣称完整移动等价:当前端仍
|
||||
用 Godot 连续速度预测替代 `.msa`/`AccumulationMovement` 根运动,`OnMove/OnMoving/`
|
||||
`OnWaiting/OnStop` 事件源、同步/受击/传送边界、相机副作用和完整网络时间线
|
||||
仍保持 `PARTIAL`。
|
||||
|
||||
### Implementation fix round 2026-09-21T12:05Z
|
||||
|
||||
本轮补齐了 40250 `__CanMove` 的两个此前缺失的主角状态门:
|
||||
`CPythonPlayer::IsOpenPrivateShop()` 与 `__IsProcessingEmotion()`。当前端新增
|
||||
`PlayerController.set_private_shop_open()` / `set_processing_emotion()`;门开启时会
|
||||
清理点地预约和旧方向目标,但保留 WASD 的方向位,和 `NEW_SetSingleDirKeyState`
|
||||
保留 `m_isDirKey`、`CPythonPlayer::Update` 解锁后重新调用
|
||||
`NEW_SetMultiDirKeyState` 的行为一致。`NetPlay._on_entity_info()` 只对主角读取
|
||||
`shop_sign` / `acting_emotion`,因此查看或打开其他角色的商店不会锁住本地输入。
|
||||
|
||||
`keyboard_motion_timeline_test.gd` 新增摆摊中、情绪处理中“无平移且保留按键”,以及
|
||||
状态解除后第一帧恢复移动的断言;本轮 targeted tests 通过。私店/情绪两个移动门已闭合,
|
||||
但根运动、动作事件源、同步/受击/传送边界、相机副作用和完整网络时间线仍保持
|
||||
`PARTIAL`。
|
||||
|
||||
## Audit round 2026-09-21T04:45Z — keyboard/root-motion boundary
|
||||
|
||||
本轮重新执行 `keyboard_motion_timeline_test.gd`、`test_wasd_steering_parity.gd`、
|
||||
`test_no_auto_move_regression.gd` 和 `player_motion_test.gd`,全部退出码为 0:
|
||||
|
||||
- 进入游戏不继承加载期按键、W/S 优先与释放切换、最后方向停止、技能结束不续走、快速 WASD 不翻跟头、跨 `±PI` 单向转向和真实模型动作装配均保持通过。
|
||||
- 继续对照 `NEW_SetMultiDirKeyState`、`NEW_MoveToDirection`、`AccumulationMovement` 和 `OnMove/OnMoving/OnWaiting/OnStop` 后,确认当前端仍是 Godot `_process` 连续速度预测,不能用输入测试替代 40250 `.msa` 根运动/动作事件源证据。
|
||||
|
||||
结论:本轮未修改实现;输入回归保持稳定,合同继续 `PARTIAL`。下一轮需要用真实动作资源和 fake network timeline 对拍固定 300cm 目标、根运动、停止事件和 MOVE/WAIT 包边沿。
|
||||
@@ -0,0 +1,156 @@
|
||||
# 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。
|
||||
@@ -0,0 +1,162 @@
|
||||
# movement.remote-sync-state
|
||||
|
||||
## Scope
|
||||
|
||||
比较远端角色 `GC_MOVE`/状态命令的接收、服务器时钟校正、TCP 状态队列释放、动作前移动、到达后的动作、速度/走跑模式、位置纠偏、死亡/击倒/表情锁、中断和换图清理。
|
||||
|
||||
## Reference call chain
|
||||
|
||||
- `UserInterface/PythonNetworkStreamPhaseGameActor.cpp::RecvCharacterMovePacket`
|
||||
- 解码 `bRot * 5`、`bFunc`、`bArg`、`dwTime`、目标坐标和 `dwDuration`。
|
||||
- `CNetworkActorManager::MoveActor` 查找已有 VID,同时更新 `SNetworkActorData` 的展示目标。
|
||||
- `UserInterface/InstanceBase.cpp::PushTCPState`
|
||||
- 用 `ELTimer_GetServerFrameMSec() - dwCmdTime` 更新 70/30 EMA。
|
||||
- 以 `dwCmdTime + m_nAverageNetworkGap` 入队。
|
||||
- `UserInterface/InstanceBase.cpp::StateProcess`
|
||||
- 服务器帧未到时保留队首。
|
||||
- `__CanProcessNetworkStatePacket` 拒绝死亡、击倒和不可取消技能。
|
||||
- `__IsEnableTCPProcess` 在表情或 TCP 状态禁用期间暂停释放。
|
||||
- 按 `FUNC_WAIT`、`FUNC_MOVE`、`FUNC_COMBO`、`FUNC_ATTACK`、`FUNC_MOB_SKILL`、`FUNC_EMOTION` 和 `FUNC_SKILL` 分支建立 `Src/Dst`、`m_kMovAfterFunc` 和动作。
|
||||
- `UserInterface/InstanceBase.cpp::MovementProcess`
|
||||
- 由 `CActorInstance::AccumulationMovement` 的动作根运动推进。
|
||||
- 到达后按 `m_kMovAfterFunc` 执行 combo/attack/skill/emotion,远端过冲超过 100 像素时重新取 Src,并把普通移动降为等待。
|
||||
- `UserInterface/PythonNetworkStreamPhaseGameActor.cpp::RecvSyncPositionPacket`
|
||||
- 每个纠偏元素调用 `CNetworkActorManager::SyncActor`。
|
||||
- `CActorInstance::TEMP_Push` 做 100 段物理碰撞探测、BlendingPosition 和大位移受击动作。
|
||||
- `UserInterface/PythonNetworkStreamPhaseHandShake.cpp`
|
||||
- 握手 `dwTime + lDelta` 与本地毫秒时钟建立服务器帧时基。
|
||||
|
||||
## Current call chain
|
||||
|
||||
- `extension/src/net/classic/classic_parser.cpp::HDR_GC_MOVE`
|
||||
- `extension/src/net/entity_store.cpp::mut_move`
|
||||
- 远端 VID 入 `std::deque<StateCmd>`,用 `average_network_gap` 做 70/30 EMA。
|
||||
- `drain_state_queue` 按 `server_frame_ms`、死亡/击倒门释放。
|
||||
- `extension/src/net/entity_store.cpp::apply_state_cmd`
|
||||
- 已覆盖 WAIT/MOVE/COMBO/ATTACK/MOB_SKILL/FUNC_EMOTION/技能位掩码,并保存 `mov_after_func`。
|
||||
- `extension/src/net/entity_store.cpp::tick`
|
||||
- 先释放命令,再以 `.msa` 提取的 WALK/RUN 根运动速度推进;无资源时按 `dwDuration` 线性插值。
|
||||
- `extension/src/net/entity_store.cpp::mut_snap_position`
|
||||
- `GC_SYNC_POSITION` 直接把当前位置和 Src/Dst 设置为纠偏坐标并停止移动。
|
||||
- `project/net_world.gd::_update_visibility`
|
||||
- 节点位置直接读取 C++ 实体坐标;按 `func`、`moving`、`walk_mode`、`dead` 更新远端动作。
|
||||
- `extension/src/net/classic/classic_session.cpp`
|
||||
- 将 classic 握手推导出的 `server_frame_ms()` 每帧喂给 `EntityStore`。
|
||||
- `extension/tests/net_state_queue_test.cpp`
|
||||
- 覆盖帧门、阈值、技能位、死亡/击倒挂起、顺序、时钟防呆、根运动和 duration fallback。
|
||||
|
||||
## Audit findings (2026-09-20)
|
||||
|
||||
已确认当前端的远端移动不再是收到 `GC_MOVE` 就直接改坐标,核心队列、70/30 网络延迟 EMA、服务器帧释放、1/50 像素阈值、到达后动作和根运动推进都已有对应实现;因此这一项不能继续保持“未审计的 MAPPED”。
|
||||
|
||||
仍有材料差异,状态定为 `PARTIAL`:
|
||||
|
||||
- 40250 的 `FUNC_EMOTION` 在目标距离大于 100 像素时先走到目标再执行情绪;当前端已补齐 100 像素阈值、到达后的 `mov_after_func` 和远程表情不启用 skip-collision 的分支。由于 `TPacketGCMove` 本身不携带 `uTargetVID`,双人表情目标解析仍无法在网络层 1:1 复刻。
|
||||
- 40250 `__CanProcessNetworkStatePacket` 还检查不可取消技能,`__IsEnableTCPProcess` 还检查 `IsActEmotion` 和 `m_bEnableTCPState`;当前已新增 presentation-owned `skill_active/skill_can_cancel/acting_emotion/tcp_state_enabled` 门,并由远端一次性技能/表情的 `motion_bound("wait")` 尾事件释放。技能可取消性仍缺少 40250 动作数据的权威来源,未知资源和无模型节点保留回退路径。
|
||||
- 40250 的远端移动权威推进来自动作 `.msa` 的 `AccumulationMovement`;当前有真实 WALK/RUN 根运动读取,但无模型时使用 `dwDuration` fallback,且坐骑直接 fallback,速度/到达时间不完全一致。
|
||||
- 40250 `FUNC_COMBO`/`FUNC_SKILL` 到达后使用 `uArg` 选择具体动作;当前队列保存了 `mov_after_arg`,且 `net_world` 已把 `FUNC_ATTACK` 映射为 `NAME_NORMAL_ATTACK`、把 `FUNC_COMBO` 的 `func_arg` 原样传给 `play_attack_motion`,把 `FUNC_SKILL` 的循环/移动位传给 `play_skill_motion`。`FUNC_EMOTION` 现在也按 `uArg` 尝试 `set_motion_id`,动作完成后释放 TCP 门;远端 combo/skill/emotion 资源缺失时仍按视图实现回退,怪物 motlist 的逐段资源覆盖和技能 grade 权威来源仍未完全闭合。
|
||||
- 40250 `TEMP_Push` 对 `GC_SYNC_POSITION` 做 100 段物理碰撞探测、BlendingPosition,并在位移超过 150 像素时触发 DAMAGE_FLYING/STAND_UP;当前端已由 `mut_snap_position` 记录源点/序号/150cm 与技能判定,`NetWorld` 执行 100 段 `is_blocked` 探测、1 秒倒数偏移和击倒动作链。无模型/无地形 world 时仍保留可验证的权威坐标回退,因此资源和真实场景碰撞结果仍为 PARTIAL。
|
||||
- 40250 对命令队列的未来时间不设置 1000ms 防呆阀;当前 `STATE_QUEUE_MAX_WAIT_MS` 超过阈值立即释放,是为未完成的真实服务器时基校准保留的有意偏离。
|
||||
- 换图清理已清除实体和队列,但“远端动作正在播出时收到死亡、击倒、传送和重新 Add”的完整资源/动作事件顺序仍缺端到端证据。
|
||||
|
||||
## Deep audit round 2026-09-20
|
||||
|
||||
本轮重新对照 `InstanceBase::StateProcess`、`TEMP_Push` 和 `NetworkActorManager::MoveActor/SyncActor` 的实际分支,并重跑当前端队列、实体和移动回归:
|
||||
|
||||
- 40250 在释放队首命令前同时检查死亡、击倒、不可取消技能、表情和 `m_bEnableTCPState`;当前队列虽然有死亡/击倒门,且已补 `FUNC_EMOTION` 的距离/到达分支,但没有不可取消技能、表情活动和 TCP 状态开关的等价状态源,双人表情目标也无法从 `TPacketGCMove` 还原。
|
||||
- 40250 的 `FUNC_COMBO`、`FUNC_ATTACK`、`FUNC_MOB_SKILL` 和 `FUNC_SKILL` 在到达后继续使用命令参数选择动作;当前 `mov_after_arg` 能保存参数,但远端视图侧没有完整的 arg→动作资源路由,当前回归只验证 func/队列状态,不验证具体 `.msa` 播放结果。
|
||||
- `GC_SYNC_POSITION` 在 40250 进入 `TEMP_Push`,当前端已补 100 段碰撞探测、BlendingPosition 偏移和大位移击倒动作门;真实模型动作、地图碰撞资源和无模型 fallback 仍不能证明完全一致。
|
||||
- 本轮 `net_state_queue_test`、`net_entity_test`、`net_world_vis_test`、`netplay_test`、`movement_parity_test` 和 `net_world_remote_action_test` 均退出码 0;这些测试证明队列门、EMA、根运动 fallback、可见性、上行移动和 TEMP_Push 纠偏事件/击倒门没有回归,但没有覆盖真实模型动作资源、表情锁、不可取消技能权威数据、实服乱序/跨图/死亡重建和地图碰撞资源。
|
||||
|
||||
本轮没有把已有 PASS 测试误升为等价完成;核心队列链保持可用,但上述状态门、动作参数和纠偏物理仍使合同保持 `PARTIAL`。
|
||||
|
||||
## Equivalence matrix
|
||||
|
||||
| 项目 | 状态 | 备注 |
|
||||
|---|---|---|
|
||||
| Preconditions | PARTIAL | VID、主角过滤、服务器帧门、死亡/击倒、不可取消技能和表情/TCP 门已有实现;技能可取消性权威来源和双人表情目标未闭合 |
|
||||
| Branch structure | PARTIAL | WAIT/MOVE/COMBO/ATTACK/MOB_SKILL/EMOTION/SKILL 距离分支已对齐;完整禁止门仍缺失 |
|
||||
| Algorithms/formulas | PARTIAL | 70/30 EMA、1/50 阈值、100 像素过冲、100 点纠偏探测、150cm 击倒阈值和 1 秒 BlendingPosition 已对齐;无模型/无 world fallback 不等价 |
|
||||
| State transition order | PARTIAL | 队列→动作前移动→到达动作→动作尾释放,以及 sync serial→碰撞→缓动→击倒/起身→释放门顺序已对齐;双人表情目标和真实资源尾时序未闭合 |
|
||||
| Constants/units | PARTIAL | 角度、cm、阈值和 70/30 已记录;1000ms 防呆阀与坐骑/fallback 是偏离 |
|
||||
| Timing/event sources | PARTIAL | classic server frame 已接入;真实握手时基、动作事件与状态动作的端到端联机证据不足 |
|
||||
| Resource/data sources | PARTIAL | WALK/RUN `.msa` 根运动、PC combo `.msa` 模式目录、PC emotion action 和 damage_flying/standup 入口已接入;技能可取消/grade、怪物逐段 combo、emotion 目标、真实击倒资源覆盖仍不完整 |
|
||||
| Protocol side effects | PARTIAL | GC_MOVE、GC_SYNC_POSITION、CHANGE_SPEED、WALK_MODE 已解析;纠偏动作已桥接,双人情绪目标和服务器时基副作用未等价 |
|
||||
| Interruption/failure/cleanup | PARTIAL | 死亡/击倒挂队、换图清理已有单测;技能/表情/传送/重建完整矩阵未完成 |
|
||||
|
||||
## Regression evidence
|
||||
|
||||
- `extension/tests/net_state_queue_test.cpp`:服务器帧释放、WAIT 1.0、攻击/技能 50.0、FUNC_EMOTION 100.0、死亡/击倒挂队、FIFO、时钟防呆、根运动和 duration fallback。
|
||||
- `extension/tests/net_entity_test.cpp`:远端移动命令的 near/far 行为、到达后动作和未知 VID 忽略。
|
||||
- `project/netplay_test.gd`、`project/movement_parity_test.gd`:上行状态节流和移动预测不回归。
|
||||
- `project/net_world_remote_action_test.gd`:远端 `FUNC_ATTACK` 使用普通攻击索引 13,`FUNC_COMBO` 保留 `bArg` 连击段,并传递武器动作模式与攻速比例。
|
||||
- `extension/tests/net_state_queue_test.cpp`:不可取消技能、活动表情、TCP 状态禁用和恢复后的队列释放边界。
|
||||
- `extension/tests/net_entity_test.cpp`:`GC_SYNC_POSITION` 记录源点、单调 serial、150cm 大位移和技能抑制状态。
|
||||
- `project/net_world_remote_action_test.gd`:TEMP_Push 大位移动作、击倒网络门、旧位置起始偏移和动作尾释放。
|
||||
|
||||
## Implementation fix round 2026-09-20
|
||||
|
||||
按 40250 `InstanceBase::StateProcess` 到 `RunNormalAttack` / `RunComboAttack` 的动作选择
|
||||
补齐远端攻击状态的参数桥:普通攻击使用 `CRaceMotionData::NAME_NORMAL_ATTACK`(13),连击
|
||||
使用 `TPacketGCMove.bArg` 原值,二者都从远端视图读取当前武器动作模式,并把 `bAttackSpeed / 100`
|
||||
传给动作播放层。此前两种状态都会降级成通用 `set_anim_state("attack")`,导致远端角色看不到
|
||||
对应的 combo 段;资源解析失败时仍保留通用状态回退。
|
||||
|
||||
回归:`godot --headless --path project --script net_world_remote_action_test.gd` 通过。
|
||||
死亡/击倒门、FUNC_EMOTION 目标、技能 grade、坐骑根运动和 `TEMP_Push` 纠偏物理仍保持
|
||||
`PARTIAL`,本轮没有把动作索引测试误当成完整资源/联机等价。
|
||||
|
||||
## Implementation fix round 2026-09-20 — state gates and emotion action
|
||||
|
||||
按 40250 `__CanProcessNetworkStatePacket` 与 `__IsEnableTCPProcess` 增加 EntityStore 的
|
||||
四个 presentation-owned 状态门:不可取消技能阻止后续状态出队,活动表情阻止 TCP 状态,
|
||||
`m_bEnableTCPState=false` 时只允许当前 `FUNC_EMOTION`,死亡/击倒门保持原有优先级。NetWorld
|
||||
在远端技能/表情动作开始时设置门,在 `motion_bound("wait")` 动作尾清除;节点剔除和换图也会
|
||||
清除门,避免模型被释放后队列永久冻结。
|
||||
|
||||
同时把 `FUNC_EMOTION` 的 `func_arg` 接入 40250 动作 ID(单人表情使用 `target_race=-1`,
|
||||
双人 `GC_MOTION` 仍走原有目标 VID 路径),并让出生时已经带有 `FUNC_*` 的实体在字段刷新后
|
||||
立即绑定初始动作,而不是等待下一次状态变化。
|
||||
|
||||
回归:`net_state_queue_test`、`net_world_remote_action_test`、`net_world_vis_test`、
|
||||
`netplay_test`、`player_motion_test`、`test_skill_state_arg_parity` 和完整 `ctest`(23/23)
|
||||
均通过。技能可取消性的动作数据来源、真实远端模型动作尾时序、双人表情目标和 `TEMP_Push`
|
||||
仍保持 `PARTIAL`。
|
||||
|
||||
## Status
|
||||
|
||||
`PARTIAL`。远端 TCP 状态队列主链已经有较充分实现和单元测试;FUNC_EMOTION 的距离/到达顺序已补齐,但 40250 的不可取消技能门、双人表情目标、纠偏物理动作、远端动作参数和真实服务器时基仍未完全等价。
|
||||
|
||||
## Implementation fix round 2026-09-21 — TEMP_Push correction path
|
||||
|
||||
按 40250 `NetworkActorManager::SyncActor` → `CInstanceBase::NEW_SyncPixelPosition` →
|
||||
`CActorInstance::TEMP_Push` 补齐 `GC_SYNC_POSITION` 的表现层纠偏:`EntityStore::mut_snap_position`
|
||||
现在保留同步前坐标、单调 correction serial、150cm 大位移判定及 `IsUsingSkill` 抑制位;
|
||||
`M2Client::entity_dict` 将这些字段交给 `NetWorld`。节点收到新 serial 后,`NetWorld` 按
|
||||
`TEMP_Push` 的 100 个采样点调用地图 `is_blocked`,无碰撞时用 `PhysicsPush` 的 1 秒
|
||||
倒数偏移保持旧位置起步并回到权威目标;大位移且非技能时启动
|
||||
`DAMAGE_FLYING -> STAND_UP`,并通过 `set_entity_knockdown` 复刻 `IsKnockDown` 网络状态门。
|
||||
动作尾、节点剔除和换图会释放该门;没有可用模型/地形时保留权威坐标回退,不伪造资源动作。
|
||||
|
||||
回归:`net_entity_test`、`net_world_remote_action_test`、`net_world_vis_test`、
|
||||
`netplay_test`、`player_motion_test`、`movement_parity_test` 和完整 `ctest`(23/23)通过。
|
||||
真实 `IPhysicsWorld` 地形数据、动作资源覆盖、技能可取消权威来源、双人表情目标和真实服
|
||||
乱序/跨图/死亡重建仍保持 `PARTIAL`。
|
||||
|
||||
## Implementation fix round 2026-09-21 — unified remote FUNC_MOVE arrival branch
|
||||
|
||||
复核 40250 `CInstanceBase::MovementProcess` 的远端 actor 分支后确认,
|
||||
`m_kMovAfterFunc == FUNC_MOVE` 穿过 `Dst` 时没有 PC/NPC/monster 的 `ch_type` 分叉:
|
||||
`FUNC_MOVE` 的到达 switch case 继续沿当前根运动推进,只有后续状态命令或超过
|
||||
`100cm` 的 overshoot 防呆分支才会改回 `WAIT`。当前端此前只让 PC 继续,NPC/MOB
|
||||
在第一次越过 `Dst` 时提前停止并清除 `skip_collision`,导致远端怪物的连续 `GC_MOVE`
|
||||
表现和 40250 不一致。
|
||||
|
||||
现已将 `EntityStore::advance_walk_by_motion` 的远端到达逻辑统一为参考分支:
|
||||
死亡/击倒仍立即停止;`FUNC_MOVE` 保留 `moving`、`mov_after_func` 和根运动推进;
|
||||
普通攻击、连击、技能、表情等到达动作继续走原有 `finish_walk`。新增 NPC/MOB
|
||||
回归用例先证明修复前会失败,修复后 `net_state_queue_test`、`net_entity_test`、
|
||||
`net_classic_session_test` 和 `git diff --check` 均通过。
|
||||
|
||||
本合同仍为 `PARTIAL`:真实 WALK/RUN 根运动资源、技能可取消/grade 权威数据、双人
|
||||
表情目标、TEMP_Push 地图碰撞和真实服务器时序尚未达到同一实现证据。
|
||||
@@ -0,0 +1,152 @@
|
||||
# network.character_select.create_delete
|
||||
|
||||
## Scope
|
||||
|
||||
对比 40250 角色列表、固定槽位、帝国选择、角色选择、创建、删除、强制改名和 DirectEnter 的完整网络调用链、固定字段、阶段副作用、回调顺序和失败清理。
|
||||
|
||||
本轮是静态审计、阶段副作用核对和现有测试复核;角色选择主路径已有多轮实现修复,合同仍为 `PARTIAL`。
|
||||
|
||||
## Reference call chain
|
||||
|
||||
### 普通登录到角色列表
|
||||
|
||||
1. `CPythonNetworkStream::SetLoginPhase()` 在 `UserInterface/PythonNetworkStreamPhaseLogin.cpp` 发送 `CG_LOGIN`/`CG_LOGIN2`,随后 `ClearLoginInfo()` 清除密码。
|
||||
2. `CPythonNetworkStream::LoginPhase()` 接收 `HEADER_GC_LOGIN_SUCCESS3/4`、`HEADER_GC_EMPIRE`、创建/删除/改名结果。
|
||||
3. `__RecvLoginSuccessPacket3/4()` 把数据写入固定的 `m_akSimplePlayerInfo[PLAYER_PER_ACCOUNT4]` 和对应 guild 数组;3 槽报文只覆盖前 3 个槽,未覆盖的第 4 槽仍是已清零的空槽。
|
||||
4. `RecvPhasePacket()` -> `SetSelectPhase()`;普通路径根据 `IsSelectedEmpire()` 进入选人或选帝国窗口。
|
||||
5. `SelectPhase()` 分派创建、删除、改名和再次登录成功包;UI 回调读取同一份固定槽位数据。
|
||||
|
||||
### 普通选人/创建/删除/改名
|
||||
|
||||
- `SendSelectEmpirePacket()` 写入 `CG_EMPIRE`,发送成功后立即 `SetEmpireID(dwEmpireID)`,再 `SendSequence()`。
|
||||
- `SendSelectCharacterPacket()` 写入 `CG_PLAYER_SELECT`,再 `SendSequence()`。
|
||||
- `SendCreateCharacterPacket()` 写入 slot/name/job/shape/CON/INT/STR/DEX 固定字段,再 `SendSequence()`。
|
||||
- `SendDestroyCharacterPacket()` 用 `strncpy(..., PRIVATE_CODE_LENGTH-1)` 写入删除码,再 `SendSequence()`。
|
||||
- `SendChangeNamePacket()` 写入 slot/name,再 `SendSequence()`。
|
||||
- `__RecvPlayerCreateSuccessPacket()` 在 `< PLAYER_PER_ACCOUNT4` 时更新固定槽位并调用 `OnCreateSuccess()`;越界只记录错误并返回,不产生成功回调。
|
||||
- `__RecvPlayerCreateFailurePacket()` 同时通知创建窗口和选人窗口。
|
||||
- `__RecvPlayerDestroySuccessPacket()` 清零整个槽位,并独立清零 guild id/name,然后调用 `OnDeleteSuccess(index)`。
|
||||
- `__RecvPlayerDestroyFailurePacket()` 消费空包并调用 `OnDeleteFailure()`。
|
||||
- `__RecvChangeName()` 只在 PID 命中固定槽位时清除 `bChangeName`、更新名称并调用 `OnChangeName(index,name)`;PID 不命中时调用创建失败码 `100`。
|
||||
|
||||
### DirectEnter
|
||||
|
||||
1. Python `net.DirectEnter(slot)` -> `CPythonNetworkStream::ConnectGameServer(slot)`。
|
||||
2. `ConnectGameServer()` 校验 `< PLAYER_PER_ACCOUNT4`,保存所选槽位,设置 `__DirectEnterMode`,按该槽的 `lAddr:wPort` 建立新的 game 连接。
|
||||
3. 新连接握手和 `SetLoginPhase()` 仍发送带 login key 的 `CG_LOGIN2`,不是普通选人窗口路径;登录信息随后被清除。
|
||||
4. `SetSelectPhase()` 在 DirectEnter 模式只切换到 Loading UI,不调用普通 `SendSelectCharacterPacket()`;DirectEnter 标记在 `SetLoadingPhase()` 中初始化清除。
|
||||
5. `GC_WARP` 也通过 `__DirectEnterMode_Set(m_dwSelectedCharacterIndex)` 后连接目标 `lAddr:wPort` 复用这条路径。
|
||||
|
||||
## Current call chain
|
||||
|
||||
- `extension/src/net/classic/classic_session.cpp::on_phase()`、`select_char()`、`create_character()`、`delete_character()`、`change_name()`、`send_empire()`、`connect_direct_enter()`。
|
||||
- `extension/src/net/classic/classic_parser.cpp::parse_login_success()`、`on_gc()`。
|
||||
- `extension/src/net/classic/wire_classic.h` 的固定包结构和 `is_sequence_cg()`。
|
||||
- `extension/src/net/m2_client.cpp::select_character()`、`enter_game()`、CRUD 转发、`pump_classic()` 事件分派和 `build_char_list()`。
|
||||
- `project/app_flow.gd::_on_char_list()`、`_build_char_list()`、`_enter_character()`、`_forward_char_evt()`。
|
||||
- `project/ui/char_select_screen.gd::_pad_slots()`、`_do_start()`、创建/删除/改名回调。
|
||||
|
||||
## Branch-by-branch comparison
|
||||
|
||||
| 场景 | 40250 参考行为 | 当前实现 | 结论 |
|
||||
|---|---|---|---|
|
||||
| 4 槽登录成功 | 写入 4 个固定槽位,空槽仍有 index 2/3 和全字段零值 | `parse_login_success(..., n=4)` 重建 4 个 `CharSlot`;主路径等价 | MAPPED |
|
||||
| 3 槽登录成功 | 写入固定 4 槽数组的前 3 项,第 4 项保持清零空槽 | `parse_login_success(..., PLAYER_PER_ACCOUNT3)` 仍分配 4 个零初始化槽,`slot_count()`/`build_char_list()` 暴露 4 槽 | MAPPED |
|
||||
| 空槽建号 | 固定槽位 UI 允许空槽,创建成功写回该槽 | 4 槽数组和 UI 补槽均保留空槽 index,创建回包按 slot 写回 | MAPPED;真实服务端异常仍缺包序证据 |
|
||||
| 帝国选择 | `CG_EMPIRE` 发送成功后立即本地写入 empire,再发 sequence | `send_empire()` 只发包;当前 `m_empire` 只有收到 `GC_EMPIRE` 后才改变 | GAP:本地状态写入时点不同 |
|
||||
| 普通选人 | UI 调用 `SendSelectCharacterPacket`,包后有 sequence | `select_char()` 在 `CharSelect` 且 slot 在当前 vector 范围内时发送固定包;stream 自动追加 sequence | MAPPED,重复触发/空槽语义缺少测试 |
|
||||
| 创建请求 | 固定 name/job/shape/四维字段,发 sequence;参考发送函数本身不做 slot 空闲判定;中文名称按 GB2312 字节字段发送 | 当前做 stage、slot、name 和整数范围校验,先将 UTF-8 名称转成 GB2312/GBK,再按 24 字节 wire cap 填包 | MAPPED:中文 40250 字节容量已对齐,前置校验/真实包序仍需继续证明 |
|
||||
| 创建成功 | `<4` 才更新固定槽并回调;越界只消费包,不产生成功回调 | `account_slot >= m_slots.size()` 只消费包并返回;有效槽才更新并排入 `CreateOk` | MAPPED |
|
||||
| 创建失败 | 同时通知创建窗口和选人窗口 | 一个 `char_create_failed` 信号由当前选人页转发 | PARTIAL:单窗口适配存在,但缺少双接收者/顺序证明 |
|
||||
| 删除请求 | 删除码最多复制 7 个字节,固定包后发 sequence | `strncpy(..., PRIVATE_CODE_LENGTH-1)`,零初始化尾部后发送 | MAPPED:删除码边界已对齐 |
|
||||
| 删除成功 | 清零槽位和 guild id/name 后回调 slot | `CharSlot{}` 清空所有字段后排入 `DeleteOk`;有效 4 槽时等价 | MAPPED,3 槽第 4 槽和异常 slot 未覆盖 |
|
||||
| 删除失败 | 消费空包并回调 `OnDeleteFailure()` | 解析为 `DeleteFail`,由 UI 清空输入框 | MAPPED,需补固定包/连续包测试 |
|
||||
| 改名成功 | 按 PID 命中槽位后清除强制改名并回调 index/name | 按 PID 更新槽位并排入 name event;UI 再发整表和 PID/name | PARTIAL:event 适配存在,index 语义和顺序未完全保持 |
|
||||
| 改名 PID 不命中 | 调用创建失败码 100 | 未命中时排入 `CreateFail(100)`,不产生 name event | MAPPED |
|
||||
| DirectEnter 目标连接 | 设置 DirectEnter,连接角色槽 `lAddr:wPort`,重新走 `CG_LOGIN2`;DirectEnter 的 select 阶段不发普通选择包 | 连接槽地址并保留 login key;在 `PHASE_SELECT` 中只切 Loading,不发送 `CG_PLAYER_SELECT` | MAPPED 主分支;真实服务器时序仍未证明 |
|
||||
| DirectEnter 重复进入 | 参考 API 通过单一网络流状态和 UI phase 控制 | 当前 `enter_game()`/`connect_direct_enter()` 没有 native epoch/重复请求保护 | PARTIAL |
|
||||
| 旧数据清理 | 登录阶段先清固定角色/guild 数组;离线/Loading 也清 DirectEnter 标记 | 新的 `ClassicSession` 通常从空 parser 开始;复用/失败/断线时没有同等固定槽和事件队列清理合同 | PARTIAL |
|
||||
|
||||
## Equivalence matrix
|
||||
|
||||
| 项目 | 状态 | 证据 |
|
||||
|---|---|---|
|
||||
| Preconditions | PARTIAL | 当前有 stage/slot/name/range 校验,但 40250 的固定 4 槽、空槽、DirectEnter 和重复调用边界未完全一致 |
|
||||
| Branch structure | PARTIAL | CRUD 主分支存在;3 槽固定数组、未命中改名、DirectEnter select 分支不同 |
|
||||
| Algorithms/formulas | PARTIAL | 包字段和 sequence 基本一致;classic 固定名称现在按 UTF-8 -> GB2312/GBK 转换后的字节截断,非中文 locale 和完整异常矩阵仍未闭合 |
|
||||
| State transition order | PARTIAL | 帝国本地写入时点、创建/删除/改名事件和 DirectEnter 选择包顺序不同 |
|
||||
| Constants/units | PARTIAL | 私码 8 字节、角色名 24 GB2312 字节 cap、3/4 槽暴露规则已定位;其它 locale 和异常边界仍未完全证明 |
|
||||
| Timing/event sources | PARTIAL | 参考 phase/window callback;当前为 parser queue -> `M2Client` signal -> Godot UI,未证明所有成功/失败顺序等价 |
|
||||
| Resource/data sources | MAPPED | 角色槽、guild、login key、角色 game 地址均已定位到对应报文/状态来源 |
|
||||
| Protocol side effects | PARTIAL | 普通 CRUD 包和 sequence 有;帝国本地副作用、越界响应和 DirectEnter 额外 `CG_PLAYER_SELECT` 不同 |
|
||||
| Interruption/failure/cleanup | PARTIAL | 创建/删除/改名异常、连接中断、重复进入和角色实例管理器生命周期缺少完整自动测试 |
|
||||
|
||||
## Existing test evidence
|
||||
|
||||
通过的现有回归:
|
||||
|
||||
- `extension/tests/net_classic_session_test.cpp` / `build/extension/net_classic_session_test`:4 槽成功列表、强制改名成功、创建失败包兼容、删除码 7 字节边界、普通选人、槽地址连接。
|
||||
- 同一测试还覆盖 legacy 3 槽保持第 4 个空槽、DirectEnter SelectPhase 不发送普通选择包、未知改名 PID 的 `CreateFail(100)`,以及 Select phase 外角色回包被拒绝。
|
||||
- `extension/tests/net_loopback_test.cpp` / `build/extension/net_loopback_test`:创建/删除/改名的经典 loopback 主路径。
|
||||
- `project/char_create_delete_test.gd`:Godot 选人页 UI 创建、删除、改名和帝国按钮流程。
|
||||
- `project/test_intro_select_parity.gd`:选人页及删除码 UI 的既有测试。
|
||||
|
||||
这些测试没有覆盖:3 槽登录后的固定第 4 槽、空槽 slot 3 的创建/删除结果、非中文 locale codepage、未命中 PID 的改名错误码、DirectEnter 在 `PHASE_SELECT` 的包序列、重复进入和中断清理。因此通过结果不能提升合同状态。
|
||||
|
||||
## Deep audit round 2026-09-20
|
||||
|
||||
本轮重跑角色选择/CRUD/loopback 回归,并把 native 代码中的 DirectEnter 与固定槽位行为重新对照:
|
||||
|
||||
- `char_create_delete_test.gd`、`test_intro_select_parity.gd`、`net_classic_session_test` 和 `net_loopback_test` 均退出码 0;它们证明当前 4 槽主路径、UI 创建/删除/改名、普通选人和 loopback CRUD 没有回归。
|
||||
- `ClassicParser::parse_login_success()` 仍按报文 `n` 清空并创建 vector;legacy 3 槽报文因此不会保留 reference 固定 4 槽数组中的 slot 3 空项。当前 UI 测试使用的是补齐后的 4 槽 fixture,不能覆盖真实 3 槽包。
|
||||
- `ClassicSession::on_phase(PHASE_SELECT)` 在 `m_direct_enter` 时先切 Loading,但仍调用 `send_select_char(m_direct_enter_slot)`;参考 DirectEnter 的 Select phase 只切 Loading,不发送普通 `CG_PLAYER_SELECT`,所以当前多发包差异仍存在。
|
||||
- 创建成功即使 `account_slot >= m_slots.size()` 也会排入 `CreateOk`;改名 PID 未命中时当前只排入 name event,不生成参考创建失败码 100。帝国选择也没有参考端“发送成功后立即写本地 empire”的时点。
|
||||
- 当前角色名、改名和创建字段仍经 UTF-8 `to_wire`,本轮安全编码审计已确认与 Windows locale codepage 不是同一语义;重复 `enter_game`/DirectEnter 失败和中断清理仍没有独立状态 epoch。
|
||||
|
||||
结论:4 槽/legacy 3 槽固定槽、DirectEnter 不重复选人、越界创建和未命中改名回调、帝国写入时点已按 40250 对齐;locale 字节、双窗口回调/连续异常包、重复进入/中断清理、角色实例管理器生命周期和真实服务端包序仍与 40250 不一致,合同保持 `PARTIAL`。
|
||||
|
||||
## Required tests before verification
|
||||
|
||||
- 3 槽/4 槽登录成功后都暴露固定 4 个槽位,空槽 index、guild 和角色字段全部清零。
|
||||
- 在 3 槽协议下创建/删除 slot 3,证明参考行为与当前行为的边界;若协议确实禁止,记录服务器分支证据而不是用 vector 长度推断。
|
||||
- 创建/改名名称在 0、最大字节、最大字节+1、非 ASCII、locale codepage 不可逆字符下的实际 wire bytes 与参考比较。
|
||||
- 创建成功越界、创建失败双窗口、删除成功/失败连续包的状态和事件顺序。
|
||||
- 改名 PID 命中和未命中(错误码 100)以及强制改名状态清理。
|
||||
- DirectEnter 从初始 game 连接到槽地址的完整阶段和 wire 序列,确认 `PHASE_SELECT` 是否可达以及是否应发送 `CG_PLAYER_SELECT`;增加重复 `enter_game`、连接失败和中断后的清理断言。
|
||||
|
||||
### Active UserInterface character-manager review
|
||||
|
||||
- `AbstractCharacterManager.h`/`PythonCharacterManager.h` 把 VID→`CInstanceBase` 实例表、主实例、选中实例、fade/dead list、设备对象、point effect、transform update 和销毁顺序作为角色管理器职责;`PythonCharacterManagerModule.cpp` 还注册 RaceData 的 motion mode、weighted motion、normal/combo attack、attach bone、shape 和 motion resource。
|
||||
- `PythonCharacterModule.cpp` 的创建/注册/删除/淡出/选择、装备/发型/武器、位置/方向/动作、render mode、affect、bound box 和 faint 起身队列构成角色选择后到游戏内的统一实例入口。当前端角色选择和 `NetWorld` entity view 能显示/删除角色,但没有同一 manager 的 dead/fade/device/motion registration 生命周期。
|
||||
- 本轮完成活跃角色管理源码静态核对;现有 character select 测试和网络包测试不能替代参考端实例表、重复 VID、fade 删除、主实例切换和进入游戏清理语义,合同保持 `PARTIAL`。
|
||||
|
||||
## Implementation fix round 2026-09-21
|
||||
|
||||
按 40250 的固定数组和 phase 分支修复当前 classic 客户端:legacy `GC_LOGIN_SUCCESS3` 现在只填充前 3 项但始终保留 4 个零初始化槽位;创建成功的越界槽不再产生成功事件;未知 PID 的改名回包改为创建失败码 `100`;`SendSelectEmpirePacket` 对齐为写包成功后立即更新本地 empire;DirectEnter 的 `PHASE_SELECT` 只切换 Loading,不再额外发送普通 `CG_PLAYER_SELECT`。
|
||||
|
||||
新增/更新 native 边界回归覆盖 legacy 三槽固定槽、帝国本地状态时点、未知改名 PID 和 DirectEnter 包序。`cmake --build build -j2`、`ctest --test-dir build --output-on-failure`(23/23)、`char_create_delete_test.gd`、`test_intro_select_parity.gd`、`p10_test.gd`、`app_flow_lifecycle_test.gd` 和 `git diff --check` 均通过。
|
||||
|
||||
剩余 locale codepage 字节语义、创建/删除异常和双窗口回调、DirectEnter 重复进入/中断清理、角色实例管理器生命周期以及真实服务端包序仍未达到 40250 统一语义,合同保持 `PARTIAL`。
|
||||
|
||||
## Audit round 2026-09-20 — Select-phase side-effect gate
|
||||
|
||||
继续核对 `PythonNetworkStreamPhaseLogin.cpp::LoginPhase`、
|
||||
`PythonNetworkStreamPhaseSelect.cpp::SelectPhase` 与当前 `ClassicSession::on_packet`:40250 的角色创建、删除、强制改名回包只在 SelectPhase 的分支中消费;错误阶段不会进入固定槽位和 UI 回调。当前端此前由 phase-neutral parser 直接接收这些 header,延迟的创建/改名包可能在 Login/Loading 阶段改写角色列表。
|
||||
|
||||
本轮修复 `ClassicSession::on_packet` 的阶段门禁:
|
||||
|
||||
- `GC_PLAYER_CREATE_SUCCESS`、`GC_PLAYER_CREATE_FAILURE`、`GC_PLAYER_DELETE_SUCCESS`、`GC_PLAYER_DELETE_WRONG_SOCIAL_ID`、`GC_CHANGE_NAME` 只有 `CharSelect` 或 DirectEnter 尚处协议 SelectPhase 时才进入 parser。
|
||||
- 错误阶段返回明确错误并断开当前 phase owner,避免角色槽、改名状态和 UI 事件产生错误副作用。
|
||||
- DirectEnter 的 UI stage 已是 `Loading`,但 `m_direct_enter` 仍作为协议 SelectPhase 标志保留,确保其合法的阶段边界不被误拒绝。
|
||||
|
||||
回归:`net_classic_session_test`、`net_classic_stream_test`、`net_classic_wire_test`、`net_loopback_test`、`char_create_delete_test.gd`、`test_intro_select_parity.gd`、`p10_test.gd` 和 `git diff --check` 均通过。
|
||||
|
||||
这只闭合了角色 CRUD 回包的阶段副作用;Login/Select/Loading/Game 的完整 117-header allowlist、burst tick 消费顺序、locale codepage、双窗口回调和真实服务端包序继续由 `network.packet_dispatch_protocol`/本合同跟踪,状态保持 `PARTIAL`。
|
||||
|
||||
## Implementation fix round 2026-09-21 — 40250 Chinese name bytes
|
||||
|
||||
角色创建和改名入口保持 UTF-8 业务字符串,但实际 `CG_PLAYER_CREATE`/`CG_CHANGE_NAME` 固定字段改由 `mtnet::to_wire()` 写入 GB2312/GBK bytes。`wire_text_fits()` 在编码后按 `CHARACTER_NAME_MAX_LEN=24` 校验:12 个中文字符可发送,13 个中文字符在客户端被拒绝;结构体零初始化保证第 25 个字节为 NUL。`net_text_codec_test` 覆盖独立转换,`net_classic_session_test` 直接检查创建/改名 packet bytes,避免只测试 helper 而漏掉真实发送入口。
|
||||
|
||||
## Status
|
||||
|
||||
`PARTIAL`。
|
||||
@@ -0,0 +1,86 @@
|
||||
# network.error_timeout_server_state
|
||||
|
||||
## Scope
|
||||
|
||||
对比认证失败、连接失败、超时、频道状态检查、服务器状态通知和恢复/回到登录界面的完整分支。
|
||||
|
||||
## Reference call chain
|
||||
|
||||
- `UserInterface/AccountConnector.cpp`
|
||||
- `UserInterface/ServerStateChecker.cpp`
|
||||
- `UserInterface/PythonNetworkStreamPhaseLogin.cpp`
|
||||
- `EterLib/NetStream.cpp`
|
||||
|
||||
## Current call chain
|
||||
|
||||
- `extension/src/net/auth_client.h`
|
||||
- `extension/src/net/classic/classic_session.cpp`
|
||||
- `project/net/channel_status.gd`
|
||||
- `project/net/serverinfo.gd`
|
||||
- `project/app_flow.gd`
|
||||
- `project/ui/reconnect_ui.gd`
|
||||
|
||||
## Branch matrix
|
||||
|
||||
| 场景 | 40250 | 当前端 | 结论 |
|
||||
|---|---|---|---|
|
||||
| auth 失败 | AccountConnector 消费错误并通知 login UI,断开 auth/game 状态 | `login_failed` 返回 AppFlow 登录页;旧 session 清理顺序未完全证明 | PARTIAL |
|
||||
| 连接超时 | `Process`/connector failure 进入 offline/disconnect | `ClassicStream` 与 `NetStream` 均保留 3 秒 non-blocking deadline,并在 socket 可写后再用 `SO_ERROR` 确认连接完成 | MAPPED:仍缺少所有上层失败通知组合证明 |
|
||||
| channel status | `ServerStateChecker` 请求首 channel,解析 count 和 `{port,status}`,逐频道通知 | 当前查询首频道,解析 classic 210;失败时 fallback TCP probe | PARTIAL:功能相近但多了 approximation |
|
||||
| 部分响应 | NetStream 保留接收缓存等待完整状态包 | 当前已缓存状态 body,遇到半条记录会继续等待后续 TCP 数据 | MAPPED:分片行为已补回归测试 |
|
||||
| 空/非法 count | 由包长度和状态处理逻辑拒绝 | `_try_parse_classic` 检查范围;body 内仍需完善负值/超大值保护 | PARTIAL |
|
||||
| 恢复 | 失败后按 connector/server state 回调回到正确 phase | 当前由 AppFlow signal 和 ReconnectUI timer 驱动 | PARTIAL |
|
||||
|
||||
## Equivalence matrix
|
||||
|
||||
| 项目 | 状态 | 证据 |
|
||||
|---|---|---|
|
||||
| Preconditions | PARTIAL | 认证/连接和状态请求有检查;完整失败通知、远端关闭与上层重入仍需补齐 |
|
||||
| Branch structure | PARTIAL | auth/channel/fallback 主分支存在,失败通知和超时分支不同 |
|
||||
| Algorithms/formulas | MAPPED | classic count/record 解析和分片 body 累积已有实现与测试 |
|
||||
| State transition order | PARTIAL | 当前 signal/UI retry,参考 connector/phase offline 回调顺序不同 |
|
||||
| Constants/units | MAPPED | classic/m2dev socket 与 channel probe 均为 3 秒;状态 count 上限为 4096,仍需边界夹具覆盖 |
|
||||
| Timing/event sources | PARTIAL | timer/fallback probe 与参考 network process/state checker 不同 |
|
||||
| Resource/data sources | PARTIAL | serverinfo 推导端口,参考接收显式 channel entries |
|
||||
| Protocol side effects | PARTIAL | 206/210 classic 包存在;fallback TCP probe 是当前扩展副作用 |
|
||||
| Interruption/failure/cleanup | PARTIAL | 连接中断、重复 probe、重试终止仍缺少测试 |
|
||||
|
||||
## Deep audit round 2026-09-20
|
||||
|
||||
本轮沿 40250 `AccountConnector::Connect/Process`、`CServerStateChecker::Request/Update`、`CPythonNetworkStream::LoginPhase` 和 `CNetworkStream::Process/Connect` 逐分支核对,得到以下结论:
|
||||
|
||||
1. 40250 的认证连接通过 `CNetworkStream::Connect` 建立非阻塞 socket,`Process` 用 socket 可写事件确认连接成功,并使用 `m_connectLimitTime` 超时清理;当前 `NetStream` 与 `ClassicStream` 现在都保留 3 秒 deadline,且只有可写事件后才用 `SO_ERROR` 确认完成。连接超时/远端错误的上层通知组合仍需扩展测试。
|
||||
2. 40250 `CAccountConnector::__AuthState_RecvPhase` 发送 `CG_LOGIN3` 后立即 `ClearLoginInfo()` 清掉密码;当前 `AuthClient` 在构造登录包后清零并清空 `m_pw`,`M2Client::pump_auth()` 同步丢弃 host-side `cfg_pw`,没有 login key 时的空密码重连会被拒绝。该边界已有 loopback 断言。
|
||||
3. 40250 的 `ServerStateChecker::Request` 只连接首频道并发送一次 `HEADER_CG_STATE_CHECKER`,连接/发送失败时将所有已登记频道通知为关闭;`Update` 收到 `HEADER_GC_RESPOND_CHANNELSTATUS` 后按端口通知并 `Initialize()`。当前 `AppFlow::_probe_channels` 使用 `ChannelStatus` 查询首频道,未知端口则逐频道做额外 TCP probe,属于当前端的 approximation,不是 1:1 分支;多个“检测频道”点击可以并行创建查询节点,未见 in-flight/cancel/generation 保护。
|
||||
4. 当前状态查询的 206/210 classic 解析已累积 body,负数和大于 4096 的 count 会拒绝;`ChannelStatus` 现在会先取消旧 probe,并在回调重入时固定旧 callback,避免旧结果投递给新请求。自动测试仍未覆盖连接后无响应超时、count=0/截断/尾随包及远端关闭。默认协议和 `MT_PROTOCOL=classic` 两次测试均通过。
|
||||
5. 当前 AppFlow 登录失败/断线的 signal 分类存在,但账号/游戏连接失败后由 `M2Client` 直接 reset session,再由 AppFlow 重建 Login;这与 40250 的统一 Offline phase / `SetLoginPhase` 顺序不同。`p10_test` 还暴露了 Loading→Game 未隐藏和无 SceneTree 时 char-select timer 空树的测试/生命周期问题,恢复链不能标记为完成。
|
||||
|
||||
## Required tests
|
||||
|
||||
- auth success/failure、connect refusal、connect timeout、remote close。
|
||||
- 206/210 完整包、分片包、半条 record、count=0、负值和超大 count。
|
||||
- channel status 失败后的 fallback、重试上限和回到登录页。
|
||||
- 同一 session 同时 login/reconnect/probe 的竞态与清理。
|
||||
|
||||
## Evidence run
|
||||
|
||||
- `godot --headless --path project --script channel_status_test.gd`:PASS。
|
||||
- `MT_PROTOCOL=classic godot --headless --path project --script channel_status_test.gd`:PASS,覆盖 40250 classic 的 206 请求和 210 响应夹具。
|
||||
- 认证 loopback 成功路径、密码清除、挂起连接 deadline 已在 `extension/tests/net_loopback_test.cpp` 覆盖;classic transport 的同等 deadline/可写门禁在 `extension/tests/net_classic_stream_test.cpp` 覆盖。
|
||||
- 当前合同继续 `PARTIAL`:通过的状态包解析测试不能替代 40250 的认证失败、远端关闭、失败通知顺序、count 边界和完整恢复链证据。
|
||||
|
||||
## Active UserInterface server-state checker review
|
||||
|
||||
- `ServerStateChecker.h`/`ServerStateCheckerModule.cpp` 维护独立 `CNetworkStream`、channel 地址/端口表、Create/AddChannel/Request/Update/Initialize 和 Python window 回调;它是登录前服务器状态探测链,不等同于游戏 session 的重连。
|
||||
- 当前端 channel status 有 classic packet smoke test,且单个 `ChannelStatus` 的旧 socket/重入 callback 已有清理逻辑;仍没有证明多 channel 请求的超时、远端关闭、失败通知顺序、窗口销毁后回调和与 login phase 的竞态清理一致;本轮纳入 active source 证据,合同仍为 `PARTIAL`。
|
||||
|
||||
## Implementation fix round 2026-09-20
|
||||
|
||||
- `AuthClient::on_phase(PHASE_AUTH)` 先把密码拷贝进 `CG_LOGIN3`,随后显式清零并清空 `m_pw`,再检查 `send_packet()`;`M2Client::pump_auth()` 看到同一边界后清空 `cfg_pw`,`reconnect()` 在没有 login key 时拒绝空凭据重试。
|
||||
- `NetStream` 和 `ClassicStream` 都增加 3000ms non-blocking connect deadline;classic 路径改为 `select` 可写后再 `SO_ERROR`,修复第一次 process 把仍在连接中的 socket 误报 Online 的差异。
|
||||
- `ChannelStatus` 取消旧 probe、清空 mutable callback 后再发 `done`,支持 callback 内启动下一次查询;`AppFlow` 增加 channel probe in-flight guard。
|
||||
- 新增/扩展 `net_loopback_test`、`net_classic_stream_test`、`channel_status_test.gd`,覆盖凭据清除、挂起连接 deadline 和 re-entrant probe callback;成功路径及默认/classic 两套状态包夹具通过。
|
||||
|
||||
## Status
|
||||
|
||||
`PARTIAL`。
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,235 @@
|
||||
# network.packet_dispatch_protocol
|
||||
|
||||
## Scope
|
||||
|
||||
核对 40250 的完整 GC 包注册表、静态/动态分帧、阶段 allowlist、包头 padding、未知包与已知未处理包的副作用、一次 pump 的消费数量,以及 ping/sequence 的发送顺序。这里的“已实现”要求完整调用链的分支、消费长度、状态变化和失败清理都等价,只有同名 `case` 或能找到解析函数不能算等价。
|
||||
|
||||
## Reference call chain
|
||||
|
||||
- `UserInterface/Packet.h`
|
||||
- `UserInterface/PythonNetworkStream.cpp::CMainPacketHeaderMap`
|
||||
- `UserInterface/PythonNetworkStream.cpp::CheckPacket`
|
||||
- `UserInterface/PythonNetworkStream.cpp::RecvErrorPacket`
|
||||
- `UserInterface/PythonNetworkStream.cpp::RecvPhasePacket`
|
||||
- `UserInterface/PythonNetworkStream.cpp::RecvPingPacket`
|
||||
- `UserInterface/PythonNetworkStream.cpp::OnProcess`
|
||||
- `UserInterface/PythonNetworkStreamPhaseLogin.cpp::LoginPhase`
|
||||
- `UserInterface/PythonNetworkStreamPhaseSelect.cpp::SelectPhase`
|
||||
- `UserInterface/PythonNetworkStreamPhaseLoading.cpp::LoadingPhase`
|
||||
- `UserInterface/PythonNetworkStreamPhaseGame.cpp::GamePhase`
|
||||
- `EterLib/NetPacketHeaderMap.cpp`
|
||||
- `EterLib/NetStream.cpp`
|
||||
|
||||
`CMainPacketHeaderMap` 的有效表实际有 104 项静态、13 项动态,共 117 个有效 GC 注册项。源码中另有两行被注释掉的 `Set`(`SLOW_TIMER`、旧 `TARGET_CREATE`),不计入运行时注册表。动态项包括 `QUEST_INFO`、`DUEL_START`、`CHAT`、`SYNC_POSITION`、`SHOP`、`SCRIPT`、`MESSENGER`、`GUILD`、`DUNGEON`、`NPC_POSITION`、`LAND_LIST`、`HYBRIDCRYPT_KEYS` 和 `HYBRIDCRYPT_SDB`;`WHISPER` 在 40250 主表中是静态注册项,文本尾部由 `RecvWhisperPacket` 在固定头消费后再按 `wSize` 消费。
|
||||
|
||||
## Current call chain
|
||||
|
||||
- `extension/src/net/classic/classic_stream.cpp::process`
|
||||
- `extension/src/net/classic/classic_stream.cpp::feed`
|
||||
- `extension/src/net/classic/classic_stream.cpp::dispatch`
|
||||
- `extension/src/net/classic/classic_stream.cpp::handle_control`
|
||||
- `extension/src/net/classic/classic_session.cpp::on_packet`
|
||||
- `extension/src/net/classic/classic_parser.cpp::on_gc`
|
||||
- `extension/src/net/classic/wire_classic.h::packet_size_gc`
|
||||
- `extension/src/net/classic/wire_classic.h::is_dynamic_gc`
|
||||
- `extension/src/net/classic/wire_classic.h::is_sequence_cg`
|
||||
- `extension/src/net/net_stream.cpp`
|
||||
|
||||
## Reference branch matrix
|
||||
|
||||
| 场景 | 40250 实现 | 当前端实现 | 结论 |
|
||||
|---|---|---|---|
|
||||
| 包头查表 | `CMainPacketHeaderMap::Get` 只接受主表中的 117 个 GC header;未注册 header 清空接收缓冲并 `PostQuitMessage(0)` | 当前主游戏流已按数值覆盖参考 117/117 个 header;仅独立 guild-mark `SYMBOL_DATA=133` 保留额外动态适配;未知普通流 header 回调错误并断流 | PARTIAL:注册数值和静态/动态类别已对齐,`sizeof`/别名布局和应用级退出副作用仍未完全证明 |
|
||||
| 静态包 | 使用 `sizeof(TPacket...)` 的完整包长;不足保留缓冲等待 | `packet_size_gc` 当前有 104 个静态 case,按数值对应参考 104 个静态注册项;不足保留缓冲等待 | PARTIAL:当前表达式与参考 `sizeof` 的逐项布局证明仍未完成 |
|
||||
| 动态包 | 先读取 `TDynamicSizePacketHeader`,以 wire `wSize` 判断完整包;随后由具体 handler 再消费字段尾部 | 以 `DynHead.size` 分帧;`QUEST_INFO` 按 flag 重算实际服务端尾部,`GUILD/SKILL_INFO` 保留 40250 服务端少写一字节的兼容长度;字段尾部不足时 parser 返回失败 | PARTIAL:特殊包的边界、过大值、声明值与实际消费值仍不是同一算法,但截断 handler 已与 `Recv()` 失败路径对齐 |
|
||||
| `WHISPER` | 主表标为静态;先消费 `TPacketGCWhisper`,再按 `wSize - sizeof(header)` 消费文本 | `packet_size_gc` 使用固定 `GCWhisperHead`,静态分派先校验完整 `wSize`,回调 parser 时去掉 `[header][wSize]` | MAPPED:固定头、文本尾和后续包边界已对齐 |
|
||||
| 前导零 | `CheckPacket` 只在检查下一个 header 时剥掉连续零;遇到第一个非零停止;全是零则等待更多数据 | `dispatch` 只剥掉当前前导零 run,零耗尽时等待下一次读取,遇到非零后再查表 | MAPPED:已覆盖 zero-only、zero-run+valid packet 回归 |
|
||||
| 阶段 allowlist | Login、Select、Loading、Game 各自 switch;未命中走 `RecvDefaultPacket/RecvErrorPacket`;Loading 的 default 才委托一次 `GamePhase` | `ClassicSession::on_packet` 已收口 Login/Select/DirectEnter;Loading 使用 Loading 专属集并回退 Game 集;Game 使用显式 Game 集;错误阶段在当前帧后清空剩余接收缓冲 | PARTIAL:完整 117 项映射、handler 失败清理和所有分支仍未逐项证明 |
|
||||
| Login allowlist | `PHASE/LOGIN_SUCCESS3/4/FAILURE/EMPIRE/MATRIX/PASSPOD/LOGIN_KEY/PING/HYBRID*` | 可解析的 GC header 由统一 parser 尝试处理;可选集成包明确 false,其他不按 Login 表拒绝 | PARTIAL |
|
||||
| Select allowlist | `PHASE/EMPIRE/LOGIN_SUCCESS3/4/CREATE/DELETE/CHANGE_NAME/HANDSHAKE/HYBRID*/KEY_AGREEMENT*/POINT_CHANGE/PING`;`PLAYER_POINT_CHANGE` 只消费不更新角色属性 | Select 专用门禁已存在;`GC_PLAYER_POINT_CHANGE` 在 Select/DirectEnter 中只消费不写入 EntityStore,Loading/Game 才进入 parser | MAPPED |
|
||||
| Loading allowlist | `PHASE/MAIN_CHARACTER*/CHARACTER_UPDATE/PLAYER_POINTS/POINT_CHANGE/ITEM_SET(20)/PING/QUICKSLOT_ADD/HYBRID*`;default 进入一次 GamePhase | `loading_phase_packet` 先收 Loading 专属包;21 号 `ITEM_SET2` 和其他 GamePhase 包走一次 `game_phase_packet` fallback;HybridCrypt 由 ClassicStream 消费后返回 | MAPPED:阶段专属包、GamePhase fallback 和控制包 owner 已按数值/调用链对齐 |
|
||||
| Game allowlist | 只在 GamePhase switch 中派发游戏包,包含实体、战斗、道具、任务、商店、公会、仓库、效果等分支 | `game_phase_packet` 显式复刻 GamePhase 非控制分支;未命中不再进入统一 parser | PARTIAL:每个分支的副作用和 `RecvDefaultPacket` 清理仍未闭合 |
|
||||
| 一次 pump | `OnProcess` 每次只运行当前 phase process 一次;GamePhase 按 `dwRecvCount++ >= MAX_RECV_COUNT-1` 与 8192 字节阈值继续;PHASE/握手类控制包立即 return | `ClassicStream::dispatch` 在 Game 阶段缓冲低于 8192 字节时消费 3 个完整包后 return;phase/握手/初始密钥协商后立即 return,`KEY_AGREEMENT_COMPLETED` 会先解密已缓存字节并继续派发;Loading→Game fallback 仍不设 Game 限额 | PARTIAL:大于 8192 字节的阈值、精确 tick 驱动和全量 burst 事件矩阵仍未完全证明 |
|
||||
| 已知但 handler 失败 | phase handler 失败后调用 `RecvErrorPacket`,记录并清空接收缓冲;不由该分支直接断 socket | `on_packet == false` 报错后消费当前帧、清空剩余接收缓冲并保留 socket/phase owner;未知 header、底层 framing 错误仍走断流 | MAPPED:已对齐清缓冲与 owner 保留,应用层对可选集成错误的提示仍是适配差异 |
|
||||
| 未知 header | 清空接收缓冲并退出消息循环 | `on_error` 后断流到 Offline | PARTIAL:应用级退出副作用不同 |
|
||||
| 断包/乱序 | `Peek` 失败则保留缓冲等待;具体 handler 自己决定二次 `Recv` 失败 | framing 缓冲保留,但动态 underflow、未知 header 和 handler false 直接断流 | PARTIAL |
|
||||
| Ping/Pong | 消费一个 `GC_PING`;发送 `CG_PONG`;安全模式额外 `SendSequence()` | `handle_control` 消费 blank;`send_fixed` 在 sequence 模式附加序号 | MAPPED:该单分支的发送顺序和 sequence 条件一致 |
|
||||
|
||||
## Concrete table audit
|
||||
|
||||
### 40250 注册表已核对的结构性事实
|
||||
|
||||
- `CMainPacketHeaderMap` 共有 117 个有效 `Set(HEADER_GC_...)`,其中静态 104、动态 13;两行注释掉的 `Set` 不计入,数量由 `audit_packet_registry.py` 从当前参考源自动解析。
|
||||
- 其中 13 个使用 `DYNAMIC_SIZE_PACKET`,其余 104 个使用 `STATIC_SIZE_PACKET`。
|
||||
- `HEADER_GC_WHISPER` 是静态注册,不在动态表;文本是 handler 在固定头之后单独读取。
|
||||
- `HEADER_GC_SLOW_TIMER` 和 `HEADER_GC_TARGET_CREATE` 在 40250 源码中是注释掉的 `Set`,不能仅因为 `Packet.h` 中存在结构体就视为有效注册项。
|
||||
- `GamePhase` 中出现的 `OBSERVER_*`、攻击等分支不能反向证明它们已进入 `CMainPacketHeaderMap`;分帧有效性以 map 为准。
|
||||
|
||||
### 当前端逐项核对结果
|
||||
|
||||
- 当前 `packet_size_gc` 有 104 个 GC 尺寸 case;按数值已覆盖参考端 117 个注册项中的全部 104 个静态项,名称使用了 `LOGIN_SUCCESS_NEWSLOT`、`CHARACTER_CREATE_SUCCESS`、`CHARACTER_POINTS` 等协议别名;逐项 `sizeof/byte layout` 仍需机器化证明。
|
||||
- 当前 `is_dynamic_gc` 有 14 个 GC 分支,除 40250 的 13 项动态集合外只额外包含已登记的 `SYMBOL_DATA` 兼容路径;这不能与 40250 主表直接视为完全等价。
|
||||
- 当前 parser 有 112 个 GC case,数量大于主表并不代表覆盖正确,因为部分 case 是 optional integration 或其他版本兼容分支,且缺少 phase gate。
|
||||
- 当前 `packet_size_gc` 有 104 个明确静态 case,`is_dynamic_gc` 有 14 个 case;按数值与类别已覆盖参考 117/117 个注册项,唯一额外数值是独立 guild-mark 连接使用的 `HDR_GC_SYMBOL_DATA=133`,必须继续作为显式兼容差异保留。
|
||||
- 当前 `QUEST_INFO` 和 `GUILD/SKILL_INFO` 的特殊尺寸修正是当前端为服务端兼容而加入的行为;需要以真实 40250 包序列证明它不会改变声明尺寸、剩余包边界和失败清理。
|
||||
|
||||
## Equivalence matrix
|
||||
|
||||
| 项目 | 状态 | 证据 |
|
||||
|---|---|---|
|
||||
| Preconditions | PARTIAL | 静态/动态长度检查存在;动态 handler 的固定字段/记录尾部不足已返回失败,但动态最小头和所有声明值边界尚未完全机器化 |
|
||||
| Branch structure | PARTIAL | 静态/动态主分支存在;当前额外兼容分支和统一 parser 改变了 40250 的分派分支 |
|
||||
| Algorithms/formulas | PARTIAL | `QUEST_INFO`/`GUILD` 重算长度,40250 `CheckPacket` 以声明 `wSize` 做完整性判断 |
|
||||
| State transition order | PARTIAL | 当前一次 dispatch 可消费完整 burst;40250 每个 OnProcess tick 只运行一次 phase process |
|
||||
| Constants/units | PARTIAL | 当前尺寸单位均为 wire bytes,104 个静态 case/13 个主表动态 case 的数值类别已对齐,但与 40250 `sizeof` 别名的逐项证明未完成 |
|
||||
| Timing/event sources | PARTIAL | `feed` 立即 dispatch 且 socket process drain;40250 由 phase tick 驱动单包消费 |
|
||||
| Resource/data sources | MAPPED | 两侧主调用链、Packet/HeaderMap、wire table 均已定位 |
|
||||
| Protocol side effects | PARTIAL | Ping/Pong/sequence 一致;未知包、handler false、错误 phase 的清理/退出不同 |
|
||||
| Interruption/failure/cleanup | PARTIAL | 已知 handler false 现在清缓冲并保留 owner;动态字段截断会进入该路径;unknown/动态 framing underflow/底层 I/O 仍断流或终止当前连接,和 40250 的应用级退出副作用尚未完全统一 |
|
||||
|
||||
## Required parity tests
|
||||
|
||||
- 从 40250 `CMainPacketHeaderMap` 自动生成 117 项 `(header, sizeof, dynamic)` 清单,与当前 `packet_size_gc/is_dynamic_gc` 逐项比较;数值/类别覆盖已达到 117/117,下一步必须补齐别名的实际 `sizeof/byte layout` 对照,不能依赖人工推断。
|
||||
- 对 13 个动态包分别覆盖:只有固定头、`wSize == header`、`wSize < header`、声明长度大于缓冲区、声明长度后紧跟下一个包、handler 实际消费尾部不足。
|
||||
- 单独覆盖 `WHISPER` 的“固定头成功但文本尾部不完整”两段式行为,并确认文本后续包边界。
|
||||
- 对 Login/Select/Loading/Game 的每个 allowlist 做允许与拒绝矩阵,确认错误阶段不会调用世界/角色/道具副作用。
|
||||
- 同一缓冲区放入 1、2、N 个完整包,分别记录 40250 phase tick 与当前 dispatch 的事件顺序和消费数量。
|
||||
- 覆盖 leading zero、packet body zero、未知 header、已知但 handler false、dynamic underflow、静态截断、连续包和错误后剩余缓冲。
|
||||
|
||||
## Evidence run
|
||||
|
||||
- `build/extension/net_classic_stream_test`、`build/extension/net_classic_wire_test`、`build/extension/net_classic_session_test`:PASS,已覆盖固定头、文本尾分片、后续静态包边界、前导零、错误阶段拒绝,以及已知 handler 失败后保留 phase owner 并继续处理下一 tick。
|
||||
- `.agents/skills/metin2-40250-parity-audit/scripts/audit_packet_registry.py`:报告有效参考注册表 `117/104/13`,当前 `packet_size_gc=104`、`is_dynamic_gc=14`,数值覆盖 `117/117`,额外数值仅为已登记的 guild-mark `HDR_GC_SYMBOL_DATA=133`;报告仍为 `PARTIAL`,因为该独立适配和 `sizeof/byte layout` 证明尚未闭合。
|
||||
- `.agents/skills/metin2-40250-parity-audit/scripts/audit_phase_dispatch.py --strict`:按非 GAIDEN 数值比较 Login/Select/Loading/Game 四个阶段,排除由 ClassicStream 消费的控制包和独立 observer stream 后,四阶段均无缺失或额外 payload case;该结果只证明阶段分支结构,不证明每个 handler 的副作用。
|
||||
- `build/extension/net_classic_session_test`、`build/extension/net_classic_stream_test`:PASS,新增 Loading 专属/ Game fallback、Game 阶段拒绝 Loading 主角色包、HYBRIDCRYPT 丢弃消费和 phase 控制包后的 pipelined packet 边界回归。
|
||||
- `build/extension/net_classic_stream_test`:PASS,新增 Game 阶段四个小包只消费前三个、下一 tick 消费第四个的 `MAX_RECV_COUNT` 回归;旧版 85 字节 `HEADER_GC_MAIN_CHARACTER=15` 也已加入分帧/解析测试。
|
||||
- 以上测试尚未覆盖 117 项逐包尺寸/布局生成矩阵、每个 GamePhase 分支的允许/拒绝矩阵和 8192 字节以上 burst,因此不能将本合同提升为 `MAPPED` 或 `TEST_VERIFIED`。
|
||||
|
||||
## Audit/fix round 2026-09-22 — dynamic handler truncation
|
||||
|
||||
沿 40250 的动态包调用链复核 `CheckPacket -> Recv*Packet -> RecvErrorPacket` 后,修复了当前 parser 在“包头/声明长度已经被分帧,但具体字段尾部不足”时错误返回成功的问题:
|
||||
|
||||
- `SCRIPT`、`QUEST_INFO`、`MESSENGER`、`SHOP`、`GUILD`、`DUNGEON`、`NPC_POSITION`、`LAND_LIST`、`CHAT` 的固定字段、变长记录和 flag-driven 字段现在不足即返回失败,不再以空数据、部分记录或默认值继续产生世界副作用。
|
||||
- `QUEST_INFO` 仍按 40250 服务端实际写入的 flag-driven 尾部计算帧长;`GUILD/SKILL_INFO` 仍保留服务端声明 22、实际写入 21 字节的已证实兼容分支。两者是协议适配差异,不伪装成主表算法完全相同。
|
||||
- `ClassicStream` 现将这些 parser 失败统一交给已修复的 `RecvErrorPacket` 适配:消费当前帧、清空同批剩余数据、保留阶段 owner;下一帧仍可继续处理。
|
||||
|
||||
回归通过:`net_classic_stream_test`、`net_classic_session_test`。新增了 handler 失败后的清理流水线和截断动态 SCRIPT 回归;parser 同时收紧了消息、公会、商店、任务、地城、NPC 位置、土地列表和聊天的尾部检查。完整 13 项动态包声明长度矩阵、逐项 `sizeof/byte layout`、8192 字节以上 burst 和应用退出副作用仍保持 `PARTIAL`。
|
||||
|
||||
## Audit/fix round 2026-09-22 — Select point-change side effect
|
||||
|
||||
复核 40250 `PythonNetworkStreamPhaseSelect.cpp::SelectPhase` 后确认,
|
||||
`HEADER_GC_PLAYER_POINT_CHANGE` 在 SelectPhase 中只执行一次固定长度 `Recv` 并立即返回,
|
||||
不会调用 `RecvPointChange()`,因此不会把选角阶段收到的旧点数写入 `CPythonPlayer`。
|
||||
|
||||
当前端此前虽然已经有 Select/DirectEnter allowlist,但会把该包继续交给
|
||||
phase-neutral `ClassicParser::on_gc`,错误地更新 `EntityStore`。现已在
|
||||
`ClassicSession::on_packet` 的 Select/DirectEnter 分支加入等价的消费后返回;同一包在
|
||||
Loading/Game 仍按 40250 的对应 handler 正常更新点数。新增选角阶段“消费但无世界副作用”
|
||||
回归测试,native session 测试通过。
|
||||
|
||||
Loading/Game 完整 allowlist、逐项 `sizeof/byte layout`、动态声明长度矩阵和大 burst
|
||||
仍保持 `PARTIAL`。
|
||||
|
||||
## Audit/fix round 2026-09-22 — phase branch matrix and Loading fallback
|
||||
|
||||
新增 `audit_phase_dispatch.py`,按实际非 GAIDEN 的 header 数值自动比较 40250
|
||||
`LoginPhase/SelectPhase/LoadingPhase/GamePhase` 与当前 session gate。报告明确排除
|
||||
ClassicStream 已消费的 ping、phase、握手、密钥协商和 HybridCrypt 控制包,以及不在主
|
||||
`CMainPacketHeaderMap` 中的 observer stream 包,避免把协议层 owner 差异误报成 payload
|
||||
缺失。当前四阶段 payload case 均为数值一致。
|
||||
|
||||
该矩阵同时发现并修正了两处调用链差异:Loading 不再把 21 号 `ITEM_SET2` 当作
|
||||
Loading 专属分支,而是与 40250 一样通过一次 GamePhase fallback;Login/Select/Loading
|
||||
的 HybridCrypt 不再进入 session allowlist,而由 ClassicStream 在分帧后消费并立即返回。
|
||||
`net_classic_session_test`、`net_classic_stream_test` 和 phase 矩阵审计通过。
|
||||
|
||||
handler 副作用、逐项 `sizeof/byte layout`、动态声明长度矩阵和大 burst 仍保持
|
||||
`PARTIAL`。
|
||||
|
||||
## Audit/fix round 2026-09-21 — Loading/Game phase-local dispatch
|
||||
|
||||
沿 `PythonNetworkStreamPhaseLoading.cpp::LoadingPhase` 与
|
||||
`PythonNetworkStreamPhaseGame.cpp::GamePhase` 的真实 switch/default 路径补齐当前端的阶段门禁:
|
||||
|
||||
- `Loading` 先接受主角色、点数、物品、快捷栏和 HybridCrypt 包;其他包只有命中 GamePhase 明确分支时才进入 parser,复刻 Loading 的一次 GamePhase fallback。
|
||||
- `InGame` 只接受 GamePhase 明确列出的实体、战斗、道具、任务、商店、公会、仓库、效果等分支;登录成功、角色主初始化等迟到包不再改写世界状态。
|
||||
- HybridCrypt 动态包按 40250 的“消费后返回”处理,不再误交给当前没有 vendor crypto handler 的通用 parser。
|
||||
- `PHASE`、握手和初始密钥协商控制包消费后立即结束当前 dispatch,避免后续包在旧阶段 owner 下被提前处理;`KEY_AGREEMENT_COMPLETED` 是例外,必须先解密同一缓冲区中的后续密文并继续派发。
|
||||
|
||||
回归通过:`net_classic_session_test`、`net_classic_stream_test`、`net_classic_encstream_test`。已补上 GamePhase 小 burst 的 `MAX_RECV_COUNT` 边界、旧版 85 字节主角色包、KEY_AGREEMENT_COMPLETED 后已缓存密文继续派发,以及已知 handler 失败后的 `RecvErrorPacket` 清缓冲/owner 保留;8192 字节以上缓冲的继续消费、逐项布局、动态包声明长度和可选集成错误提示仍保持 `PARTIAL`。
|
||||
|
||||
## Audit/fix round 2026-09-21 — numeric registry alignment
|
||||
|
||||
读取 40250 Windows 工程的实际条件分支后确认:`GAIDEN` 未定义,`_IMPROVED_PACKET_ENCRYPTION_` 由 `EterBase/ServiceDefs.h` 启用。审计脚本现在会先计算非 GAIDEN 的包头数值,再比较当前 wire 表的静态/动态类别。
|
||||
|
||||
- 当前主游戏流按数值覆盖参考端全部 `117/117` 个有效注册项,静态项为 `104`,主表动态项为 `13`。
|
||||
- 移除了不在 40250 `CMainPacketHeaderMap` 中的普通流包头:攻击 `12`、旧更新 `24`、观察者 `96/97/98` 和 Panama `151`;它们仍可作为 parser 历史兼容代码存在,但不会再被主流分帧接受。
|
||||
- `HDR_GC_SYMBOL_DATA=133` 仅保留给独立 guild-mark 下载连接,并由审计报告明确标为额外适配,不再混入主游戏注册表的等价结论。
|
||||
|
||||
验证:`audit_packet_registry.py` 报告数值覆盖 `117/117`、类别无 mismatch;主流 `packet_size_gc=104`、`is_dynamic_gc` 含 13 个参考动态项加 1 个独立标志适配。逐项 `sizeof`/字节布局和动态包实际消费仍待继续闭合。
|
||||
|
||||
## Implementation fix round 2026-09-21
|
||||
|
||||
按 40250 `CMainPacketHeaderMap::Set(HEADER_GC_WHISPER, sizeof(TPacketGCWhisper), STATIC_SIZE_PACKET)` 和 `RecvWhisperPacket()` 的两段式读取修复当前 classic framing:`GC_WHISPER` 不再归入通用动态 GC,固定头先按静态尺寸等待,随后按 `wSize` 等待文本尾;传给 phase-neutral parser 时去掉 `[header][wSize]`,保持 `[type][name][text]` 数据形状,并验证文本后紧跟静态包不会被吞掉。
|
||||
|
||||
回归:`net_classic_wire_test`、`net_classic_stream_test`、`net_classic_session_test`、`audit_packet_registry.py`、`ctest --test-dir build --output-on-failure`(23/23)和 `git diff --check` 均通过。逐项 `sizeof`/byte layout、burst 大缓冲、handler 失败清理和 `SYMBOL_DATA` 独立连接边界仍未完全闭合,合同保持 `PARTIAL`。
|
||||
|
||||
### Active EterLib transport-core review
|
||||
|
||||
- `EterLib/NetStream.cpp`/`.h` 实现 TCP connect/clear/disconnect、收发 buffer、Peek/Recv/Send/SendFlush、packet sequence 和可选 TEA security;`NetPacketHeaderMap` 将 header 映射为 packet type,`NetAddress` 负责地址/端口序列化。
|
||||
- `NetDatagram`/`NetDatagramReceiver`/`NetDatagramSender` 提供 UDP socket、bind、recv/peek、缓冲区和清理;`NetDevice` 管理网络设备初始化/销毁。当前端 `classic_session`/parser 已覆盖主要 classic stream,但没有证明 EterLib 的 buffer partial read、sequence、断线清理、UDP 辅助路径和 header map 规则均在所有调用方闭环。
|
||||
- 本轮完成 EterLib 网络底层活跃文件静态核对;已有 native wire/session 测试只覆盖当前适配层,合同继续为 `PARTIAL`。
|
||||
|
||||
## Active UserInterface packet bridge review
|
||||
|
||||
- `PythonNetworkDatagram.cpp/h` 和 `PythonNetworkDatagramModule.cpp` 维护 state→header 的 datagram sender 注册、删除、选择、处理/检查 packet、bind 和状态包;它不是普通 stream 的 parser 别名,sender 生命周期和 header map 必须同步。
|
||||
- `PythonNetworkStream.h` 列出的接口覆盖角色/物品/商店/交易/任务/公会/飞行/聊天等全部 CG 请求以及相应 phase/GC 处理入口;`PythonNetworkStreamPhaseOffline.cpp::OffLinePhase` 是离线 phase 的清理/转移入口。
|
||||
- 当前端 classic session 已有主要 packet registry 和适配测试,但没有证明所有 UI bridge 请求的状态门控、sender 注册、partial read、offline 清理、重复/未知 header 和副作用顺序都与 40250 一致;本轮纳入 active source 证据,合同保持 `PARTIAL`。
|
||||
|
||||
## Status
|
||||
|
||||
`PARTIAL`。
|
||||
|
||||
## Audit round 2026-09-21 — registry/phase/sequence refresh
|
||||
|
||||
按当前工作树重新运行包注册表、阶段分派和发送序列表审计:
|
||||
|
||||
- `audit_packet_registry.py`:40250 有效主游戏注册表为 117 项,其中静态 104、动态 13;当前端主流按数值覆盖 117/117,静态/动态类别无 mismatch。当前额外的 `HDR_GC_SYMBOL_DATA=133` 仍只属于独立 guild-mark 连接适配,未计入主游戏流等价结论。
|
||||
- `audit_phase_dispatch.py`:Login、Select、Loading、Game 四阶段的 payload case 按数值均无缺失或额外分支;脚本排除了由 `ClassicStream` 消费的 transport/control 包和独立 observer stream,结果只证明阶段结构,不证明 handler 的世界副作用等价。
|
||||
- `audit_sequence_table.py`:当前端与 40250 的 32768 字节 sequence table SHA-256 完全一致。
|
||||
- `audit_source_coverage.py`:活动 40250 Visual Studio 源文件 524 个,显式归属 142 个、静态复核 364 个、未归属行为 0 个;这说明清单覆盖门通过,但不代表这些合同已完成等价验证。
|
||||
|
||||
本轮没有发现可以在不改变已登记适配语义的前提下安全修复的新代码差异。合同继续保持 `PARTIAL`,待办仍是逐项 `sizeof`/字节布局证明、13 个动态包的声明长度与截断矩阵、8192 字节以上 burst/单 tick 消费顺序、每个 handler 的副作用和错误阶段清理,以及 unknown header 的应用级退出副作用取证。
|
||||
|
||||
## Audit evidence reconciliation 2026-09-22T07:20Z
|
||||
|
||||
按当前工作树重新执行 packet/phase 结构审计:`audit_packet_registry.py` 报告参考有效注册表 `117` 项(静态 `104`、动态 `13`),当前主游戏流数值覆盖 `117/117`,类别无 mismatch;额外 `HDR_GC_SYMBOL_DATA=133` 仍只属于独立 guild-mark 连接适配。`audit_phase_dispatch.py` 的 Login/Select/Loading/Game payload case 均无缺失或额外数值。两项结果仅证明注册表和阶段分支结构,不能替代逐项 `sizeof/byte layout`、动态包边界、8192 字节 burst、handler 副作用与失败清理证明,因此合同继续 `PARTIAL`。
|
||||
|
||||
## Audit round 2026-09-20 — registry and dynamic-boundary recheck
|
||||
|
||||
本轮重新执行 40250 `CMainPacketHeaderMap` 数值/类别对照和四阶段 dispatch 矩阵:
|
||||
|
||||
- `audit_packet_registry.py` 仍为参考主表 `117` 项、静态 `104`、动态 `13`;当前主游戏流按数值覆盖 `117/117`,类别无 mismatch。额外的 `HDR_GC_SYMBOL_DATA=133` 仅作为独立公会图标下载适配保留,不能计入主流 1:1 等价。
|
||||
- `audit_phase_dispatch.py --strict` 的 Login/Select/Loading/Game payload case 均无缺失或额外数值;控制包、HybridCrypt 和 observer 专用包仍由各自 owner 消费。
|
||||
- `net_classic_wire_test`、`net_classic_stream_test`、`net_classic_session_test` 均通过,固定包、WHISPER 两段式尾部、前导零、已知 handler 失败清缓冲和已确认的 GUILD/SKILL_INFO 短写兼容没有回归。
|
||||
|
||||
本轮没有把“注册表覆盖”和“phase case 数值一致”升级为完整等价。仍未闭合的是 13 个动态包的声明长度/实际字段尾部全矩阵、逐项 `sizeof/byte layout` 自动证明、8192 字节以上 burst 的单 tick 消费边界,以及主流与独立 `SYMBOL_DATA` 连接的运行时隔离证据;合同继续为 `PARTIAL`。
|
||||
|
||||
## Audit/fix round 2026-09-20 — login/select phase dispatch gate
|
||||
|
||||
沿 40250 `LoginPhase`、`SelectPhase`、`LoadingPhase` 的 switch/default 分支核对后,当前 classic session 增加了 Login 与 Select 的显式 GC allowlist:
|
||||
|
||||
- Connecting 阶段允许真实游戏连接先收到 `LOGIN_SUCCESS3/4`,随后仍由 `PHASE_SELECT` 接管;这保留了 DirectEnter 的实际连接顺序。
|
||||
- LoggingIn 只接受 LoginPhase 的登录成功/失败、帝国、登录 key、可选认证集;CharSelect 只接受 SelectPhase 的角色 CRUD、帝国、登录成功和 point-change 集。
|
||||
- DirectEnter 在 host stage 已显示 Loading、协议仍为 SelectPhase 时沿用 Select allowlist,普通 Loading 继续由 GamePhase fallback 处理游戏包。
|
||||
- 错误阶段的游戏 spawn 包现在在到达 `ClassicParser` 前被拒绝,不能改写 EntityStore;增加 Login/Select 反例测试。
|
||||
|
||||
阶段 allowlist 未命中现在按 40250 `RecvErrorPacket` 语义在当前帧消费后清空剩余接收缓冲并保留 socket/phase owner;真正的 parser/handler 失败仍由适配层 `on_packet == false` 断开 socket。Loading/Game 全量 allowlist、117 项机器映射、burst tick 和特殊动态包边界仍需继续审计,合同保持 `PARTIAL`。
|
||||
|
||||
## Audit/fix round 2026-09-20 — character CRUD phase gate
|
||||
|
||||
本轮沿 `LoginPhase`/`SelectPhase` 的 default/error 分支复核发现,当前端 phase-neutral parser 会让角色创建、删除和改名回包绕过阶段 owner。该差异已在 `ClassicSession::on_packet` 收口:角色 CRUD/改名只允许 `CharSelect`,或允许 DirectEnter 在协议仍处于 SelectPhase 的 `Loading + m_direct_enter` 窗口;其他阶段返回明确错误,不再写入角色槽或发 UI 事件。
|
||||
|
||||
验证通过:`net_classic_session_test`、`net_classic_stream_test`、`net_classic_wire_test`、`net_loopback_test`、`char_create_delete_test.gd`、`test_intro_select_parity.gd`、`p10_test.gd` 和 `git diff --check`。
|
||||
|
||||
完整四阶段 allowlist、117-header 机器映射、burst tick、动态包边界和其他世界/道具副作用仍未闭合,合同保持 `PARTIAL`。
|
||||
@@ -0,0 +1,145 @@
|
||||
# network.reconnect_warp
|
||||
|
||||
## Scope
|
||||
|
||||
核对游戏中 `GC_WARP` 换图、断线重连、保留角色槽位/direct-enter、旧 socket/实体/特效清理和重试策略。换图和断线重连虽然都要重新建立 TCP,但必须分别对照 40250 的调用链,不能只因为最终进入同一张地图就判定等价。
|
||||
|
||||
## Reference call chain
|
||||
|
||||
- `UserInterface/PythonNetworkStreamPhaseGame.cpp::GamePhase`
|
||||
- `UserInterface/PythonNetworkStreamPhaseGame.cpp::RecvWarpPacket`
|
||||
- `UserInterface/PythonNetworkStream.cpp::ConnectGameServer`
|
||||
- `UserInterface/PythonNetworkStream.cpp::SetLoadingPhase`
|
||||
- `UserInterface/PythonNetworkStreamPhaseLoading.cpp::SetLoadingPhase`
|
||||
- `UserInterface/PythonNetworkStreamPhaseGame.cpp::SetGamePhase`
|
||||
- `UserInterface/NetworkActorManager.cpp`
|
||||
- `EterLib/NetStream.cpp::Connect`
|
||||
- `EterLib/NetStream.cpp::Clear`
|
||||
|
||||
## Current call chain
|
||||
|
||||
- `extension/src/net/classic/classic_session.cpp::connect_direct_enter`
|
||||
- `extension/src/net/classic/classic_session.cpp::connect_warp`
|
||||
- `extension/src/net/classic/classic_session.cpp::pump`
|
||||
- `extension/src/net/m2_client.cpp::pump_classic`
|
||||
- `extension/src/net/m2_client.cpp::warp_to_game_server`
|
||||
- `extension/src/net/m2_client.cpp::reconnect`
|
||||
- `extension/src/net/entity_store.cpp::reset_for_map_change`
|
||||
- `project/game_scene.gd::_on_world_reset`
|
||||
- `project/net_play.gd::clear_for_map_change`
|
||||
- `project/net_world.gd::clear_for_map_change`
|
||||
- `project/app_flow.gd::_on_disconnected`, `_on_char_list`, `_enter_character`
|
||||
- `project/ui/reconnect_ui.gd::_retry_now`
|
||||
|
||||
## Reference branch matrix
|
||||
|
||||
| 场景 | 40250 实现 | 当前端实现 | 结论 |
|
||||
|---|---|---|---|
|
||||
| 普通选人进入 | `ConnectGameServer(slot)` 在同一个 `CPythonNetworkStream` 保存 `m_dwSelectedCharacterIndex` 并设置 DirectEnter,再连接角色地址 | `ClassicSession::connect_direct_enter` 保存 `m_selected_slot/m_direct_enter_slot`,发送登录 key 后在 SELECT 进入 Loading 并发送 select | MAPPED:已覆盖主要状态和包序,但仍需真实服务端断点证据 |
|
||||
| `GC_WARP` | `RecvWarpPacket` 消费完整 warp;设置当前 selected slot 的 DirectEnter;在同一个 `CNetworkStream` 上 `Connect(lAddr,wPort)`;`Connect` 先清旧 socket/缓冲/加密/sequence | `pump_classic` 每次只取一个 warp cue,先 `EntityStore::reset_for_map_change` 和 `world_reset`,再在同一 `ClassicSession` 调 `connect_warp`;`ClassicStream::disconnect/connect` 清 socket/缓冲/加密/sequence,后续 warp 保留在队列 | PARTIAL:单次消费和保留状态方向已对齐,但清理 owner/真实包序仍不同 |
|
||||
| warp 地址 | 40250 对 `lAddr,wPort` 直接 `Connect`,即使回到当前 channel 也按重连处理 | classic warp 同样格式化 `addr/port` 后连接;Godot/旧 backend 仍有 `same_server` 分支 | PARTIAL:classic 主链接近,双 backend 的 same-server 语义仍未统一 |
|
||||
| Loading 进入 | `SetLoadingPhase` 设置 Loading process/leave、`CPythonPlayer::Clear`、删除全部飞行物和全部效果、清空 DirectEnter mode | classic warp 路径在收到 warp 时提前清 EntityStore/Godot actor/fly/effect/UI,再由 `SetLoading` 等价状态继续;`connect_warp` 将 DirectEnter 重新设置为 true | PARTIAL:当前在旧连接仍可能处理剩余 queue 前先清理;DirectEnter 清除/重设顺序与参考不同 |
|
||||
| Game phase 离开 | `__LeaveGamePhase` 清 PVP key、NetworkActorManager、combo、character/item manager;随后 Loading 再清 player/fly/effect | `_on_world_reset`、`net_play.clear_for_map_change`、`net_world.clear_for_map_change`、`EntityStore.reset_for_map_change` 分散清理 | PARTIAL:覆盖面较全,但没有一份可证明顺序与重复调用幂等的单一 owner |
|
||||
| warp 后角色槽位 | 同一 `CPythonNetworkStream` 保留 `m_dwSelectedCharacterIndex` 与 DirectEnter;新 Select/Loading 流程不回普通选人 | classic `connect_warp` 和有 selected slot/login key 的 `M2Client::warp_to_game_server` 均保留同一 `ClassicSession`;无票据/无槽位时仍有 fallback | PARTIAL:主链已对齐,fallback 和真实服务端包序仍待证 |
|
||||
| 远端断线 | `CNetworkStream::Clear` 清 transport;Python phase/应用层按已有网络状态处理,不自动模拟角色列表 UI 回放 | `ClassicSession` 进入 Failed 并发 `disconnected`;GameScene 保留;ReconnectUI 每 5 秒最多重试 5 次 | PARTIAL:当前增加了 UI 自动重试和角色列表回放,未证明与参考的 phase/owner 行为一致 |
|
||||
| generic reconnect | 40250 没有 `M2Client::reconnect` 这条新 session + Godot slot 回放调用链;已有网络对象和选中状态是参考依据 | 有 login key 且已有 selected slot 时,`M2Client::reconnect` 调用同一 `ClassicSession::connect_warp`;仅无票据/无槽位时才走新 session fallback | PARTIAL:主 classic 重连方向一致,fallback 和真实服务端包序仍待证 |
|
||||
| 重连失败 | 由 `Connect`/phase process 的结果进入已有 network phase/error 路径;只有 `PHASE_GAME` 后才属于远端游戏断线 | classic `Failed` 按 `was_online_lost` 转 `disconnected`,AppFlow 保留 GameScene;`PHASE_GAME` 前连接失败走 `login_failed` | PARTIAL:事件分类已修正,但 timeout、重复回调和最终停止条件未与参考建立对应 |
|
||||
| 重试并发 | 参考没有独立 5 秒 UI timer;连接对象本身负责一次 Connect 和 limitSec | `M2Client::reconnect` 对 Auth/GameConnect/GameLogin 做幂等 in-flight guard;ReconnectUI 的 5 秒 timer 仍是平台适配 | PARTIAL:guard 已有 native 实现,尚缺真实重复点击/旧 socket 竞态证据 |
|
||||
| 超时 | Win32 `Connect` 记录 `m_connectLimitTime`,`OnProcess` 检查连接时限并清理 | `ClassicStream::connect` 使用 nonblocking connect,并默认记录 3000ms monotonic deadline;`process` 超时清理并进入失败状态 | MAPPED:连接 deadline 算法已对齐,仍需实际阻塞连接测试 |
|
||||
|
||||
## Cleanup order matrix
|
||||
|
||||
| 顺序点 | 40250 | 当前端 | 结论 |
|
||||
|---|---|---|---|
|
||||
| 1 | `RecvWarpPacket` 先读完 warp,再调用 `Connect`;`Connect::Clear` 丢弃旧 socket 中 warp 后的尾包 | parser 产生 warp cue 后,`ClassicStream` 在当前帧结束时清空剩余接收缓冲,`M2Client` 再 drain cue、reset world 并 connect | MAPPED:classic 旧连接尾包不会跨越 GC_WARP 边界 |
|
||||
| 2 | `Connect::Clear` 清 socket、cipher、recv/send buffer、sequence | `ClassicStream::disconnect` 清 socket、buffer、sequence、cipher、phase | MAPPED |
|
||||
| 3 | 进入 Loading 后 `CPythonPlayer::Clear`、Flying、Effect、DirectEnter initialize | `_on_world_reset` 清 NetPlay/NetWorld/Ground/UI/audio,EntityStore 清实体/状态/效果/fly/队伍局部状态 | PARTIAL:数据集合基本覆盖,owner 和时序不同 |
|
||||
| 4 | Game leave 清 NetworkActor/character/item manager | GameScene/NetPlay/NetWorld 节点可能继续存活,靠 reset 方法清状态;节点本身不重建 | PARTIAL |
|
||||
| 5 | 新 MainCharacter/Loading 数据重新建立 actor/main VID | 当前收到新包后由 EntityStore signal/catch-up/scene model 重建 | PARTIAL:需真实 warp burst 和重复 warp 测试 |
|
||||
|
||||
## Equivalence matrix
|
||||
|
||||
| 项目 | 状态 | 证据 |
|
||||
|---|---|---|
|
||||
| Preconditions | PARTIAL | classic `connect_warp` 检查 selected slot/login key;generic `reconnect` 增加连接中幂等 guard,但 fallback 仍需真实失败矩阵 |
|
||||
| Branch structure | PARTIAL | GC_WARP 和有槽位的 generic reconnect 均走保留 session 分支;无槽位/无 ticket 仍走销毁/新建 session |
|
||||
| Algorithms/formulas | MAPPED | warp 地址、端口、坐标单位和 IPv4 字节序已有明确映射 |
|
||||
| State transition order | PARTIAL | 40250 是同一 stream 的 Connect→Loading;当前包含 external world_reset、Godot 场景清理和新 session 补偿路径 |
|
||||
| Constants/units | MAPPED | `lAddr/wPort`、角色槽位和 server coordinate 转换均已定位 |
|
||||
| Timing/event sources | PARTIAL | 参考由 GC_WARP/phase/Connect limit 驱动;当前另加 `ReconnectUI` 5 秒 timer 和 Loading enter delay |
|
||||
| Resource/data sources | PARTIAL | GC_WARP 和 classic fast reconnect 使用当前 parser session;fallback 仍使用 cfg/login key 并重新取 char list |
|
||||
| Protocol side effects | PARTIAL | 主 classic 重连保持 login key/direct-enter;fallback 仍可能先回角色列表再选人,包序不同 |
|
||||
| Interruption/failure/cleanup | PARTIAL | 旧 transport 和多数世界状态可清理,但 timeout、重复 reconnect、重复 warp、旧 queue 和 UI retry 终止未闭环 |
|
||||
|
||||
## Required parity tests
|
||||
|
||||
- 真实 `GC_WARP`:记录旧 socket close、新 socket endpoint、login key、selected slot、DirectEnter 和 `PHASE_SELECT→CG_PLAYER_SELECT→PHASE_LOADING` 包序。
|
||||
- 同服地址与跨服地址分别测试;确认两者都遵循 40250 的 `Connect(lAddr,wPort)`,且不会遗漏 world reset。
|
||||
- warp burst 前后验证 EntityStore、NetWorld、NetPlay、GroundItems、Flying/Effect、PVP/duel、shop/exchange/safebox、quickslot/skill/points 的清理和重建顺序。
|
||||
- 远端断线后验证:旧连接只产生一次 `disconnected`,重连期间不重复创建 session,不短暂切到 Login/Select,成功后回到原 slot。
|
||||
- 对 `reconnect()` 加 in-flight、连接失败、连接超时、5 次上限、手动立即重试、再次断线和停止自动重试矩阵。
|
||||
- 连续两个 `GC_WARP`、warp 后立刻断线、旧连接尾包与新连接首包交错等竞态测试。
|
||||
|
||||
本轮新增的包级边界回归:`GC_WARP + GC_CHARACTER_ADD` 同一读缓冲中到达时,旧尾包
|
||||
不会在 host 调用 `connect_warp()` 前写入 EntityStore;warp cue 仍按 FIFO 保留给外层
|
||||
重连 owner。classic session 与 legacy `GameClient` 都在当前 warp 帧后丢弃旧缓冲;
|
||||
该测试仍不替代真实旧/新 socket 交错、重复 warp 和 datagram 注销验证。
|
||||
|
||||
同时新增真实 loopback 断开回归:TCP 已建立但尚未收到 `PHASE_GAME` 时 peer close,
|
||||
不会设置 `was_online_lost`,从而不会把角色选择/DirectEnter 失败误导向局内重连。
|
||||
|
||||
## Evidence run
|
||||
|
||||
- `build/extension/net_classic_session_test` 已覆盖 direct-enter 的 slot endpoint、GC_WARP 的目标 endpoint、selected slot/login key 保留和 warp cue。
|
||||
- `build/extension/net_entity_test` 额外覆盖多个 warp cue 的 FIFO 取出:第一个重连被消费后,第二个仍保留,不会因 owner `return` 丢失。
|
||||
- `project/app_flow_lifecycle_test.gd` 已覆盖游戏态断线遮罩、角色列表回放和原 slot 选择的 UI 级行为。
|
||||
- `project/gamescene_test.gd`、`project/p9_test.gd` 通过,证明世界重置/地图换图的局部装配和 warp 辅助状态没有断言失败。
|
||||
- `project/p10_test.gd` 当前已通过;此前失败的 Loading 遮罩和无 SceneTree timer 问题已在前一轮修复。
|
||||
- 静态逐行核对确认:classic generic reconnect 现在保留同一 session 的 selected slot/login key/DirectEnter;`ClassicStream::connect` 已有 3000ms deadline;`M2Client::reconnect` 对连接中状态做幂等保护。
|
||||
- `project/disconnect_world_reset_test.gd` 已验证显式 `M2Client.disconnect_from_server()` 在 transport teardown 前发出一次 `world_reset`,重复调用仍按每个离线边界通知一次,避免保留的 GameScene 依赖 session 析构清理旧 presentation state。
|
||||
- 现有测试仍没有证明真实 M2Client 重复点击、deadline 触发、旧/新 socket 交错和同服/跨服包序,因此合同保持 `PARTIAL`。
|
||||
|
||||
## Deep audit round 2026-09-20(历史快照,部分结论由后续修复轮覆盖)
|
||||
|
||||
本轮按 40250 的实际调用顺序重新核对了以下不可合并的差异:
|
||||
|
||||
1. `RecvWarpPacket` 在 40250 中读完完整 `TPacketGCWarp` 后,直接在原 `CNetworkStream` 上 `Connect(lAddr,wPort)`;当前 classic 专用分支先在 `M2Client::pump_classic` 外部发出 `world_reset`,再调用同一 `ClassicSession` 的 `connect_warp`。传输清理方向一致,但清理 owner/时序不是 1:1,仍需真实旧 socket 尾包和新 socket 首包测试。
|
||||
2. 当时 generic reconnect 与 `GC_WARP` 不能视为同一实现:`reconnect()` → `warp_to_game_server()` 会 `disconnect()`、销毁 `ClassicSession`;该差异已由 2026-09-21 修复轮收敛,当前主 classic 分支保留原 session。
|
||||
3. 40250 的 `SetLoadingPhase()` 明确执行 `CPythonPlayer::Clear()`、删除全部 Flying/Effect 并 `__DirectEnterMode_Initialize()`;当前 `_on_world_reset()` 将清理分散到 EntityStore、NetPlay、NetWorld、GroundItems、UI 和 scene 状态,随后 `connect_warp()` 又把 DirectEnter 设回 true。覆盖面存在,但顺序和单一属主未对齐。
|
||||
4. 40250 的非阻塞 `Connect` 在 `OnProcess()` 以 `m_connectLimitTime` 超时后清理并触发失败;该 deadline 已由 2026-09-21 修复轮加入 `ClassicStream`,仍需实际阻塞连接测试。
|
||||
5. 当时 `p10_test` 失败的 Loading/SceneTree 问题已修复;当前 `p10_test`、`app_flow_lifecycle_test`、`gamescene_test`、`p9_test` 均通过。
|
||||
|
||||
## Active UserInterface datagram/stream reconnect review
|
||||
|
||||
- `PythonNetworkDatagram*` 的状态 sender 表和 `PythonNetworkStream.h` 的 phase/请求 API 共同决定 warp、observer、角色和 UI 请求在何时仍可发送;`PythonNetworkStreamPhaseOffline.cpp` 明确切离 offline 时不能保留旧 phase 的 packet 状态。
|
||||
- 本轮静态核对确认当前端虽然有 `connect_warp`/world reset,但仍需补 datagram sender 注销、offline 旧包丢弃、warp 前后 selected/direct-enter 状态和新旧 socket 尾包隔离的逐分支证据;合同保持 `PARTIAL`。
|
||||
|
||||
## Implementation fix round 2026-09-21
|
||||
|
||||
按 40250 `CNetworkStream::Connect(limitSec=3)` 和同一 stream `Clear()->Connect()` 语义修复 classic 重连:`ClassicStream` 为非阻塞 TCP 连接记录 3000ms 单调时钟 deadline,超时清理 socket/buffer/cipher/sequence 并进入失败;`M2Client::reconnect` 在已有 login key 和 selected slot 时不再销毁 `ClassicSession`,而是清理 world 后调用同一 session 的 `connect_warp`,保留角色槽位、login key 和 DirectEnter。连接中的重复重试改为幂等返回,不创建第二个 session。
|
||||
|
||||
回归:`net_classic_session_test`、`net_classic_stream_test`、完整 `ctest`(23/23)、`p10_test`、`app_flow_lifecycle_test`、`gamescene_test`、`p9_test` 和 `git diff --check` 均通过。真实阻塞连接 deadline、重复点击/旧 socket 竞态、同服/跨服 burst 包序及 datagram 注销仍保持 `PARTIAL`。
|
||||
|
||||
## Audit/fix round 2026-09-20 — warp queue ownership
|
||||
|
||||
继续核对 `PythonNetworkStreamPhaseGame.cpp::RecvWarpPacket` 的“消费一个完整 warp 后立即替换同一连接”语义与当前 `M2Client::pump_classic/pump_game`:当前实现原先对 `drain_warps()` 返回的整个 vector 遍历,但在第一个跨服 warp 后立即 `return`,导致同一帧已进入队列的后续 warp 被一起清空;这与参考端后续 phase tick 保留后续包不一致。
|
||||
|
||||
修复内容:
|
||||
|
||||
- `EntityStore::take_next_warp()` 改为 FIFO 一次取一个 cue,未处理 cue 保留在队列中。
|
||||
- classic 和 legacy game 两条 M2Client warp 分支都改为每次 pump 只消费一个 warp;跨服重连 `return` 后,下一次 pump 仍能看到后续 cue。
|
||||
- 新增 `net_entity_test` 的双 warp 顺序/队列耗尽断言。
|
||||
|
||||
回归:`net_entity_test`、`net_classic_session_test`、`net_loopback_test`、`p9_test.gd`、`gamescene_test.gd`、`p10_test.gd`、`app_flow_lifecycle_test.gd` 和 `git diff --check` 均通过;完整 `ctest` 在前一轮已为 23/23。
|
||||
|
||||
仍为 `PARTIAL`:真实旧/新 socket 交错、同服/跨服 live 包序、连接 deadline 实际超时、重复 reconnect/warp、datagram 注销和清理 owner 的时序仍需继续取证。
|
||||
|
||||
## Implementation fix round 2026-09-22T08:20Z
|
||||
|
||||
复核 40250 `SetOffLinePhase`/`__LeaveGamePhase` 后发现,当前显式 `M2Client.disconnect_from_server()` 只销毁 auth/game/classic session;如果 `AppFlow` 保留 `GameScene` 进入断线重连或用户主动离线,场景侧不会收到 `world_reset`,旧 Actor、飞行、目标、UI 和 BGM 状态只能等待节点销毁。按参考端“先清 phase owner,再清 transport”的顺序,当前实现先对活动 `EntityStore` 调用 `reset_for_map_change()`,发出 `world_reset`,再断开各 transport 并回到 Idle。
|
||||
|
||||
修复前新增 `disconnect_world_reset_test.gd` 稳定复现两项失败;修复后通过,且 `app_flow_lifecycle_test.gd`、`netbridge_test.gd` 通过。该修复只闭合显式 disconnect 的清理通知,不代表真实 GC_WARP/重连包序、统一 generation、旧新 socket 竞态和通用 EffectManager 已等价,合同仍保持 `PARTIAL`。
|
||||
|
||||
## Status
|
||||
|
||||
`PARTIAL`。
|
||||
@@ -0,0 +1,127 @@
|
||||
# network.security_encoding
|
||||
|
||||
## Scope
|
||||
|
||||
完整核对 40250 客户端的握手时间基准、后续 time sync、sequence 表和 `bSeq` 规则、`_IMPROVED_PACKET_ENCRYPTION_` 的 DH2/CTR、未启用 improved encryption 时的 TEA 安全分支、HybridCrypt/pack 注入、locale codepage、定长字符串以及连接清理。判断标准是调用链、分支条件、状态转移顺序、时钟/资源来源和失败副作用是否一致;“当前端有同名算法”不等于已对齐。
|
||||
|
||||
## Reference call chain
|
||||
|
||||
- `UserInterface/PythonNetworkStreamPhaseHandShake.cpp`
|
||||
- `CPythonNetworkStream::HandShakePhase`
|
||||
- `CPythonNetworkStream::RecvHandshakePacket`
|
||||
- `CPythonNetworkStream::RecvHandshakeOKPacket`
|
||||
- `CPythonNetworkStream::RecvHybridCryptKeyPacket`
|
||||
- `CPythonNetworkStream::RecvHybridCryptSDBPacket`
|
||||
- `CPythonNetworkStream::RecvKeyAgreementPacket`
|
||||
- `CPythonNetworkStream::RecvKeyAgreementCompletedPacket`
|
||||
- `UserInterface/PythonNetworkStreamPhaseLogin.cpp`, `PythonNetworkStreamPhaseSelect.cpp`, `PythonNetworkStreamPhaseGame.cpp`
|
||||
- 各阶段 `HEADER_GC_HANDSHAKE`、`HEADER_GC_HANDSHAKE_OK`、HybridCrypt 和 key-agreement allowlist;每个 packet handler 完成后立即从该阶段处理函数返回。
|
||||
- `UserInterface/AccountConnector.cpp`
|
||||
- auth connector 的重复 handshake、ping/sequence、legacy security、登录成功后的 Panama key 和 game stream 切换。
|
||||
- `EterLib/NetStream.cpp`
|
||||
- `CNetworkStream::SetSecurityMode`、`IsSecurityMode`、`SendSequence`、`__SendInternalBuffer`、`Clear`、`Connect`、`ActivateCipher`。
|
||||
- `EterBase/cipher.cpp`, `cipher.h`
|
||||
- DH2 Prepare/Agree、算法选择、密钥/IV 派生和 CTR stream 状态。
|
||||
- `EterBase/tea.cpp`
|
||||
- 未定义 `_IMPROVED_PACKET_ENCRYPTION_` 时的收发 TEA 缓冲区及 8-byte 对齐。
|
||||
- `EterLocale/StringCodec.cpp`、`UserInterface/Locale*.h/.cpp`
|
||||
- Windows `WideCharToMultiByte`/`MultiByteToWideChar`、CP1258 特殊处理和 locale codepage 来源。
|
||||
|
||||
40250 的具体时钟语义:初始 handshake 设置全局 `ELTimer_SetServerMSec(dwTime + lDelta)`,回显 `dwTime + 2*lDelta`、`lDelta=0`,之后调用 `CTimer::SetBaseTime()`;Select/Game 的 handshake 保存 `m_kServerTimeSync`,发送 `HEADER_CG_TIME_SYNC` 并追加 sequence;收到 `HEADER_GC_HANDSHAKE_OK` 后用收到 blank 的本地耗时重新设置全局 server msec。这个全局 owner 和二次校准不能用“本地 stream 能计算一个相对时间”替代。
|
||||
|
||||
## Current call chain
|
||||
|
||||
- `extension/src/net/classic/classic_stream.cpp/.h`
|
||||
- `handle_control` 处理 handshake、`HDR_GC_TIME_SYNC`、key agreement/completed、ping、phase;`server_frame_ms` 用 `steady_clock` 与每个 `ClassicStream` 的两个基准值计算。
|
||||
- `emit_bytes` 在进入发送队列时立即加密;`flush_send` 只负责写 socket。
|
||||
- `extension/src/net/classic/classic_cipher.cpp/.h`
|
||||
- 当前实现包含 40250 DH2/CTR 所需的 RFC 5114 参数、算法 selector、key/IV 派生和双向 CTR 状态。
|
||||
- `extension/src/net/classic/sequence_table.h`, `wire_classic.h`
|
||||
- 内置 32768-byte sequence 表和 `is_sequence_cg` 规则。
|
||||
- `extension/src/net/classic/classic_session.cpp`, `extension/src/net/m2_client.cpp`
|
||||
- phase/版本发送、time-sync mode 和 Godot 网络生命周期。
|
||||
- `extension/src/net/secure_cipher.cpp/.h`
|
||||
- 另一套 libsodium X25519/XChaCha20-Poly1305 backend;它不是 40250 `EterBase::Cipher` 的替代实现,不能作为 classic improved-encryption 的等价证据。
|
||||
- `extension/src/net/text_codec.h/.cpp`
|
||||
- classic fixed text now crosses one explicit UTF-8 -> GB2312/GBK wire boundary;
|
||||
fixed fields are capped after conversion and decoded back to UTF-8.
|
||||
|
||||
## Branch matrix
|
||||
|
||||
| 场景 | 40250 完整行为 | 当前端实际行为 | 结论 |
|
||||
|---|---|---|---|
|
||||
| 初始 handshake | 读取固定包,设置全局 `ELTimer`,回显 `time+2*delta`/`delta=0`,调用 `CTimer::SetBaseTime`;auth connector 也有自己的同类路径 | 保存 `m_server_time_base`/`m_client_time_base`,回显字节基本一致;没有全局 `ELTimer`/`CTimer` owner,auth/game 也没有统一时钟服务 | `PARTIAL`:wire 主体接近,时间副作用和 owner 不同 |
|
||||
| Select/Game 后续 handshake | 保存 `m_kServerTimeSync`,发 `0xFC + handshake + sequence`;收到 blank `HANDSHAKE_OK` 后按 `changeClientTime` 到达时间重新设置全局 server clock | `ClassicStream` 暂存后续 handshake 的 server/client 基准,发 `0xFC`/sequence;收到 blank 后才提交二次校准 | `PARTIAL`:stream 语义已对齐,仍缺少全局 clock owner/跨 connector fixture |
|
||||
| improved DH2/CTR | `Prepare`→`Activate`→发送固定 key-agreement reply→收到 completed 后 `ActivateCipher`;completed 前后的缓冲和状态顺序由 `CNetworkStream` 控制 | `ClassicCipher` 的 DH2/selector/派生公式接近 reference;stream 也实现 reply/completed/已缓冲密文解密 | `PARTIAL`:算法可做双端 round-trip,但没有 40250 server fixture、固定边界/失败分支和真实分片互操作证据 |
|
||||
| improved 发送时点 | 明文先进入 send buffer,`__SendInternalBuffer` 在真正 flush 时按当前 security mode 加密 | `emit_bytes` 在排队时立即加密,随后再 flush | `PARTIAL`:通常字节顺序相同,但队列、重复 flush、切换 security mode 和部分写时点不是同一语义 |
|
||||
| legacy TEA | 未定义 improved 时 `SetSecurityMode` 保存独立收发 key;接收按 8-byte 缓冲解密,发送按 8-byte 对齐/缓存后发送;auth/game 均可启用 | classic 侧没有完整的 `SetSecurityMode`/TEA 收发缓冲调用链;`classic_cipher` 中的 TEA 只是可选 DH2 algorithm selector,不是 legacy stream mode | `PARTIAL`:分支没有实现为同一协议路径 |
|
||||
| HybridCrypt keys/SDB | handshake、login、select、game 阶段均允许动态包;消费固定头+精确 stream 长度,并注入 `CEterPackManager` | header 在 wire table 中可识别,但 classic session/parser 没有等价 pack manager 注入,当前会拒绝或忽略该可选资源分支 | `PARTIAL`:动态 framing 不是完整功能等价 |
|
||||
| Panama pack/login key | auth 成功按 `loginKey XOR g_adwEncryptKey[0..3]` 解密 pack IV,设置 login key,再连接 game stream | 当前登录/资源主链没有 40250 Panama pack IV 解密和同一 auth→game 交接证据 | `PARTIAL`:资源/密钥副作用缺失或未证明 |
|
||||
| anti-cheat/HackShield/XTrap | 40250 handshake/game allowlist 有条件编译的 HS/XTrap request/response 分支 | header/尺寸部分存在,但没有 Windows anti-cheat provider 或完整 response 处理 | `PARTIAL`:条件分支未等价 |
|
||||
| sequence table | `SEQUENCE_TABLE_SIZE=32768`,`SendSequence` 在 bSeq 开启后逐包取表项、到末尾回 0;bSeq 来自 packet registration | 当前内置 32768 表并按 `is_sequence_cg` 添加 trailer;代码注释声称 verbatim,但还没有和 40250 源数组的 hash/逐项生成校验 | `PARTIAL`:主路径存在,证据和注册矩阵未闭合 |
|
||||
| 版本包 | 根据 locale/region 选择 `CG_CLIENT_VERSION2` 或 `CG_CLIENT_VERSION`,固定字段按 Windows locale 字节复制 | 当前已按 `m_locale_is_europe` 选择两个包,字段走 UTF-8 `to_wire` | `PARTIAL`:header/分支存在,非 ASCII 字节语义仍不同 |
|
||||
| locale 转码 | `LocaleService_GetCodePage` + Windows codepage 转换;中文固定字段使用 GB2312-compatible bytes;固定字段本质是 byte array/`strncpy` | classic 使用 Windows CP936 或 POSIX iconv 的 GB2312/GBK 转码;Godot/Native 内部仍保持 UTF-8;其它 locale codepage 尚未接入 | `PARTIAL`:40250 中文固定字段已对齐,非中文 locale 仍不同 |
|
||||
| fixed string/NUL | reference 对固定 `char[]` 以字节 cap、NUL 结尾和 struct zero-fill 为准;不可表示字符遵循 Win32 codepage 策略 | classic `to_wire()` 在 GB2312 字节上限后截断且结构体 zero-fill;`wire_text_fits()` 按编码后字节拒绝超长/不可表示值;无系统 codec 的平台拒绝高位字节而不泄漏 UTF-8 | `PARTIAL`:中文字段已对齐,fallback/其它 locale 仍需真实 fixture |
|
||||
| cleanup/failure | `Clear` 清 cipher、socket、TEA buffer、recv/send positions 和 sequence;key agreement/发送失败按阶段返回并由上层断开 | `disconnect` 清 stream/cipher/buffer/sequence;缺少 legacy buffer、global timer、HybridCrypt/pack 和 auth connector 分层清理矩阵 | `PARTIAL` |
|
||||
|
||||
## Equivalence matrix
|
||||
|
||||
| 项目 | 状态 | 证据 |
|
||||
|---|---|---|
|
||||
| Preconditions | `PARTIAL` | classic 对固定长度、DH2 key-ready、completed 顺序有保护;legacy/hybrid/anti-cheat 和阶段 allowlist 不完整 |
|
||||
| Branch structure | `PARTIAL` | initial/time-sync/improved 主分支存在;TEA、HybridCrypt、Panama、anti-cheat 分支没有完整等价实现 |
|
||||
| Algorithms/formulas | `PARTIAL` | `ClassicCipher` 的 DH2/CTR 公式与参数接近 reference;缺少 40250 server 密文 fixture,且 legacy TEA stream 没有主链 |
|
||||
| State transition order | `PARTIAL` | completed 后激活和已有缓冲解密存在;global clock 二次校准、auth/game 双 connector、flush 时加密时点不同 |
|
||||
| Constants/units | `PARTIAL` | handshake/header/32768 sequence 常量已定位;locale byte cap、TEA block/buffer 和 optional packet size 规则未闭合 |
|
||||
| Timing/event sources | `PARTIAL` | 当前每个 stream 使用 `steady_clock`,并在 `HANDSHAKE_OK` 提交暂存基准;40250 使用全局 `ELTimer`,auth/game 共用时钟 owner 仍不同 |
|
||||
| Resource/data sources | `PARTIAL` | key/sequence/version 来源已定位;当前没有真实 locale codepage 和 EterPack HybridCrypt/Panama 注入来源 |
|
||||
| Protocol side effects | `PARTIAL` | handshake/key reply/ping/version 有路径;clock、pack IV、HybridCrypt 和 anti-cheat side effect 缺失或未证明 |
|
||||
| Interruption/failure/cleanup | `PARTIAL` | classic disconnect 会清基本状态;TEA 对齐缓存、auth connector 和可选资源的失败/重入/断开组合未覆盖 |
|
||||
|
||||
## Evidence already available
|
||||
|
||||
- `extension/tests/net_classic_cipher_test.cpp`:client/server polarity 的 DH2 round-trip、CTR 连续性和新会话密钥差异。
|
||||
- `extension/tests/net_classic_encstream_test.cpp`:handshake、key agreement、completed、已缓冲密文和 sequence 的 stream happy path。
|
||||
- `extension/tests/net_text_codec_test.cpp`:12 个中文字符在 24 个 GB2312 字节内 round-trip、13 个字符被拒绝、真实中文名称和 32 字节商店招牌边界。
|
||||
- `extension/tests/net_classic_session_test.cpp`:角色创建/改名包验证实际写入 GB2312 字节、NUL 结尾和 24 字节边界。
|
||||
- `extension/src/net/classic/sequence_table.h`:32768 项静态表和编译期长度断言;`audit_sequence_table.py` 已逐字节比对 40250 数组,SHA-256 为 `3f6f31964896e712f1f54cb813c7b488b656f700f6ca948ece3313419f876279`。
|
||||
|
||||
## Deep audit round 2026-09-20
|
||||
|
||||
本轮重新检查 `ClassicStream::handle_control/emit_bytes`、`ClassicCipher`、sequence 表和文本边界,并重跑当前加密/编码回归:
|
||||
|
||||
- `net_classic_cipher_test`、`net_classic_encstream_test` 和 `net_text_codec_test` 均退出码 0;它们证明当前 DH2/CTR round-trip、连续 CTR、key agreement/已缓存密文、sequence trailer 和 UTF-8 codepoint-safe 截断没有回归。
|
||||
- 当前 `HANDSHAKE_OK` 已提交后续 handshake 的暂存 server clock;`server_frame_ms()` 仍是每个 `ClassicStream` 的 steady-clock 相对计算,尚未接入 40250 `CTimer/ELTimer` 全局 owner。`net_classic_stream_test` 已覆盖 blank 前不跳时钟、blank 后完成校准。
|
||||
- 当前 `emit_bytes()` 在进入发送队列时加密,参考端在真正 flush internal buffer 时按当前 security mode 加密;在正常单次发送中可能得到相同字节,但切换 security mode、部分写、重复 flush 和失败清理的状态边界不同。
|
||||
- `classic_cipher` 中的 TEA 仅作为可选算法 selector,当前 classic stream 没有 40250 legacy `SetSecurityMode` 的独立收发 TEA block buffer 主链;HybridCrypt/SDB、Panama pack IV、HS/XTrap 和其它 locale codepage 也没有完整注入/消费路径。中文 40250 fixed text 已由 `text_codec.cpp` 在网络边界转为 GB2312/GBK-compatible bytes。
|
||||
- 32768 项 sequence 表已通过 reference 数组逐字节校验;主要 `bSeq` 分支仍依赖当前 wire 注册表,未完成所有版本/locale/可选包的生成式矩阵。
|
||||
|
||||
结论:改进加密的局部算法和当前 wire framing 可用,但全局时钟、加密时点、legacy/optional security 分支、codepage 和 sequence 参考数据仍未闭合,合同保持 `PARTIAL`。
|
||||
|
||||
## Required tests before claiming parity
|
||||
|
||||
- 使用 40250 wire fixture 验证初始 handshake、Select/Game handshake、`HANDSHAKE_OK` 二次校准和实际 server-frame 时钟;当前已有 socket-free 二次校准回归。
|
||||
- 用 40250 server polarity fixture 验证 DH2/CTR:split recv/send、flush 前后加密、重复 key agreement、completed 前后明文、非法长度、失败清理和 reconnect。
|
||||
- 保持 40250 sequence 数组逐字节校验,并补齐从 `PacketInfo`/`CMainPacketHeaderMap` 生成的完整 `bSeq` 矩阵。
|
||||
- 为 CP949/CP1252/CP1258 等非中文部署建立 reference byte fixtures;中文 GB2312 已覆盖不可表示字符、NUL、byte cap、固定字段 zero-fill 和反向 decode。
|
||||
- 对 HybridCrypt keys/SDB、Panama pack、legacy TEA、HS/XTrap 明确支持/拒绝策略,并分别验证不乱 consume、不把包流错位、不产生错误的成功状态。
|
||||
|
||||
## Status
|
||||
|
||||
`PARTIAL`。
|
||||
|
||||
本轮已修复后续 handshake 的二次时钟校准,并加入 40250 sequence table 逐字节审计;DH2/CTR 的局部算法相似性仍不能覆盖全链路差异,在补齐全局 clock、locale、legacy/optional 分支和真实 wire fixture 前,合同保持 `PARTIAL`。
|
||||
|
||||
## Implementation fix round 2026-09-21 — Chinese fixed-field wire encoding
|
||||
|
||||
按 40250 中文客户端的 `LocaleService_GetCodePage`/固定 `char[]` 语义补齐文本边界:Godot 和 native 业务层继续以 UTF-8 保存文本,`text_codec.cpp` 在 classic 发送前按 Windows CP936 或 POSIX GB2312/GBK 转码;`wire_text_fits()` 在转码后按字节容量校验,`to_wire()` 只写完整的 GB2312 字符并依靠 zero-initialized packet 保留 NUL。接收路径用同一 codepage 解回 UTF-8。没有系统 codepage codec 的平台不会把高位 UTF-8 字节直接发送到 40250 字段,而是将该值标记为不可表示。
|
||||
|
||||
角色创建、改名以及聊天、密语、公会、商店等 classic 固定/动态文本发送入口均使用这个边界;地图名、任务名和系统提示仍按各自动态包长度处理,不套用 24 字节角色名限制。
|
||||
|
||||
回归:`net_text_codec_test`、`net_classic_session_test`、`net_classic_stream_test`、`net_loopback_test`、CTest 23/23、Godot 生命周期/角色选择/移动回归和 `git diff --check` 通过。中文角色名实际创建/改名包已证明 12 个汉字占 24 个 GB2312 字节,13 个汉字被拒绝。合同仍保持 `PARTIAL`,因为 legacy TEA、可选安全分支、其它 locale codepage 和真实服务端中文建号包序仍未完全闭合。
|
||||
|
||||
## Implementation fix round 2026-09-21
|
||||
|
||||
按 40250 `RecvHandshakePacket`/`RecvHandshakeOKPacket` 修复 `ClassicStream`:Select/Game 后续 handshake 只保存 pending server/client 基准,发送 `CG_TIME_SYNC` 后等待一字节 `HANDSHAKE_OK`,再按到达耗时提交 server clock;断开时清理 pending 状态,避免下次连接复用旧校准。新增 `audit_sequence_table.py`,对 40250 `EterLib/NetStream.cpp` 的 32768 项 sequence table 与当前静态表逐字节比较,当前两端 SHA-256 完全一致。
|
||||
|
||||
回归:`net_classic_stream_test`、`net_classic_session_test`、sequence table audit 和 `git diff --check` 通过。全局 `ELTimer/CTimer` owner、legacy TEA、HybridCrypt/Panama/HS-XTrap、Windows codepage、flush 时加密时点和真实 server fixture 仍保持 `PARTIAL`。
|
||||
@@ -0,0 +1,103 @@
|
||||
# npc.dialog_shop
|
||||
|
||||
## Scope
|
||||
|
||||
审计 NPC 点击与脚本对话、普通 NPC 商店、扩展多货架商店、购买/出售、商店错误和结束清理,以及个人商店招牌、点击打开、摆摊编辑和关闭流程。判断标准是当前端是否复现 40250 的服务端货架数据来源、全局槽位索引、NPC/玩家商店分支、动作门控、请求包副作用和失败清理,而不是本地 helper 是否能算出一个价格或库存结果。
|
||||
|
||||
## Reference call chain
|
||||
|
||||
- `UserInterface/PythonNetworkStreamPhaseGame.cpp`
|
||||
- `RecvShopPacket` 处理 `START`、`START_EX`、`END`、`UPDATE_ITEM`、`UPDATE_PRICE` 与错误子包;普通商店固定读取 40 个槽位,多货架按 tab 读取每个货架的名称、货币类型和 40 个槽位。
|
||||
- `CPythonShop::SetItemData(globalIndex, item)` 将增量更新的全局位置映射为 `tabIdx = globalIndex / SHOP_HOST_ITEM_MAX_NUM`、`slot = globalIndex % SHOP_HOST_ITEM_MAX_NUM`,因此多货架位置不是永远落在 tab 0。
|
||||
- `SendOnClickPacket` 发送 NPC/个人商店点击请求并追加 sequence;脚本回包通过事件管理器打开对话窗口。
|
||||
- `RecvShopSignPacket` 将玩家商店招牌绑定到已有角色实体;个人商店广告牌点击后由 `uiprivateshopbuilder.py` 发送 `SendOnClickPacket(vid)`。
|
||||
- `UserInterface/PythonNetworkStreamPhaseGameItem.cpp`
|
||||
- `SendShopEndPacket`、`SendShopBuyPacket`、`SendShopSellPacket` 和 `SendShopSellPacketNew` 都先经过 `__CanActMainInstance()`,再写 `CG_SHOP` 子头、槽位/数量并 `SendSequence()`。
|
||||
- 购买包使用数量 1 加显示位置;出售分旧版单槽包和新版槽位+数量包。结果、金币、背包和售罄状态由服务器回包更新。
|
||||
- `UserInterface/PythonShop.cpp`、`Client/Eternexus/root/uishop.py`
|
||||
- `CPythonShop` 保存所有 tab 和全部 40 个位置,普通、NPC 扩展、多货架和个人商店由窗口状态区分。
|
||||
- `ShopDialog.Open(vid)` 根据 NPC、他人玩家和主角自己的商店分别显示买卖、货架或关闭控件;自己商店不能走买卖分支。
|
||||
- 左键购买有确认,右键/取消选中可直接购买;出售检查反出售标记、贵重物品确认并发送服务端请求。
|
||||
- `Client/Eternexus/root/uiprivateshopbuilder.py`、`PythonShop.cpp`、`Packet.h`
|
||||
- 摆摊最多 39 个商品,商品按展示位置排序,源物品位置和价格由服务端消费;标题有长度限制,禁止状态和 anti flag 在提交前检查。
|
||||
- `BuildPrivateShop` 发送 `CG_MYSHOP`,关闭自己的摊位也使用相应的个人商店协议语义。
|
||||
- `PythonNonPlayer.cpp`、`PythonPlayerInput.cpp`、`PythonPlayerInputMouse.cpp`
|
||||
- NPC/商店点击由实体类型、可点击状态和距离/输入流程进入 `SendOnClickPacket`;个人商店广告牌是可点击的独立入口,不等同于普通 PC 攻击或选中。
|
||||
|
||||
## Current call chain
|
||||
|
||||
- `project/net_play.gd::_on_pick` 对 NPC/WARP 走 `click_npc`,有距离预约和同 VID 节流;但没有对带 `shop_sign` 的 PC 建立 40250 广告牌点击分支,当前 `project/net_world.gd::_refresh_shop_sign` 只创建显示用 `Label3D`。
|
||||
- `project/ui/quest_dialog.gd` 和网络事件可以承接脚本/对话显示;`extension/src/net/m2_client.cpp::click_npc`、`classic_session.cpp::send_click_npc` 能发送基础点击包。
|
||||
- `extension/src/net/entity_store.cpp` 已解析普通 `START`、扩展 `START_EX`、`END`、更新和常见错误,并保存商店 VID、tab 和 item;但 `mut_shop_update_item` 拒绝 `pos >= 40`,且多 tab 时只更新 tab 0,未复现 40250 的全局位置映射。
|
||||
- `project/ui/shop_ui.gd` 有买/卖模式、40 格、多货架、确认框、距离关闭和右键购买;但是 `_drop_to_sell` 在购买模式下仍继续执行出售请求,当前 `project/complex_item_drop_test.gd` 已复现该失败。
|
||||
- `ShopUI.refresh` 没有根据 `shop.vid` 区分主角自己的个人商店、他人个人商店和 NPC 商店,因此当前买/卖按钮状态不等价于 `ShopDialog.Open`。
|
||||
- `project/npc_shop_system.gd` 使用硬编码 NPC 库存与本地 `buy_item`/`sell_item` 直接修改金币和背包;它不是 `CPythonShop` 的服务器货架回包链,且目前主要由孤立 parity 测试使用。
|
||||
- `project/private_shop_system.gd` 同样是本地 stock/价格 helper;live `project/ui/private_shop_ui.gd` 能提交 `M2Client.open_private_shop`,但未在 live 提交前统一调用死亡、骑乘、战斗状态和 50200 摆摊道具校验。
|
||||
- `M2Client::shop_buy`、`shop_sell` 只做 in-game、u8 和数量边界,缺少 40250 的 `__CanActMainInstance`、实际物品/anti flag/商店状态门控;`open_private_shop` 只验证字段 wire bounds 与最多 39 条。
|
||||
- `classic_session.cpp` 的买卖、关闭和个人商店包布局及 sequence 基础存在;现有 native 测试验证布局,但没有真实 `GC_SHOP` 多阶段包和失败回包驱动的 UI 状态测试。
|
||||
|
||||
## Branch matrix
|
||||
|
||||
| 场景 | 40250 | 当前端 | 结论 |
|
||||
|---|---|---|---|
|
||||
| NPC 点击/脚本对话 | 实体输入、距离和 `SendOnClickPacket` 后由服务器脚本回包驱动 | NPC 点击、距离预约、节流和基础点击存在 | PARTIAL:事件/脚本结果与异常清理缺真实包覆盖 |
|
||||
| 普通商店 START | 服务器提供 VID 和固定 40 格,空槽也有确定位置 | 能解析并保存非空 item,UI 补 40 格 | PARTIAL:空槽原始状态和真实 UI 回包顺序未证明 |
|
||||
| START_EX 多货架 | 每 tab 40 格,更新位置是全局 `tab*40+slot` | 初始 tab 能显示;增量更新限制 `<40` 且只更新 tab 0 | GAP |
|
||||
| 商品更新/售罄/错误 | `SetItemData`、价格更新、错误事件和 UI 刷新 | 基础错误字符串和刷新存在 | PARTIAL:更新索引和回包失败后 UI/金币/库存清理不足 |
|
||||
| 买入 | `__CanActMainInstance`→CG_SHOP/BUY→sequence→服务器结果 | UI/native 能发买包并有确认 | PARTIAL:缺动作门控、pos 全范围和服务器结果闭环 |
|
||||
| 卖出/批量卖出 | anti-sell/贵重确认后旧 SELL 或 SELL2,服务端扣物品加金币 | UI 有部分 anti flag/确认;native 只做 u8 边界 | PARTIAL:购买模式拖入仍可出售,实际库存/服务端回包未闭环 |
|
||||
| 自己的个人商店 | 隐藏买卖,只显示关闭;个人商店协议关闭自己的摊位 | ShopUI 主要按普通买卖模式显示,未按 shop VID 统一区分 | GAP |
|
||||
| 他人个人商店/招牌 | 广告牌点击发送 `SendOnClickPacket(vid)` 后打开玩家商店 | 招牌目前只有 `Label3D`,PC 点击未路由到点击商店 | GAP |
|
||||
| 摆摊编辑/提交 | 39 条、标题/禁止状态/anti flag/源物品校验后 `CG_MYSHOP` | UI/Native 字段和数量限制存在 | PARTIAL:live 状态校验和服务端消费/拒绝反馈不足 |
|
||||
| 关闭/离开/断线 | `END` 清理访问商店;个人商店关闭遵循独立协议语义 | 基础 close 和本地 UI 关闭存在 | PARTIAL:访问商店、自己的摆摊和断线/乱序清理未统一验证 |
|
||||
|
||||
## Implementation-equivalence matrix
|
||||
|
||||
| 项目 | 状态 | 证据/限制 |
|
||||
|---|---|---|
|
||||
| Preconditions | PARTIAL | NPC 距离和部分 UI anti flag 存在;native 买卖缺 `__CanActMainInstance`、真实物品/商店状态和动作门控,摆摊 live 缺死亡/骑乘/战斗及 bundle 校验 |
|
||||
| Branch structure | PARTIAL | 普通商店、多货架、购买确认和脚本对话分支存在;自己的个人商店、玩家招牌点击和购买模式拖放分支不等价 |
|
||||
| Algorithms/formulas | PARTIAL | 货架槽位和价格由协议保存;硬编码 NPC stock、本地买卖 helper 和本地出售价格不是 40250 的服务端权威实现 |
|
||||
| State transition order | GAP | 参考是请求→服务端判定→item/gold/shop update/error→UI 刷新;本地 helper 可直接改库存/金币,且 UI 购买模式错误放行出售 |
|
||||
| Constants/units | PARTIAL | 40 格、最多 3 tab、最多 39 条、标题/招牌和字段 wire bounds 基本存在;全局多 tab 更新范围未实现,价格/数量服务端约束未闭合 |
|
||||
| Timing/event sources | PARTIAL | 点击节流、距离关闭、确认框和 shop signal 存在;真实 START→UPDATE→ERROR/END 时序、招牌点击和重复请求未覆盖 |
|
||||
| Resource/data sources | GAP | 40250 商品库存来自服务器 `CPythonShop`/item data;当前 `NpcShopSystem` 额外维护硬编码本地库存,和 live authority 不是同一数据源 |
|
||||
| Protocol side effects | PARTIAL | CG_SHOP、CG_ON_CLICK、CG_MYSHOP 的基础布局和 sequence 存在;动作门控、个人招牌点击和完整错误/结果副作用不完整 |
|
||||
| Interruption/failure/cleanup | GAP | 购买模式拖放泄漏到出售;多 tab 更新可能落错/丢失;断线、拒绝、售罄、自己摊位关闭和未找到实体的招牌清理没有统一端到端回滚 |
|
||||
|
||||
## Regression evidence
|
||||
|
||||
- `project/test_npc_shop_parity.gd`:26/26 通过,但主要验证硬编码 `NpcShopSystem` 的本地库存、金币和背包变化,不能作为 40250 服务器商店等价证明。
|
||||
- `project/test_npc_shop_flow.gd`:NPC 点击距离/节流、UI 多 tab/右键购买和距离关闭通过;使用 fake client,未覆盖真实 GC shop 包。
|
||||
- `project/test_npc_dialog_shop_ui_parity.gd`:窗口尺寸、格子、按钮、脚本对话与 fake client 通过;没有多 tab 全局更新和 PC 招牌点击。
|
||||
- `project/private_shop_ui_test.gd`、`project/test_private_shop_bundle_parity.gd`、`project/shop_rules_test.gd`、`project/p8_test.gd`:本地摆摊编辑与规则通过;未证明 live UI 的状态门控、`CG_MYSHOP` 服务端拒绝/关闭和招牌打开。
|
||||
- `project/complex_item_drop_test.gd`:当前有 1 项失败,购买模式仍允许把背包物品拖到商店出售目标;这是可复现的实现回归。
|
||||
- `build/extension/net_classic_session_test`、`build/extension/net_entity_test`:基础 packet/store 行为通过,但没有 `START_EX` 位置 40..119 的更新回归,也没有完整错误/结束/重新打开序列。
|
||||
|
||||
## Deep audit round 2026-09-21
|
||||
|
||||
### Fresh regression results
|
||||
|
||||
- `godot --headless --path project --script test_npc_shop_parity.gd`:退出码 0,26/26 通过;覆盖的是 `NpcShopSystem` 的本地库存、价格、金币和背包 helper。
|
||||
- `godot --headless --path project --script test_npc_shop_flow.gd`:退出码 0,38 项通过;覆盖 NPC 距离/预约/节流、fake shop 多货架 UI、右键购买和距离关闭,没有真实 `GC_SHOP START_EX/UPDATE/ERROR/END` 包。
|
||||
- `godot --headless --path project --script test_npc_dialog_shop_ui_parity.gd`:退出码 0;窗口、40 格、买卖按钮、脚本对话和 QuestCurtain 通过,仍使用 fake client。
|
||||
- `godot --headless --path project --script private_shop_ui_test.gd`、`test_private_shop_bundle_parity.gd`、`shop_rules_test.gd`、`p8_test.gd`:均退出码 0;本地摆摊规则、bundle、价格和社交窗口通过,不等价于真实 `CG_MYSHOP`/个人商店招牌/失败回包。
|
||||
- `godot --headless --path project --script complex_item_drop_test.gd`:退出码 1;购买模式下卖出目标仍错误接受背包物品拖放。
|
||||
- `build/extension/net_classic_session_test` 与 `build/extension/net_entity_test`:退出码 0;基础包/store 行为通过,但没有覆盖多货架全局位置 40..119 的增量更新。
|
||||
|
||||
### Static parity findings
|
||||
|
||||
- 40250 `START_EX` 的每个 tab 固定 40 格,`UPDATE_ITEM` 使用全局位置 `tab * 40 + slot`;当前 `EntityStore::mut_shop_update_item` 仍要求 `pos < 40`,并且只更新 `m_shop_tabs[0]`,所以 tab 1+ 的增量回包会被丢失或落错货架。
|
||||
- `ShopUI::_drop_to_sell` 没有以 `_mode == 2` 作为硬门,购买模式拖拽仍能调用 `_request_sell`;这与 `ShopDialog` 的买/卖分支不一致,也是本轮失败测试的直接原因。
|
||||
- 40250 `ShopDialog.Open(vid)` 按 NPC、他人个人商店和自己的个人商店分支显示;当前 `ShopUI` 主要按通用买卖窗口处理,未以 `shop_vid` 和主角 VID 做统一分支,自己的摊位与他人摊位行为仍可能混用。
|
||||
- `shop_sign` 在 native/entity store 中有保存和显示数据,但 `NetPlay::_on_pick` 没有完整复刻 `SendOnClickPacket(shop_vid)` 的个人商店广告牌分支;显示招牌不等于可打开玩家商店。
|
||||
- `NpcShopSystem`、`PrivateShopSystem` 等本地 helper 可以直接修改金币、库存和 stock;live UI 的 native 请求路径存在,但两套权威来源并存,测试通过不能证明服务端回包覆盖本地结果。
|
||||
- `M2Client::shop_buy/shop_sell/shop_close/open_private_shop` 的基础 wire bounds 存在,但没有统一 `__CanActMainInstance`、商店状态、实际 item/anti-flag 和重复/失败请求门控;`SendSequence`/错误/售罄/重开顺序也未由真实 fixture 验证。
|
||||
|
||||
### Round conclusion
|
||||
|
||||
本轮仍为 `PARTIAL`:NPC 点击、普通商店 UI、基础买卖/摆摊包和局部规则测试可运行,但多货架增量索引、购买模式拖放门、个人商店招牌/自有摊位分支和服务器权威回包仍未统一;本轮未修改实现代码。
|
||||
|
||||
## Status
|
||||
|
||||
`PARTIAL`。NPC 点击、基础对话承接、普通/扩展商店协议骨架、购买出售 UI 和个人商店字段已经存在,但当前端仍有明确的多 tab 更新索引错误、购买模式错误放行出售、个人商店招牌不可点击打开、自己商店状态不分支、native 动作门控缺失以及本地硬编码库存/金币旁路。已完成审计并记录,未修改实现代码。
|
||||
@@ -0,0 +1,225 @@
|
||||
# quest.quest_event_navigation
|
||||
|
||||
## Scope
|
||||
|
||||
审计 `GC_QUEST_INFO` 任务日志、任务计时/计数、任务信件按钮、`GC_SCRIPT` NPC 对话与 `CPythonEventManager` 事件标签、确认/输入/选择、任务地图信号、Atlas/小地图导航、剧情镜幕、相机/淡入淡出和副本结果提示。判断标准是当前端是否与 40250 的协议字段、任务生命周期、事件处理顺序、脚本按钮语义和地图/过场副作用统一;本地生物学家、悬赏、活动调度等脚本 helper 的独立测试不等于 40250 客户端 parity。
|
||||
|
||||
## Reference call chain
|
||||
|
||||
- `UserInterface/PythonNetworkStreamPhaseGame.cpp`
|
||||
- `RecvQuestInfoPacket` 先按 `QUEST_SEND_IS_BEGIN` 判定 BEGIN/UPDATE/END,再按 flag 读取 title、clock name/value、counter name/value、icon;BEGIN 注册新任务并记录开始时间,UPDATE 只更新带 flag 的字段,END 删除指定任务,最后刷新任务 UI。
|
||||
- `RecvScriptPacket` 读取动态 `GC_SCRIPT`,交给 `CPythonEventManager::RegisterEventSetFromString`,然后以 server skin 打开 QuestDialog;`RecvQuestConfirmPacket` 将 message、timeout、request PID 交给 UI。
|
||||
- `SendScriptAnswerPacket` 使用 8-bit answer;`SendScriptButtonPacket` 使用 unsigned 32-bit index,任务日志点击通过 `-2147483648 + questIndex` 形成 unsigned wire value。
|
||||
- `UserInterface/PythonQuest.cpp`
|
||||
- `CPythonQuest::MakeQuest/RegisterQuestInstance/SetQuestClockValue` 都会更新 `iStartTime`;`GetQuestLastTime` 用 `iStartTime + iClockValue - currentSecond`,因此任务窗口关闭时计时仍继续。
|
||||
- `CPythonQuest` 保持任务实例容器的顺序,`GetQuestIndex` 将 UI 行号映射到真实 quest index。
|
||||
- `UserInterface/PythonEventManager.cpp` / `PythonEventManager.h`
|
||||
- `ProcessEventSet` 逐命令处理 42 类标签:LETTER/DELAY/ENTER/WAIT/NEXT/DONE/QUESTION、图片、QUESTBUTTON、地图信号、相机、FADE/WHITE、ITEM/MOB、WINDOW_SIZE、INPUT、CONFIRM_WAIT、SELECT_ITEM 和 DUNGEON_RESULT。
|
||||
- `NEXT`/取消发送 answer 254;`DONE` 只执行 `DoneEvent/CloseSelf`,不发送 answer。`RUN_CINEMA` 只在首命令时替换为 `.msc` 事件文件;FADE/WHITE 设置 wait flag,动画结束后由 `EndEventProcess` 解锁。
|
||||
- `Client/Eternexus/root/uiquest.py` / `interfacemodule.py` / `uicharacter.py`
|
||||
- QuestDialog 的 skin、按钮和 ESC 语义由 UI 层执行;任务信件点击发送 unsigned script button 后删除信件按钮;任务日志最多显示 5 条并使用原始任务顺序。
|
||||
- `UserInterface/PythonMiniMap.cpp` / `Client/Eternexus/root/uiminimap.py` / `atlasinfo.txt`
|
||||
- `ADDMAPSIGNAL` 写入 CPythonMiniMap 信号点并打开 Atlas;`CLEARMAPSIGNAL` 清除信号点;`SETCMAPPOS` 改变 Atlas 中心,不改变玩家世界坐标。
|
||||
|
||||
## Current call chain
|
||||
|
||||
- native `EntityStore::apply` 已解析 `GC_SCRIPT`、`GC_QUEST_CONFIRM` 和 flag-driven `GC_QUEST_INFO`,`M2Client` 暴露 `script_answer/script_button/script_select_item/quest_input/quest_confirm/quest_cancel/get_quests`,`M2Client::poll` 通过 signal 将脚本、确认和任务变更交给 Godot。
|
||||
- `project/ui/quest_dialog.gd` 已实现 42 个事件标签的解析/状态机、图片、选择分页、输入、确认等待、地图信号、相机、淡入淡出、电影镜幕和副本结果信号;`project/game_scene.gd` 已连接对话、相机、淡入淡出、选择物品、小地图和 Atlas。
|
||||
- `project/ui/quest_log.gd` 与 `project/ui/char_status_ui.gd` 提供任务按钮和 5 条分页列表;`project/ui/atlas_ui.gd` 提供地图拼接、拖动、玩家/队伍/公会领地/观察者标记;`project/atlas_navigation_system.gd` 是另一个带硬编码地图/NPC/距离规则的本地 helper。
|
||||
- `project/biologist_quest_system.gd`、`dynamic_bounty_event_system.gd`、`event_manager_system.gd`、`metin_spawn_event_system.gd` 是本地游戏规则/活动 helper,40250 Windows 客户端没有对应的客户端 authority 实现;它们不能替代任务/事件回包链。
|
||||
|
||||
## Branch matrix
|
||||
|
||||
| 场景 | 40250 | 当前端 | 结论 |
|
||||
|---|---|---|---|
|
||||
| 任务 BEGIN/UPDATE | BEGIN 注册全新实例并记录开始时间;UPDATE 只更新带 flag 字段 | `EntityStore` 的 BEGIN/未知 UPDATE 会记录 `clock_start_ms`,并追加到 `m_quest_order`;普通 UPDATE 保持位置和起点,`clock_value` 更新会重置起点 | PARTIAL:生命周期与本地计时已对齐,CTimer/服务端时间基准仍需端到端验证 |
|
||||
| 任务 END | `DeleteQuestInstance(index)` 后刷新 UI | `EntityStore::apply` 与 classic parser 都删除 `m_quests[index]`、移除 `m_quest_order` 项并排入一次 `quest_changes` 刷新 | PARTIAL:删除和顺序分支已对齐,BEGIN 时间戳和 UI 重建仍未闭合 |
|
||||
| 任务倒计时 | `iStartTime` 从 BEGIN 或 clock update 时记录,窗口关闭也继续倒计时 | native 保存 `clock_start_ms`;QuestLog 和 `CharStatusUI` 每次显示按起点重新计算,关闭窗口期间不重置 | PARTIAL:关闭/重开语义已对齐,精确 CTimer 时间基准、过期任务清理和后台恢复仍未端到端验证 |
|
||||
| 任务列表顺序/索引 | vector 顺序与 `GetQuestIndex(row)` 一致 | `EntityStore::m_quest_order` 按 CPythonQuest vector 语义枚举;UPDATE 保持位置、BEGIN 追加、END 删除 | PARTIAL:native 顺序已对齐,真实 UI 行号/服务器序列仍未端到端验证 |
|
||||
| 任务日志点击 | `-2147483648 + questIndex` 按 unsigned 32-bit 发 `CG_SCRIPT_BUTTON` | `QuestLog`/`CharStatusUI` 传入负数 sentinel,`M2Client` 按 40250 的范围转换为 `uint32` | PARTIAL:wire sentinel 已对齐,真实 server 回包和任务窗口生命周期仍未测 |
|
||||
| 任务信件按钮 | 新信件同 index 先删除旧按钮、插入最前;点击发送 script button 并删除按钮 | `QuestLog.recv_quest` 顺序/点击删除逻辑已存在 | PARTIAL:UI 结构接近,但实际任务日志负索引门控仍会阻断另一入口,重连/服务器重新发送未测 |
|
||||
| GC_SCRIPT 事件标签 | 42 标签按逐命令、等待、锁定和事件回调处理 | parser/state machine 基本覆盖 42 标签;首命令 `RUN_CINEMA` 会读取 `.msc` 并替换事件流,缺失资源不打开空窗口 | PARTIAL:真实 `.msc` 资源序列和中断清理仍未验证 |
|
||||
| 对话按钮语义 | QUESTION 发送 0-based answer;NEXT/取消发送 254;DONE 只关闭;ESC 根据按钮类型分支 | QUESTION/NEXT/取消、DONE 的 `DoneEvent/CloseSelf` 和 ESC 的 QUESTION/NEXT/CANCEL 分支已对齐 | PARTIAL:真实 server 脚本结束/重复提交/中断时序仍未完全覆盖 |
|
||||
| 输入/选择/确认 | INPUT→`CG_QUEST_INPUT_STRING`;SELECT_ITEM 打开选择窗;CONFIRM_WAIT 显示倒计时并超时取消;END_CONFIRM_WAIT 关闭 | native/UI 信号和主要控件存在,选择/输入/确认测试通过 | PARTIAL:真实超时、重复提交、断线和 server close 清理未覆盖 |
|
||||
| 地图信号/Atlas | ADDMAPSIGNAL 同时写小地图 signal point/Atlas waypoint 并打开 Atlas;CLEAR 清除;SETCMAPPOS 调中心 | GameScene 将同一坐标写入 Minimap 与 AtlasUI,Atlas overlay 绘制橙色 waypoint 并支持清除 | PARTIAL:两处消费链已对齐,真实 Atlas 资源、地图切换和多点生命周期仍未端到端验证 |
|
||||
| 相机/淡入淡出/镜幕 | 相机设置、blend、restore 和 fade wait 由应用/事件窗完成;动画完成后释放 wait | GameScene 已连接 camera/fade,QuestCurtain 可开关 | PARTIAL:没有真实 `.msc`/资源和中断清理回归,skin/Done/ESC 边界与原版不一致 |
|
||||
| 自定义任务/活动 helper | 40250 客户端只负责显示/请求,规则通常由服务端给回包 | 当前 helper 本地推进生物学家、悬赏、活动掉落和倍率 | GAP(custom authority):本地测试通过不能说明与 40250 Windows 客户端统一 |
|
||||
|
||||
## Implementation-equivalence matrix
|
||||
|
||||
| 项目 | 状态 | 证据/限制 |
|
||||
|---|---|---|
|
||||
| Preconditions | PARTIAL | native 阶段、answer/input 和 script-button sentinel 边界存在;事件窗口/重复提交/服务器任务存在性缺少门控 |
|
||||
| Branch structure | PARTIAL | 42 标签和 UI 主分支已覆盖;RUN_CINEMA 的缺失资源分支、Atlas 任务信号和部分中断分支已补齐/接通,真实过场消费者仍未验证 |
|
||||
| Algorithms/formulas | PARTIAL | 事件等待、颜色、相机参数和地图坐标转换大体存在;任务计时缺 reference 的 start-time 算法,硬编码 Atlas/NPC/活动 helper 不是 40250 客户端算法 |
|
||||
| State transition order | PARTIAL | 参考的 GC 任务包→创建/更新/删除→刷新主顺序已补齐;QuestLog 只在可见时递减,脚本 close/answer 仍有额外本地动作 |
|
||||
| Constants/units | PARTIAL | 5 条任务、30/16/24 字段、42 标签、5×9/按钮布局、254/255 answer 和 uint32 script-button sentinel 已对齐;skin 和地图信号显示语义未闭合 |
|
||||
| Timing/event sources | PARTIAL | native signal、毫秒事件等待和 fade callback 存在;任务 start timestamp、真实 `.msc` 事件、超时/断线/重连时序缺证据 |
|
||||
| Resource/data sources | PARTIAL | quest icon、ITEM/MOB 名称和 minimap DDS 使用资源接口;Atlas/NPC helper 另有硬编码 registry,RUN_CINEMA 仍缺真实 `.msc` 资源与消费者证据 |
|
||||
| Protocol side effects | PARTIAL | GC_SCRIPT/QUEST_INFO/CONFIRM parser 和基本 CG quest packets 存在;任务按钮 wire、DONE close-only、ESC 的 answer 分支已补,电影文件注册未统一 |
|
||||
| Interruption/failure/cleanup | GAP | 任务结束删除已覆盖 native 两条解析路径,但地图切换、对话 ESC/DONE、确认超时、fade/cinema 中断、信号清理和任务列表重建没有完整 server-sequence 回归 |
|
||||
|
||||
## Regression evidence
|
||||
|
||||
- `project/quest_test.gd`、`project/quest_event_test.gd`:通过,覆盖脚本对话、确认、任务按钮和 42 标签/状态字段;使用 fake client/离线 parser,没有真实 `GC_QUEST_INFO` END、unsigned button 和 server sequence。
|
||||
- `project/test_quest_dialog_parity.gd`:修正 DONE/ESC/CANCEL 断言后 47 项通过;该测试现在确认 `[DONE]` 处理时发 `done_event`、按钮点击只关闭,QUESTION 的 ESC 发送最后选项,CONFIRM_WAIT 的取消发送 254。
|
||||
- `project/test_quest_hud_parity.gd`:19/19 通过;任务按钮布局和 fake quest array 通过,未覆盖 native unordered map、计时持续性和 `M2Client::script_button` 负数拒绝。
|
||||
- `project/quest_curtain_test.gd`、`project/atlas_test.gd`、`project/test_atlas_navigation_parity.gd`、`project/test_map_name_shower_parity.gd`、`project/test_minimap_radar_parity.gd`:通过;主要验证本地窗口、硬编码导航 registry、地图标记和假数据,未证明 `ADDMAPSIGNAL` 在 Atlas 的实际资源/生命周期等价。
|
||||
- `project/test_biologist_quest_parity.gd`:28/28 通过;验证本地生物学家任务 helper,不是 40250 Windows 客户端功能实现。
|
||||
- `project/test_event_manager_parity.gd`:24/24 通过;验证本地活动倍率/宝盒 helper,40250 客户端没有对应 authority,不能作为 parity PASS。
|
||||
- `build-debug/extension/net_entity_test`、`build-debug/extension/net_classic_session_test`、`build-debug/extension/net_bounds_test`、`build-debug/extension/net_classic_wire_test`:通过基础 quest/script packet、quest END 删除、vector 顺序/UPDATE 活跃状态和负数 sentinel 检查;任务计时、DONE 语义、RUN_CINEMA 消费者和 Atlas 信号仍未覆盖。
|
||||
|
||||
## Deep audit round 2026-09-21
|
||||
|
||||
### Fresh regression results
|
||||
|
||||
- `godot --headless --path project --script quest_test.gd`、`quest_event_test.gd`:均退出码 0;脚本解析、确认、任务日志和 42 标签本地状态机通过。
|
||||
- `godot --headless --path project --script test_quest_dialog_parity.gd`:退出码 0,45/45 通过;但 DONE 断言明确要求发送 `script_answer(254)`,与 40250 DONE 只关闭事件的实现相反,因此该条不能作为正确 parity 证据。
|
||||
- `godot --headless --path project --script test_quest_hud_parity.gd`:退出码 0,19/19 通过;fake quest array 的按钮、分页、计数和时间格式通过,但使用负数 script button 值的 fake client,未经过 native wire gate。
|
||||
- `godot --headless --path project --script quest_curtain_test.gd`、`atlas_test.gd`、`test_atlas_navigation_parity.gd`、`test_map_name_shower_parity.gd`、`test_minimap_radar_parity.gd`:均退出码 0;窗口、Atlas、地图标记和小地图 helper 通过。
|
||||
- `godot --headless --path project --script test_biologist_quest_parity.gd`:退出码 0,28/28 通过;只验证本地生物学家任务 helper。
|
||||
- `godot --headless --path project --script test_event_manager_parity.gd`:退出码 0,24/24 通过;只验证本地活动倍率/宝盒 helper。
|
||||
- `build/extension/net_entity_test`、`net_classic_session_test`、`net_classic_wire_test`:均退出码 0;基础任务/脚本包通过。
|
||||
|
||||
### Static parity findings
|
||||
|
||||
- 40250 `GC_QUEST_INFO` BEGIN/UPDATE/END 的 END 会真正删除 quest instance;当前 `EntityStore` 对 END 未删除对应记录,旧任务可能继续出现在 `get_quests` 和日志中。
|
||||
- 40250 在 BEGIN/clock update 保存 `iStartTime`,窗口关闭时计时仍继续;当前端已在 `QuestInfo::clock_start_ms` 保存起点,并让 QuestLog/CharStatusUI 按起点计算剩余值,仍需验证 Godot 单调时钟与参考 `CTimer::GetCurrentSecond()` 的时间基准和后台行为。
|
||||
- 40250 任务列表保持 vector 顺序并通过真实 quest index 映射;当前端已加入 `m_quest_order` 并覆盖 BEGIN/UPDATE/END 的位置语义,仍缺真实任务日志回包到 UI 行号的端到端验证。
|
||||
- 40250 任务日志按钮使用 `-2147483648 + questIndex` 的 unsigned 32-bit wire 值;当前 `M2Client::script_button(int)` 看到负数直接拒绝,虽然 fake UI 测试把负数记录为成功。
|
||||
- `QuestDialog` 的 DONE、QUESTION/NEXT/CANCEL ESC 和确认取消已按 40250 分支对齐;真实 server 断线、重复输入和事件窗口中断仍缺端到端证据。
|
||||
- `RUN_CINEMA` 的首命令替换和 `.msc` loose-resource 读取已在 `QuestDialog.begin_script()`;缺失/空文件现在按 40250 注册失败语义不打开空对话框,仍缺真实 `.msc` 资源序列/中断验证。ADDMAPSIGNAL 现在同时进入 Minimap 与 AtlasUI,仍缺真实地图资源和切图生命周期验证。
|
||||
- 本地 `biologist_quest_system`、活动倍率/宝盒 helper 是服务端规则的复制状态机,不属于 40250 Windows 客户端 authority,不能因 28/28、24/24 通过而标记 parity。
|
||||
|
||||
### Round conclusion
|
||||
|
||||
本轮仍为 `PARTIAL`:任务/对话/Atlas 基础骨架和多数局部测试可运行,但 END 删除、持续计时、任务按钮 wire、DONE/ESC、电影播放消费者和 Atlas 信号消费存在明确差异,且现有 DONE 测试断言本身与 40250 不一致。本轮未修改实现代码。
|
||||
|
||||
## Active UserInterface quest-event bridge review
|
||||
|
||||
- `PythonEventManagerMoudle.cpp` 暴露从文件/字符串注册 event set、清理、限制数量、逐行位置/宽度、等待/跳过/结束、handler/answer、可见起始行和 `SendScriptButtonPacket`;位置更新使用事件管理器的坐标变换,不能只用一个对话框文本数组替代。
|
||||
- `PythonGameEventManagerModule.cpp` 将中心位置注入 `CGameEventManager` 后调用 `Update`,是过场/地图事件的逐帧桥接;`PythonQuest.h` 维护 quest instance 的标题、clock/counter 名和值、图标、创建/删除和索引查询。
|
||||
- 当前端有脚本解析和 QuestDialog/Atlas 骨架,但 event-set 生命周期、handler 回调、持续计时/逐帧位置、任务实例删除以及日志按钮 wire 值尚未统一;RUN_CINEMA 和地图信号也没有发现完整消费者,合同保持 `PARTIAL`。
|
||||
|
||||
## Static review evidence
|
||||
|
||||
已将 `PythonEventManagerMoudle.cpp`、`PythonGameEventManagerModule.cpp`、`PythonQuest.h` 纳入 active source 证据;本轮只确认 reference bridge 的完整入口和状态字段,没有把局部脚本测试当作行为等价。
|
||||
|
||||
## Status
|
||||
|
||||
`PARTIAL`。当前端已经有较完整的任务脚本解析、对话控件、任务 HUD、地图和过场骨架,42 个事件标签与基础 native packet 可工作;但任务生命周期/计时/顺序、ESC 语义、电影事件消费和 Atlas 任务信号存在明确差异,另有多个本地规则 helper 不是 40250 客户端实现。DONE、任务 END 和 script-button wire 已有对应修复。
|
||||
|
||||
## Implementation fix round 2026-09-21 — quest END removes the instance
|
||||
|
||||
按 40250 `CPythonNetworkStream::RecvQuestInfoPacket()` 的明确分支复刻任务结束语义:
|
||||
`QUEST_SEND_IS_BEGIN` 旗标存在且后续 `isBegin == 0` 时,参考端将包分类为
|
||||
`QUEST_PACKET_TYPE_END`,调用 `CPythonQuest::DeleteQuestInstance(index)`,随后刷新任务
|
||||
列表。当前端此前只把 `begin` 改成 false,`get_quests()` 仍会枚举原有记录,造成已结束
|
||||
任务继续显示。
|
||||
|
||||
现已在 `EntityStore::mut_quest_info`(classic parser)和 `EntityStore::apply`(协议兼容
|
||||
路径)加入同一删除分支,并保留一次 `quest_changes` 刷新事件。新增的 native 回归先在
|
||||
修复前确认“结束任务仍存在”失败,修复后 `net_entity_test`、`net_classic_session_test`
|
||||
和 `git diff --check` 均通过。
|
||||
|
||||
合同仍为 `PARTIAL`:任务 BEGIN 的 start timestamp、vector 顺序、负数 script-button
|
||||
sentinel、DONE/ESC、RUN_CINEMA 消费者、Atlas 信号和断线清理仍未闭合。
|
||||
|
||||
## Implementation fix round 2026-09-21 — quest-log script-button sentinel
|
||||
|
||||
继续按 40250 `uicharacter.py::QuestButtonClick` 与
|
||||
`CPythonNetworkStream::SendScriptButtonPacket(unsigned int)` 核对,确认任务日志入口的
|
||||
`-2147483648 + questIndex` 不是非法参数,而是写入 `uint32` wire 字段的任务索引
|
||||
sentinel。此前 `M2Client::script_button(int)` 以 `idx < 0` 直接拒绝,导致
|
||||
`QuestLog`/`CharStatusUI` 的任务日志点击无法到达服务器。
|
||||
|
||||
现已允许普通非负 `uint32` 索引,以及 `INT32_MIN .. INT32_MIN + 0xffff` 的任务索引
|
||||
sentinel;其他负数仍拒绝,避免把任意错误值包装成大整数。`net_bounds_test` 已覆盖
|
||||
普通端点、sentinel 端点和越界负值,构建与运行通过。
|
||||
|
||||
本合同仍为 `PARTIAL`:任务 BEGIN 的 start timestamp、vector 顺序、RUN_CINEMA 消费者、
|
||||
Atlas 信号和断线清理仍未闭合。
|
||||
|
||||
## Implementation fix round 2026-09-21 — CANCEL and ESC answer branches
|
||||
|
||||
继续按 40250 `uiquest.py::MakeNextButton` / `OnPressEscapeKey` 修复剩余对话关闭分支:
|
||||
`BUTTON_TYPE_CANCEL` 的取消按钮发送 `SCRIPT_ANSWER(254)`;ESC 在 QUESTION 时发送最后
|
||||
一个选项索引,在 NEXT/CANCEL 时发送 254,在 DONE 时只关闭;没有可见答复按钮时才
|
||||
回落到 native 的 255 cancel sentinel。此前当前端统一调用 `quest_cancel()`,会把
|
||||
CONFIRM_WAIT 和 QUESTION/NEXT 的关闭语义压成错误的 255/取消路径。
|
||||
|
||||
新增回归先确认修复前 QUESTION ESC 与 CONFIRM_WAIT 取消断言失败,修复后
|
||||
`test_quest_dialog_parity.gd` 47 项、`quest_test.gd`、`quest_event_test.gd` 和
|
||||
`git diff --check` 通过。
|
||||
|
||||
本合同仍为 `PARTIAL`:任务时钟与列表顺序、RUN_CINEMA 资源消费者、Atlas 信号、真实
|
||||
服务端脚本序列和断线/重连清理仍未闭合。
|
||||
|
||||
## Implementation fix round 2026-09-21 — DONE event and close-only button
|
||||
|
||||
按 40250 `PythonEventManager::ProcessEventSet(EVENT_TYPE_DONE)` 与
|
||||
`uiquest.py::MakeNextButton(BUTTON_TYPE_DONE)` 修复当前任务对话时序:`[DONE]` token
|
||||
被消费时立即触发 `DoneEvent`;随后生成的“关闭”按钮只调用 `CloseSelf()`,不发送
|
||||
`CG_SCRIPT_ANSWER(254)`。此前当前端把 `done_event` 延迟到按钮点击,并把 DONE 错误地
|
||||
当成 NEXT/取消发送 254。
|
||||
|
||||
新增回归先把测试断言改为参考行为并确认修复前失败;修复后
|
||||
`test_quest_dialog_parity.gd` 45 项、`quest_test.gd` 和 `git diff --check` 通过。
|
||||
|
||||
本合同仍为 `PARTIAL`:ESC 尚未按 CANCEL/DONE/NEXT 分支完全复刻,任务时钟、索引顺序、
|
||||
RUN_CINEMA 消费者、Atlas 信号和断线清理仍未闭合。
|
||||
|
||||
## Implementation fix round 2026-09-21 — quest instance vector order
|
||||
|
||||
按 40250 `CPythonQuest::RegisterQuestInstance`、`MakeQuest` 和
|
||||
`DeleteQuestInstance` 的容器语义修复任务列表顺序:参考端使用 vector 保存任务实例,
|
||||
普通 UPDATE 不改变位置,未知 UPDATE 在末尾创建,BEGIN 先删除同 index 的旧实例再把新
|
||||
实例追加到末尾,END 同时删除实例和对应顺序项。当前端此前直接枚举
|
||||
`unordered_map<uint16_t, QuestInfo>`,相同任务回包在不同运行中可能改变任务行顺序。
|
||||
|
||||
现已在 `EntityStore` 增加 `m_quest_order`,classic mutator 与协议兼容 `apply` 两条路径
|
||||
共用相同的注册/删除规则;`quest_indices()` 只按该顺序返回仍存在的任务。修复前新增的
|
||||
顺序回归按预期失败,修复后 `net_entity_test`、`net_classic_session_test`、
|
||||
`net_bounds_test` 和 `git diff --check` 均通过。
|
||||
|
||||
本合同仍为 `PARTIAL`:精确时间基准、真实任务日志到 UI 行号映射、
|
||||
RUN_CINEMA/Atlas 生命周期、服务端脚本序列和断线清理仍未闭合。
|
||||
|
||||
## Implementation fix round 2026-09-21 — classic UPDATE keeps the quest active
|
||||
|
||||
复核发现 classic parser 的 `mut_quest_info` 与协议兼容 `apply` 对同一 UPDATE 的结果不
|
||||
一致:前者会把已有任务的 `begin` 暂存字段写成 false,后者保持为有效实例。40250 在
|
||||
`QUEST_PACKET_TYPE_UPDATE` 中只调用 `MakeQuest`(仅当任务不存在)和各字段 setter,
|
||||
不会把已存在任务变成结束态;只有 `QUEST_PACKET_TYPE_END` 才删除实例。
|
||||
|
||||
现已让 classic mutator 对所有非 END 的任务包保持 `QuestInfo.begin == true`,并增加了
|
||||
“UPDATE 保持活动实例且更新标题”的回归。修复前该断言失败,修复后
|
||||
`net_entity_test`、`net_classic_session_test` 和 `git diff --check` 通过。
|
||||
|
||||
本合同仍为 `PARTIAL`:任务 start timestamp、真实 UI 行号映射、RUN_CINEMA/Atlas 生命周期、
|
||||
服务端脚本序列和断线清理仍未闭合。
|
||||
|
||||
## Implementation fix round 2026-09-21 — ADDMAPSIGNAL reaches Atlas
|
||||
|
||||
按 40250 `PythonEventManager::ProcessEventSet(EVENT_TYPE_ADD_MAP_SIGNAL)` 和
|
||||
`CPythonMiniMap::AddSignalPoint` 核对,任务脚本坐标不只是圆形小地图的边缘提示;参考端
|
||||
同时调用 `AddWayPoint(TYPE_WAYPOINT, ...)`,因此 Atlas 也能显示同一个 waypoint,
|
||||
`CLEARMAPSIGNAL` 会移除两处记录。
|
||||
|
||||
当前端的 `GameScene` 现在把 `map_signal_added` 同时转发给 `Minimap` 和 `AtlasUI`,
|
||||
Atlas overlay 使用与小地图相同的 server-centimetre→world→map 坐标转换绘制橙色标记,
|
||||
并由 `map_signals_cleared` 同时清理。`atlas_test.gd` 新增添加/清理回归,测试通过。
|
||||
|
||||
本合同仍为 `PARTIAL`:真实 Atlas 资源、地图切换后的 signal 生命周期、RUN_CINEMA 真实
|
||||
`.msc` 序列和断线/中断清理仍未闭合。
|
||||
|
||||
## Implementation fix round 2026-09-21 — missing RUN_CINEMA resource
|
||||
|
||||
按 40250 `CPythonEventManager::RegisterEventSetFromString()` 的首命令特例和
|
||||
`CPythonNetworkStream::RecvScriptPacket()` 的返回值判断修复过场脚本失败分支:
|
||||
`RUN_CINEMA` 指向不存在或空路径时,参考端注册返回 `-1`,不会调用
|
||||
`OnScriptEventStart()`;当前端现在清理临时 event set、保持窗口关闭且不发出 `opened`,
|
||||
不再显示空白锁定的任务对话框。有效 `.msc` 仍沿当前 loose-resource 读取路径替换事件流。
|
||||
|
||||
`test_quest_dialog_parity.gd` 新增缺失资源回归,当前 49 项全部通过;真实 `.msc` 文件的
|
||||
完整事件序列、过场播放器消费者和中断清理仍保持 `PARTIAL`。
|
||||
@@ -0,0 +1,119 @@
|
||||
# render.effect_particle_motion —— MDE/MSE、粒子、动作事件挂点和回收
|
||||
|
||||
状态:`PARTIAL`
|
||||
|
||||
本合同不仅检查 MDE/MSE 粒子播放器,还检查 40250 `ActorInstanceMotionEvent` 在每个动作帧分派的特效、音效、飞行物、特殊攻击、显示/隐藏、传送和目标特效。判断标准是资源字段、事件类型、帧时序、挂点矩阵、生命周期和所有 Actor 的消费范围一致;只验证“特效能播放”不能视为与 40250 等价。
|
||||
|
||||
## 40250 参考调用链
|
||||
|
||||
- `GameLib/RaceMotionData.cpp` / `RaceMotionDataEvent.h`
|
||||
- 读取 `MotionEventDataCount`、事件类型、`StartingTime`,按 `1 / g_fGameFPS` 转为动作帧。
|
||||
- `EFFECT` 保存 `EffectFileName`、`EffectPosition`、`AttachingEnable`、`FollowingEnable`、`IndependentFlag` 和骨骼名。
|
||||
- `EFFECT_TO_TARGET` 保存目标偏移、跟随/钓鱼标志;`FLY` 使用独立的 `FlyFileName`/`FlyPosition`;`SPECIAL_ATTACKING` 保存 `DuringTime`、`EnableHitProcess`、攻击参数和碰撞球。
|
||||
- 动作加载时还从同名 `.mss` 读取声音脚本,后续按动作帧调用 `SoundEventProcess`。
|
||||
- `GameLib/ActorInstanceMotionEvent.cpp`
|
||||
- `MotionEventProcess()` 对当前动作的每一帧事件调用 `MotionEventProcess(dwcurFrame, index, data)`。
|
||||
- `EFFECT` 分独立世界特效、挂骨骼并跟随、挂骨骼但只快照、挂 Actor 根节点四条路径。
|
||||
- `EFFECT_TO_TARGET` 分钓鱼落点、目标对象跟随和目标位置快照三条路径。
|
||||
- `SPECIAL_ATTACKING` 建立带持续时间、命中去重和动态碰撞球的 splash 区;`FLY` 从骨骼/角色位置创建 `.fly` 实例并调用 `OnShoot`。
|
||||
- `CHARACTER_SHOW/HIDE` 改变渲染模式和透明度;`WARP` 做地图阻挡检查、朝向目标并移动到目标前 270cm。
|
||||
- `SOUND` 调用 `PlaySound3D`,声音脚本由 `SoundEventProcess` 独立更新。
|
||||
- `EffectLib/EffectManager.cpp`、`EffectInstance.cpp`、`ParticleInstance.cpp`、`PythonEffectModule.cpp`
|
||||
- 负责 MSE/MDE 注册、解析、粒子时间曲线、混合、颜色、缩放、旋转、纹理动画、渲染空间、生命周期和实例回收。
|
||||
|
||||
## 当前端实现映射
|
||||
|
||||
- `formats/msa.cpp` / `formats/msa.h`
|
||||
- 已读取动作时刻、通用效果/声音/骨骼/挂点字段,另有 MDE/MSE 和攻击数据的基础解析。
|
||||
- 本轮已补齐 40250 动作事件的专用数据:`FlyFileName`/`FlyPosition`/飞行骨骼、震屏 `DuringTime`/`Power`/`AffectingRange`、特殊攻击 `DuringTime`/`EnableHitProcess`/攻击参数/碰撞球,以及 type 7/8/9 的稳定事件记录。
|
||||
- `extension/src/metin2_anim.cpp`
|
||||
- 已在 `.msa` 播放时间跨过事件时发出 `motion_event`/`motion_event_detailed`,并提供骨骼姿态查询。
|
||||
- 本轮 `motion_event_detailed`/`get_events()` 已暴露上述专用字段和 `collision_spheres`,表现层可按 40250 分支消费;生产路由尚未全部接入。
|
||||
- `project/fx/effect_player.gd`、`project/fx/mde.gd`、`project/fx/mse.gd`、`project/fx/effect_registry.gd`
|
||||
- 已覆盖 MDE/MSE、粒子曲线、材质、颜色、混合、纹理动画、one-shot/loop 生命周期、注册/缓存和 `fx_spawned`/`fx_finished` 追踪。
|
||||
- `project/fx/motion_effect_anchor.gd` / `target_effect_anchor.gd`
|
||||
- 已实现角色根、骨骼姿态、follow/independent、目标中心和目标快照的基础矩阵转换;缺失骨骼时会清理特效。
|
||||
- `project/game_scene.gd`
|
||||
- 本地主角 type 1/10 走完整挂点/目标特效,type 2/3 使用固定震屏/屏闪,type 6 只从待发队列触发 `client.shoot`;本轮远端真实 Actor 的 type 1/10、7/8/9 和 direct sound 也经由 `NetWorld` 路由。
|
||||
- 本地和远端真实 Actor 已接入 type 7/8 的 `__HideEvent`/`__ShowEvent` 等价消费;type 9 已按 40250 计算目标前 270cm、检查 `world.is_blocked`、调整朝向并在本地主角路径发送一次 `FUNC_WAIT`。仍没有特殊攻击事件的 splash 碰撞球,也没有把 `.fly` 文件按 40250 的起点/目标/owner/skill 链创建。
|
||||
- `project/ui/player_view.gd`、`project/ui/mob_view.gd`
|
||||
- 已按当前动作加载同名 `.mss` 并逐 60FPS 更新 3D 声音脚本;本轮还为远端真实视图提供统一的 animator signal 接入点。
|
||||
- `project/ui/remote_player_view.gd`、`project/net_world.gd`
|
||||
- 远端 PC 和怪物会创建真实模型并播放 `.msa`;本轮 `net_world` 已为有 native animator 的远端模型连接 `motion_event_detailed`,并由 `GameScene` 消费远端 type 1/10 特效、type 7/8 显示隐藏和直接动作声。飞行仍只走服务端 `GC_CREATE_FLY`,避免把远端动作帧错误地变成客户端发包。
|
||||
|
||||
## 分支等价矩阵
|
||||
|
||||
| 40250 判据/副作用 | 当前实现 | 结论 |
|
||||
|---|---|---|
|
||||
| MSE/MDE 注册、解析、粒子曲线、混合和 one-shot 回收 | `effect_player`/`mse`/`mde`/`effect_registry` + FX 测试 | 基本等价;真实渲染后端像素证据仍有限 |
|
||||
| 动作事件按 `StartingTime → dwFrame` 在每个动作 Actor 上触发 | native 以时间区间发信号,本地主角和有真实 animator 的远端 Actor 均接入详细事件 | 部分等价;时间精度/重复跨帧和占位 Actor 升级前后的完整逐帧对照仍未完成 |
|
||||
| EFFECT 的独立、骨骼跟随、骨骼快照、根节点四分支 | 本地和远端真实 Actor 的 type 1 都用 `spawn_motion` 和 anchor | 部分等价;基础矩阵存在,但远端 target/清理和非详细 fallback 仍需逐分支对照 |
|
||||
| EFFECT_TO_TARGET 的钓鱼、目标跟随、目标快照 | 本地主角 type 10 有目标 anchor,钓鱼直接跳过 | 部分等价;缺少 40250 fishing effect 生命周期/落点字段和目标失效后的统一回收证据 |
|
||||
| FLY 读取 `.fly`、骨骼起点、目标、owner/skill,并执行 `OnShoot` | 当前 type 6 已保留 `fly_file`/`fly_pos`/`fly_bone` 并能传到 native 事件字典;`GC_CREATE_FLY` 的索引飞行已按 40250 注册表加载 `.msf`,动作事件仍从待发队列触发 `client.shoot` | 部分等价;索引资源参数已接入,动作事件 `.fly` 起点/owner/skill、动态碰撞和 attach/tail 仍未闭合 |
|
||||
| SPECIAL_ATTACKING 持续时间、动态球、命中去重和结束清理 | 当前 parser/native 字典已保留 `duration_time`、攻击参数和碰撞球 | 部分等价;splash 的生产碰撞/去重/生命周期分支仍缺失,资产中实际存在 818 个 type 4 事件 |
|
||||
| SOUND 直接动作声和 `.mss` 帧声 | 本地和远端详细事件都能触发 direct sound,PlayerView/MobView 更新 `.mss` | 部分等价;频率/距离和路径转换尚未做逐事件对照 |
|
||||
| SCREEN_WAVING/FLASHING 使用事件自身参数 | 当前固定 `shake(0.06, 9.0)` 和固定 0.12s 屏闪 | 不等价;type 2 的 `DuringTime`、`Power`、`AffectingRange` 没有保留/使用 |
|
||||
| CHARACTER_SHOW/HIDE、WARP | 本地和真实远端 Actor 的 type 7/8 已按事件帧切换 actor model 可见性;type 9 由 `_apply_motion_warp` 执行目标前 2.70m、阻挡检查和朝向,本地再发送 `FUNC_WAIT` | 部分等价;真实 `seomjeon.msa`、允许/阻挡/无目标路径有回归,远端目标生命周期和 live 服务端状态包仍待验证 |
|
||||
| 事件资源和模型从网络实体删除、切图、死亡时清理 | FX anchor 对局部无效目标会回收,map reset 会清飞行/实体 | 部分等价;没有所有 Actor 事件实例、splash、fishing effect 和跨图中断的统一清理矩阵 |
|
||||
|
||||
## 已执行测试
|
||||
|
||||
- `project/fx_test.gd`:PASS;MSE 解析、EffectPlayer 和 registry。
|
||||
- `project/effect_playback_test.gd`:0 failures;播放时间和生命周期基础路径。
|
||||
- `project/target_effect_test.gd`:0 failures;目标特效跟随/快照基础路径。
|
||||
- `project/skill_fx_test.gd`:PASS;技能 FX 基础映射。
|
||||
- `project/effect_color_test.gd`、`effect_emission_test.gd`、`effect_faces_test.gd`、`effect_lie_test.gd`、`effect_random_rotation_test.gd`、`effect_render_regression_test.gd`、`effect_rotation_test.gd`、`effect_scale_test.gd`、`effect_space_test.gd`、`effect_surface_test.gd`、`effect_texture_animation_test.gd`:均无断言失败;surface/texture animation 在 headless 下明确跳过像素检查。
|
||||
- `project/motion_effect_anchor_test.gd`:断言显示 `failures=0`,但进程以 exit 139 结束并有 6 个 ObjectDB 泄漏,不能作为稳定通过证据。
|
||||
- 资源静态统计:当前资产中的 `MotionEventType` 为 type 1=1892、2=263、4=818、6=268、7=47、8=47、9=47、10=197,共 3579 个事件;没有 type 5 样本,但不能因此删除 type 5 分支。
|
||||
|
||||
## Deep audit round 2026-09-21
|
||||
|
||||
### Fresh regression results
|
||||
|
||||
- `fx_test.gd`、`effect_playback_test.gd`、`target_effect_test.gd`、`skill_fx_test.gd` 及颜色、发射、面、lie、随机旋转、渲染回归、旋转、缩放、空间、surface、texture animation 测试均无断言失败;surface/texture 的 headless 像素检查明确跳过。
|
||||
- `motion_effect_anchor_test.gd` 的断言为 0 failures,但进程 exit 139,并泄漏 6 个 ObjectDB;因此不能升级为稳定回归通过。
|
||||
|
||||
### Static parity conclusion
|
||||
|
||||
- MDE/MSE 基础解析、粒子曲线、材质/颜色/纹理动画和本地主角基础挂点存在,但 `MotionEvent` 仍未完整保留 FLY、SPECIAL_ATTACKING、SCREEN_WAVING 参数以及 CHARACTER_SHOW/HIDE/WARP 专用字段。
|
||||
- `game_scene.gd` 主要连接本地主角;RemotePlayerView/MobView 的动作没有接入 40250 式统一 motion-event 分派,因此远端特效、飞行、音效、隐藏、震屏和 warp 仍不一致。
|
||||
- 当前 type 4/7/8/9 生产分支缺失,资产统计分别发现 818、47、47、47 个事件;type 6 也未按参考 `.fly` 起点、目标、owner/skill 形成等价飞行实例。
|
||||
- 基础 FX 断言通过不能覆盖 splash 命中去重、事件跨帧/loop、目标删除、死亡、地图切换和实例回收;挂点测试的 exit 139/泄漏需先修复。
|
||||
|
||||
### Active source group review
|
||||
|
||||
- 对 Visual Studio `EffectLib` 活跃编译单元逐组核对后,参考链补充确认:`CEffectData::LoadScript` 同时加载 mesh、particle、light 和 `.mss` 声音数据;`CEffectManager::RegisterEffect/CreateEffectInstance/Update/Render/Destroy` 负责缓存、实例池、更新/渲染顺序和销毁;`CParticleSystemInstance`/`CParticleInstance`、`CEffectMeshInstance`、`CLightInstance` 分别维护粒子、网格帧/纹理帧和光源生命周期。
|
||||
- 当前端虽然有 `EffectPlayer`/MDE/MSE/registry 的格式和局部播放测试,但没有证明声音脚本、mesh/light 三类实例、固定池耗尽、注册失败和 `DestroySystem` 的顺序与参考一致;本组静态核对完成,合同仍为 `PARTIAL`。
|
||||
|
||||
## 已确认差异和未验证项
|
||||
|
||||
1. 已将 `MotionEvent` 扩展为按 40250 事件类型保留专用数据,并让 `motion_event_detailed` 传完整字典;本地主角和真实远端 Actor 已消费 type 1/7/8,仍需证明所有分支使用这些字段。
|
||||
2. 已把动作事件连接扩展到 `NetWorld` 创建的真实远端 PC/NPC/Mob;仍需把震屏、局部 FX、飞行和隐藏行为按本地相机与远端 Actor 的范围/目标语义完整分开。
|
||||
3. 必须建立 `.msa` 事件快照测试:每种事件的字段、帧、跨帧/loop、重复触发和缺字段回退;并建立与 40250 样本资源的数量和时序基线。
|
||||
4. 必须增加 SPECIAL_ATTACKING splash 的碰撞、命中去重、持续时间、死亡/删除/切图清理测试,以及 FLY `.fly` 的起点、目标、owner/skill 和回收测试。
|
||||
5. `motion_effect_anchor_test` 的 exit 139 和 ObjectDB 泄漏需要先修复测试稳定性,才能把基础挂点证据提升为可靠回归门禁。
|
||||
|
||||
## Implementation fix round 2026-09-22 — dedicated MotionEvent payload and local show/hide
|
||||
|
||||
- 对照 `RaceMotionDataEvent.h` / `RaceMotionData.cpp`,`formats::MotionEvent` 现在独立保存:
|
||||
- type 6 的 `FlyFileName`、`FlyPosition`、挂接骨骼;
|
||||
- type 2 的 `DuringTime`、`Power`、`AffectingRange`;
|
||||
- type 4 的 `DuringTime`、`EnableHitProcess`、攻击参数和 `SphereData*`;
|
||||
- type 7/8/9 的启动时间和稳定的详细事件字典。
|
||||
- `Metin2AnimPlayer::get_events()` / `motion_event_detailed` 已传出 `fly_*`、震屏参数、特殊攻击参数和 `collision_spheres`。
|
||||
- `GameScene` 已按 40250 的 type 7/8 事件帧调用本地 Actor 的 `__HideEvent` / `__ShowEvent` 等价接口;`PlayerView` / `MobView` 暴露同一 Actor 级可见性 seam。
|
||||
- `NetWorld` 已按 40250 的每 Actor 事件处理边界连接远端 native animator,`GameScene` 消费远端 type 1/10 FX、type 7/8 显示隐藏和 direct sound;远端 type 6 不发客户端包。
|
||||
- 回归:`formats_msa_test`、真实 type 2/4/6/7 资源的 `motion_event_payload_test`、`motion_event_visibility_test`、`net_world_motion_event_test` 通过。
|
||||
- 本轮仍为 `PARTIAL`:`.fly` 资源和飞行实例、splash 碰撞/去重/清理、type 9 warp、震屏范围和统一跨图清理没有完成。
|
||||
|
||||
## Implementation fix round 2026-09-22 — WARP event branch
|
||||
|
||||
- 对照 `GameLib/ActorInstanceMotionEvent.cpp::ProcessMotionEventWarp` 和 `ActorInstanceEvent.cpp::__OnWarp`,`GameScene::_apply_motion_warp` 已实现相同的 270cm 单位换算、目标前落点、地图阻挡拒绝、朝向目标和本地主角 `FUNC_WAIT` 状态回调;远端 Actor 也消费 type 9,但不伪造本地网络包。
|
||||
- `project/motion_event_warp_test.gd` 使用真实 `seomjeon.msa` 验证 type 9,并覆盖可移动、阻挡、无目标三条分支;测试通过。
|
||||
- 当前合同仍为 `PARTIAL`:type 4 splash、type 6 `.fly` 数据权威、震屏参数/范围和所有 Actor 的跨图/死亡清理仍未闭合。
|
||||
|
||||
## 下一轮验证入口
|
||||
|
||||
- 先以本轮 `formats/msa`/native payload 为输入,实现按 Actor 的事件连接/路由。
|
||||
- 以 type 4、6、7、8、9 的真实资产各选样本,补逐帧事件、碰撞、飞行、显示隐藏和 warp 测试。
|
||||
- 统一本地/远端/怪物/切图/死亡的 FX 实例清理,并把 `fx_spawned`/`fx_finished` 纳入可审计事件日志。
|
||||
@@ -0,0 +1,100 @@
|
||||
# render.model_attachment —— Granny 模型、LOD、骨骼、武器、盾牌和发型挂点
|
||||
|
||||
状态:`PARTIAL`
|
||||
|
||||
本合同检查 40250 的模型实例生命周期、Granny 骨骼姿态、LOD 控制器、装备/发型挂点、资源换装、材质替换、碰撞/包围盒和清理语义。判断标准是模型数据来源、LOD 分档、姿态传播、子模型重新挂点、装备更新顺序、缺失资源回退和销毁顺序一致;“能加载模型”或“测试没有断言失败”不能单独视为与 40250 等价。
|
||||
|
||||
## 40250 参考调用链
|
||||
|
||||
- `EterGrnLib/ThingInstance.cpp` / `ThingInstance.h`
|
||||
- `CGraphicThingInstance` 为每个 Granny LOD 持有 `CGrannyLODController`,模型实例和附着子模型在每个 LOD 上保持关联。
|
||||
- `AttachModelInstance` 将子模型挂到父模型骨骼;切换活动 LOD 时要同步活动模型、动作和附着模型,而不是只替换一张渲染网格。
|
||||
- `EterGrnLib/LODController.cpp` / `LODController.h`
|
||||
- Actor LOD 使用 500cm 高度/5000cm 距离范围,距离分档的关键阈值为 500cm、1500cm、2500cm;选中模型后复制动作、刷新附着模型,并按距离调整动作 FPS。
|
||||
- 控制器同时参与更新、变形、渲染、碰撞和包围盒计算;LOD 资源缺失时按 40250 的模型队列回退。
|
||||
- `EterGrnLib/ModelInstance*.cpp`
|
||||
- `CGrannyModelInstance::UpdateWorldPose` 采样骨骼姿态;`GetBoneMatrixPointer` / `GetCompositeBoneMatrixPointer` 为挂点提供骨骼矩阵;`SetParentModelInstance` 维护子模型父骨骼关系。
|
||||
- `GameLib/ActorInstanceAttach.cpp`
|
||||
- `PART_WEAPON`、`PART_WEAPON_LEFT`、`PART_HAIR`、`PART_ARMOR` 通过 RaceData 注册的 attaching bone 连接;缺少左手挂点时不伪造挂接。
|
||||
- `GameLib/RaceData.cpp` / `RaceData.h` / `GameLib/ItemData.cpp`
|
||||
- RaceData 解析基础模型、LOD 模型、挂点骨骼和 HairSkin;ItemData 提供装备模型、LOD 模型列表、材质与皮肤数据,换装时对所有 LOD 控制器生效。
|
||||
|
||||
## 当前端实现映射
|
||||
|
||||
- `extension/src/metin2_model.cpp` / `.h`
|
||||
- 已加载 GR2/MSM、构造 Skeleton3D 和 CPU/GPU skin mesh,解析兄弟文件 `_lod_01/_02/_03.gr2`,提供发型、武器、盾牌和材质替换入口。
|
||||
- 当前默认 LOD 距离为 `[18, 42, 90]` 米;`_set_lod` 只在一个基础骨骼上替换网格,并增加 0.18 秒 `LodGhost` 淡入淡出。它不是 40250 的多 `CGrannyModelInstance` 控制器切换,且未证明切换时动作、子模型、碰撞和所有挂点同步。
|
||||
- 刚性武器/盾牌通过 `slot_pre * base_world_pose[bone]` 更新;该路径已有实际 03150 双手武器姿态失败,说明矩阵语义尚未达到参考实现。
|
||||
- `extension/src/metin2_anim.cpp`
|
||||
- 每个采样动作姿态后调用 `Metin2Model::update_weapon_pose`,当前能驱动本模型的武器/盾牌,但尚无完整 LOD 控制器级动作复制和附着子模型矩阵证据。
|
||||
- `extension/src/gr2_bridge.cpp`
|
||||
- 提供 GR2 文件、骨骼、材质、网格和姿态桥接;资源解析及单位/坐标转换是 Godot 适配层,不等同于 40250 的 Granny Thing/LOD 生命周期。
|
||||
- `project/ui/player_view.gd` / `remote_player_view.gd` / `mob_view.gd`
|
||||
- 已按角色、远端角色和怪物创建 `Metin2Model`,设置动作、武器/左手骨骼、发型和基础模型;远端与怪物共享部分 PlayerView 逻辑,但不同 Actor 的完整装备/LOD/事件消费矩阵还没有 40250 级别的逐分支证据。
|
||||
- `project/ui/equip_model.gd`
|
||||
- 已实现身体、发型/头部、武器的刷新顺序、武器双手判定、MSM HairData 与皮肤选择以及部分精炼效果;当前装备模型主要解析单个 GR2,`_is_poly()` 仍为固定 `false`,变身/遮挡/资源列表与 40250 的 ItemData/RaceData 语义不完全一致。
|
||||
|
||||
## 分支等价矩阵
|
||||
|
||||
| 40250 判据/副作用 | 当前实现 | 结论 |
|
||||
|---|---|---|
|
||||
| GR2/MSM、骨骼绑定姿态、CPU/GPU 变形 | `gr2_bridge` + `Metin2Model` + `Metin2AnimPlayer` 已能加载并采样真实 GR2 | 基础路径存在;GPU/CPU、材质边界和骨骼矩阵仍缺逐资产等价证明 |
|
||||
| Actor LOD 阈值 500/1500/2500cm、模型队列和距离范围 | 默认 `[18,42,90]m`,即约 1800/4200/9000cm,并带自定义 15% hysteresis | 不等价;默认常量已与 40250 Actor 分档不同 |
|
||||
| LOD 切换时复制动作、刷新装备/发型/盾牌挂点、碰撞和包围盒 | 只替换当前 `MeshInstance3D` 网格,保留一个骨骼并创建 `LodGhost` 淡入淡出 | 不等价;没有控制器级子模型和完整副作用同步 |
|
||||
| 右手/左手骨骼解析及武器/盾牌挂接 | `player_view`/`equip_model` 注册骨骼,`update_weapon_pose` 更新刚性附件 | 部分等价;`weapon_attach_test.gd` 的 warrior/run/03150 左手握持距离为 0.75m,超过 0.12m 阈值 |
|
||||
| 发型骨骼按名称重映射、HairSkin/SourceSkin→TargetSkin | `_load_hair` 重映射基座骨骼并合并 CPU/GPU mesh,支持 skin override | 部分等价;缺少对所有 LOD、材质数组和缺失骨骼回退的参考级证据 |
|
||||
| Armor/weapon/hair 资源更新及多 LOD 生命周期 | `EquipModel` 按身体→头发→武器重建 Godot 子树 | 部分等价;重建顺序存在,但不是 RaceData/ItemData 驱动的全 LOD `Change*`/`SetMaterialImagePointer` 链 |
|
||||
| 缺模型、缺骨骼、无左手挂点、缺 LOD 的回退 | 有 GR2/MSM 加载失败日志、骨骼索引 `-1` 和连续 `_lod_0N` 查找 | 部分等价;资源列表、非连续 LOD、子模型清理和缺失挂点的所有边界尚未对齐 |
|
||||
| visual/collision/fly target bounds 与地面贴合 | 提供 visual AABB、骨骼 OBB、ground offset 和飞行目标 bounds | 部分等价;当前测试覆盖基础包围盒,但未证明 40250 LOD/碰撞控制器语义和动作切换后的 bounds 一致 |
|
||||
| 删除、换图、死亡和重复换装时释放模型/材质/纹理 | `reload` 清理子节点、GR2、材质和缓存引用 | 部分等价;真实模型测试普遍出现 ObjectDB 泄漏,进程有 exit 139,清理不能作为已等价 |
|
||||
|
||||
## 已执行测试
|
||||
|
||||
- `project/weapon_attach_test.gd`:失败 1 项;warrior race 0 的 `run` 动作中 `03150.gr2` 左手到武器可见 AABB 距离为 0.750m,期望小于 0.12m;进程另有 exit 139 和 52 个 ObjectDB 泄漏。
|
||||
- `project/gpu_lod_attachment_test.gd`:`failures=0`、`ghosts=48`;但有 48 个 ObjectDB 泄漏,且测试证明的是当前自定义网格/ghost 路径,不证明 40250 LOD 控制器等价。
|
||||
- `project/gpu_pose_bounds_test.gd`:8 个 race × 6 个 motion × 5 个 sample,`failures=0`;有 48 个 ObjectDB 泄漏。
|
||||
- `project/race_motion_assembly_test.gd`:8 个 race × 6 个 motion × 3 个 sample,`failures=0`;有 82 个 ObjectDB 泄漏。
|
||||
- `project/equip_model_test.gd`、`project/remote_player_test.gd`、`project/mob_view_test.gd`:基础装备/远端角色/怪物模型断言通过;分别观察到 0、8、8 个 ObjectDB 泄漏,其中远端样本的 03150 武器和 00010 发型出现骨骼匹配告警。
|
||||
- `project/rendering_scenario_test.gd`:GPU 0/1 两种场景 `failures=0`;有 54 个 ObjectDB 泄漏。
|
||||
- `project/fly_target_bounds_test.gd`、`project/mob_winding_test.gd`:断言通过,但仍存在模型资源/RID 清理告警。
|
||||
- `project/animation_cache_test.gd`:`hot motion switches reuse decoded clips` 和 `hot switches reuse parsed MSA metadata` 失败各 1 项;统计为 hits=3/misses=3、msa_hits=3/msa_misses=3,进程 exit 139。该结果是模型动作资源生命周期的相关回归证据,不能视为挂接已通过。
|
||||
|
||||
## Deep audit round 2026-09-21
|
||||
|
||||
### Fresh regression results
|
||||
|
||||
- `weapon_attach_test.gd` 仍失败 1 项:warrior/run/03150 双手姿态左手挂点距离 0.750m,超过 0.12m 阈值;进程 exit 139,并泄漏 52 个 ObjectDB。
|
||||
- `gpu_lod_attachment_test.gd`、`gpu_pose_bounds_test.gd`、`race_motion_assembly_test.gd` 的断言分别为 0 failures,但均 exit 139,分别泄漏 48、48、82 个 ObjectDB。
|
||||
- `equip_model_test.gd` 断言通过;`remote_player_test.gd`、`mob_view_test.gd` 断言通过但各有 8 个 ObjectDB 泄漏并 exit 139;`animation_cache_test.gd` 仍有 decoded clip/MSA metadata 热切换各 1 项失败,exit 139。
|
||||
- `formats_map_test`、`formats_msa_test`、`formats_msm_test`、`net_bounds_test`、`net_entity_test`、`net_classic_session_test`、`net_classic_wire_test` 均退出码 0;formats live A1 断言因未设置 `M2_ASSETS` 跳过。
|
||||
|
||||
### Static parity conclusion
|
||||
|
||||
- 40250 的 500/1500/2500cm Actor LOD 控制器、动作复制和每个 LOD 的附着模型生命周期仍未被当前端的 `[18,42,90]m` 单骨骼网格替换等价实现。
|
||||
- 03150 左手姿态失败和 animation cache 失败是可重复的实现差异;不能用 GPU/CPU bounds 或基础装备测试通过掩盖矩阵和资源缓存问题。
|
||||
- 当前端真实模型路径仍有大量 exit 139/ObjectDB 泄漏,换图、死亡、重复换装、LOD 切换和远端模型删除的清理语义尚不能视为等价。
|
||||
|
||||
### Active source group review
|
||||
|
||||
- 对 Visual Studio `EterGrnLib` 活跃编译单元逐组核对后,参考链补充确认:`CGraphicThing` 维护模型/动作资源,`CGraphicThingInstance` 管理多模型实例、LOD 注册、Attach/Detach、材质/高光重绑、motion、bounds 和清理;`CGrannyLODController` 在每个 LOD 上同步模型、动作、材质、骨骼、shadow/bounds;`CGrannyModelInstance::UpdateWorldPose/UpdateWorldMatrices` 生成骨骼世界姿态;`CGrannyMaterial/Palette` 管理材质与 sphere map。
|
||||
- 当前 `Metin2Model` 的单骨骼/网格替换和 `slot_pre` 附件路径没有覆盖上述每个控制器的状态传播;这组核心源码已静态核对,但 03150 挂点、LOD、动作缓存和资源清理的已知差异仍保持 `PARTIAL`。
|
||||
|
||||
### Active GameLib attachment review
|
||||
|
||||
- `GameLib/ActorInstanceData.cpp` 的 `SetRace`/`SetShape`/`SetPart`、`AttachWeapon` 和 `UpdateAttachingInstances` 将 RaceData/ItemData 的 base、LOD、发型、武器/盾牌和挂接数据注册到多模型实例;`ActorInstance.h` 还暴露按 part 的 bone name、collision piece 和 effect attach 入口。
|
||||
- 当前 `EquipModel`/`PlayerView` 能重建身体、头发、武器和 `slot_pre` 附件,但没有同等的多 `CGraphicThingInstance` 控制器传播;03150 左手姿态失败、LOD ghost 泄漏和动作缓存失败仍是可重复差异。
|
||||
- 本轮完成活跃 `GameLib` actor 数据/挂接文件的静态核对;该证据进一步确认“单骨骼网格替换”不是 40250 的 1:1 实现,合同保持 `PARTIAL`。
|
||||
|
||||
## 已确认差异和未验证项
|
||||
|
||||
1. 必须先统一 Actor LOD 常量、距离单位、分档边界和缺失 LOD 回退,再决定 Godot 网格替换是否能保留 40250 的可观察副作用。
|
||||
2. 必须把 LOD 切换时的动作复制、武器/盾牌/发型/盔甲挂点、材质和 bounds 更新做成可观测状态,并用同一组真实 race/item/motion 资产与 40250 快照对照。
|
||||
3. 必须定位 `03150.gr2` 在 `run` 姿态的根/局部变换差异,建立右手、左手、双手武器、盾牌、弓和远端角色的逐动作挂点矩阵测试;当前不能以固定 `slot_pre` 通过一组静态样本代替。
|
||||
4. 必须覆盖非连续/缺失 LOD、缺骨骼、重复换装、死亡、换图和节点释放,并清除真实模型测试的 ObjectDB 泄漏与 exit 139。
|
||||
5. 必须补齐 `_is_poly`、变身装备遮挡和 ItemData/RaceData 多模型资源列表的等价实现或明确其平台适配边界。
|
||||
|
||||
## 下一轮验证入口
|
||||
|
||||
- 先用参考常量和真实 `_lod_0N.gr2` 资产建立 LOD 分档快照,分别验证 0/500/1500/2500/5000cm 边界。
|
||||
- 再对 03150、盾牌、弓、发型和远端 Actor 采集每个动作采样点的父骨骼/子模型矩阵与可见 bounds,定位坐标系或 authored root offset 差异。
|
||||
- 最后修复模型测试清理/崩溃,再把装备换装、LOD 切换、跨图删除和资源复用纳入稳定回归门禁。
|
||||
@@ -0,0 +1,127 @@
|
||||
# resource.pack-locale-proto
|
||||
|
||||
## Scope
|
||||
|
||||
比较 40250 的 EterPack 文件查找/覆盖、路径归一化、缓存、locale 编码、Python `pack.Get/Exist`、item/mob proto、MSM/MSA 和当前端资源入口、失败回退及测试证据。
|
||||
|
||||
## Reference call chain
|
||||
|
||||
- `EterPack/CEterPackManager::Get`
|
||||
- 按 `SEARCH_FILE_FIRST` / pack-first 选择 loose file 或 pack。
|
||||
- `ConvertFileName` 统一小写、反斜杠为 `/`,以 `CEterFileDict` 的 CRC/name 映射 pack。
|
||||
- `GetFromPack` 支持根包、目录包、静态缓存和 pack overlay。
|
||||
- `EterPack/CEterPackCursor`
|
||||
- 对 pack 内数据提供有边界的 `Read/Seek/Size`。
|
||||
- `UserInterface/PythonPackModule.cpp`
|
||||
- `pack.Exist` 查询虚拟路径。
|
||||
- `pack.Get` 只对 `.py/.pyc/.txt` 返回原始字节,其余失败。
|
||||
- `EterLocale/StringCodec.cpp` 及 locale 变体
|
||||
- 按 locale code page 处理非 UTF-8 文本,再交给 Python/UI。
|
||||
- `GameLib/ItemManager.cpp::LoadItemTable/LoadItemList/LoadItemDesc`
|
||||
- 从 EterPack 获取 MIPT/MIPX item proto、文本 item list 和 itemdesc;检查 fourcc、version、stride、数据尺寸及记录合法性。
|
||||
- `UserInterface/PythonNonPlayer.cpp::LoadNonPlayerData`
|
||||
- 从 EterPack 获取 MMPT mob proto,检查 fourcc、解压尺寸和记录。
|
||||
- `GameLib/RaceDataFile.cpp`、`UserInterface/PythonSkill.cpp`
|
||||
- 通过资源管理器加载 MSM/MSA、动作列表、技能和模型依赖。
|
||||
|
||||
## Current call chain
|
||||
|
||||
- `project/asset_root.gd` / `project/asset_pack.gd`
|
||||
- loose `assets/`、Godot `assets.zip`/PCK 和 `asset_index.txt` 是主运行时资源入口。
|
||||
- `formats/asset_resolver.cpp`
|
||||
- 扫描目录或装载 `MTIDX1` 索引;按固定一级目录优先级和 `ymir work/` 后缀选择路径。
|
||||
- `extension/src/pack/eterpack.cpp` / `pack_mount.cpp` / `asset_source.cpp`
|
||||
- 实现当前 fork 自定义 XChaCha20 + Zstd `.epk`,并提供 loose/pack 读取和缓存抽取;目前没有被 GameScene/AssetRoot 主链调用。
|
||||
- `extension/src/proto/proto.cpp` / `proto_node.cpp`
|
||||
- 读取 MIPX/MIPT/MMPT、CLZO 和 proto key,建立 item/mob 索引。
|
||||
- `project/locale.gd`
|
||||
- 从 `locale/locale/<lang>` 直接用 Godot `FileAccess` 读取 4 个文本表及 item/mob names;缺失表静默保留空字典。
|
||||
- `formats/msm.cpp`、`formats/msa.cpp`、`formats/textscript.cpp`、`formats/combo_table.cpp`
|
||||
- 解析动作、模型和脚本格式,测试覆盖合成输入及部分真实资源。
|
||||
|
||||
## Audit findings (2026-09-20)
|
||||
|
||||
### 已对齐的部分
|
||||
|
||||
- `proto.cpp` 已支持 40250 实际使用的 MIPX/MIPT/MMPT fourcc、版本/stride/element 边界、CLZO 解压和 item/mob 固定字段;item `values[3]`、specular、mob type/rank/level 等调用方字段已有测试。
|
||||
- `AssetResolver` 和 `PackMount` 都处理大小写/斜杠、`d:/ymir work/` 后缀和后挂 pack 覆盖;MSM/MSA、文本脚本和连击表已有格式级回归。
|
||||
- locale、proto、MSA/MSM 资源都已经从当前 assets 目录进入游戏场景的不同子系统,缺少资源时多数调用方有明确 fallback。
|
||||
|
||||
### 材料差异
|
||||
|
||||
- 40250 的主资源权威是 `CEterPackManager`,而当前主运行时是 loose/`assets.zip`/Godot PCK + `AssetResolver`。`AssetSource`/`PackMount` 目前没有被 `AssetRoot`、`Metin2World`、模型、音频和 UI 统一调用;因此“实现了 pack reader”不等于“游戏实际按 40250 pack 优先级加载”。
|
||||
- 当前 `EterPack` 使用 fork 自定义的 XChaCha20 + Zstd 单文件格式,40250 `CEterPack` 使用其原有 pack header/index/压缩/加密/策略链;两者不能互读。它是明确的平台/格式替换,不是已证明等价的适配。
|
||||
- 40250 的 `SEARCH_FILE_FIRST`/pack-first、根包与目录包、CRC 字典、静态缓存、文件覆盖优先级和 pack 内失败语义没有在统一运行时入口中闭环;`AssetResolver` 的固定目录 rank 不能直接证明与所有 40250 pack 注册顺序一致。
|
||||
- `EterLocale` 按 code page/locale 变体转换文本;当前 `locale.gd` 直接按 UTF-8 `FileAccess` 读取。中文 assets 目前可用,但 CP125x、Vietnamese、Arabic、Japanese 等路径和异常字节行为没有等价证明。
|
||||
- `locale.gd` 只加载 `locale_interface.txt`、`locale_game.txt`、`itemdesc.txt`、`skilldesc.txt` 及两个 names 表;40250 的 Python/locale 系统会按功能继续读取 jobdesc、empire、insult、guild、地图和 UI 路径等资源。各调用方自行拼路径,缺表时容易静默退化为 key/空值。
|
||||
- proto 当前单独由 GameScene 直接拼 `assets_root/locale/...` 文件路径加载;当资源只存在于当前自定义 pack 或 Godot PCK 内且没有解出到真实文件系统时,`Metin2Proto`/`SkillTable`/`Locale` 不共享同一虚拟读取器,可能出现 proto/文本/模型“一部分能读、一部分找不到”。
|
||||
- 40250 `CItemManager::LoadItemTable`、`CPythonNonPlayer::LoadNonPlayerData` 的具体错误分支和记录尺寸校验较完整;当前 `proto_node` 失败后只写 `last_error` 并由上层继续使用空表,ItemManager 的“阻止未知 vnum/缺记录”语义还没有全部接到物品/模型入口。
|
||||
- MSA/MSM 格式解析已有较强测试,但资源失败时当前部分视图使用动作别名、目录文件 fallback 或胶囊/占位模型;这些是可见行为替换,不能仅凭解析测试宣称与 40250 的 `CResourceManager` 失败/保留当前动作语义一致。
|
||||
|
||||
## Equivalence matrix
|
||||
|
||||
| 项目 | 状态 | 备注 |
|
||||
|---|---|---|
|
||||
| Preconditions | PARTIAL | 文件/pack/proto 存在性和尺寸有检查;统一虚拟资源入口及编码前置未闭合 |
|
||||
| Branch structure | PARTIAL | loose/pack、proto fourcc、MSA/MSM 主分支已定位;搜索模式、locale 变体、失败回退未全对齐 |
|
||||
| Algorithms/formulas | PARTIAL | proto 字段/CLZO 和路径归一化已对齐;pack 格式、CRC/cache 和目录优先级不等价 |
|
||||
| State transition order | PARTIAL | GameScene 资源初始化顺序已定位;pack mount→proto→UI/model 的统一顺序无证据 |
|
||||
| Constants/units | PARTIAL | proto stride/offset 和 pack name field 有测试;code page、压缩/加密参数属于不同格式 |
|
||||
| Timing/event sources | PARTIAL | 当前同步加载与 `AssetResolver` 索引存在;40250 的 pack cache/lazy mapped read 未闭环 |
|
||||
| Resource/data sources | PARTIAL | MIPX/MMPT/MSA/MSM 可读取;主链仍可能绕过 PackMount,locale 编码和完整资源集合不足 |
|
||||
| Protocol side effects | MAPPED | 该合同主要是本地资源,不产生网络协议副作用;proto 失败对物品/实体的上层影响仍属 remaining |
|
||||
| Interruption/failure/cleanup | PARTIAL | 单个读失败有错误返回;切换 locale、pack 重载、cache 失效和资源部分失败清理未完整证明 |
|
||||
|
||||
## Regression evidence
|
||||
|
||||
- `extension/tests/pack_roundtrip_test.cpp`:当前自定义 `.epk` 的加密/压缩、路径归一化、ymir suffix 和 patch 覆盖。
|
||||
- `extension/tests/proto_test.cpp`、`extension/tests/proto_item_layout_test.cpp`:真实/合成 item_proto、mob_proto、stride 和字段偏移。
|
||||
- `formats/tests/msm_test.cpp`、`formats/tests/msa_test.cpp`、`formats/tests/combo_table_test.cpp`、`formats/tests/map_formats_test.cpp`:格式解析与真实资源样本。
|
||||
- `project/test_chinese_locale.gd`、`project/skill_test.gd`:locale 和技能文本 smoke test。
|
||||
|
||||
`project/package_render_test.gd` 未计入本轮通过证据:它要求导出包的 `MT_TEST_MODE=render` 入口,直接用 `godot --headless --script` 不会复现该生命周期。
|
||||
|
||||
## Deep audit round 2026-09-21
|
||||
|
||||
### Fresh regression results
|
||||
|
||||
- `pack_roundtrip_test`、`proto_test`、`proto_item_layout_test`、`formats_map_test`、`formats_msa_test`、`formats_msm_test`、`formats_combo_table_test` 均退出码 0;proto live 文件缺失时按测试设计跳过,formats live A1 在未设置 `M2_ASSETS` 时跳过。
|
||||
- `test_chinese_locale.gd` 16/16、`skill_test.gd` 通过;中文角色、怪物、物品和技能文本可读取。
|
||||
- `package_render_test.gd` 在直接 `godot --headless --script` 入口未能结束,和合同中要求的导出包 `MT_TEST_MODE=render` 生命周期不一致,不能计为通过。
|
||||
|
||||
### Static parity conclusion
|
||||
|
||||
- 当前自定义 `.epk`、proto 和路径归一化测试通过,但主运行时仍以 loose/assets.zip/PCK + AssetResolver 为主,PackMount/AssetSource 没有统一接入模型、音频、UI、proto 和 locale 主链。
|
||||
- 自定义 XChaCha20+Zstd `.epk` 不能互读 40250 原生 EterPack;pack-first/search order、CRC 字典、cache overlay 和失败回退仍不是同一实现。
|
||||
- 中文 UTF-8 路径可用,但 EterLocale code page/异常字节/完整功能文本表和 proto/模型共享虚拟读取器仍缺等价证据;缺资源时部分入口继续用空表/占位/动作别名。
|
||||
|
||||
本轮仍为 `PARTIAL`,完成资源、pack、locale、proto 第二轮证据登记,未修改实现。
|
||||
|
||||
### Active EterLib resource-core review
|
||||
|
||||
- `EterLib/Resource.cpp` 的 `CResource` 通过 `CEterPackManager` 加载、重载、清理和错误回退;`ResourceManager.cpp` 维护资源缓存、CRC、后台加载、资源 map、延迟删除和 `Update` 生命周期;`FileLoaderThread.cpp` 将读取请求、取数和处理拆成线程阶段。
|
||||
- `TextFileLoader.cpp`/`parser.cpp` 提供缓存模式、token/group 解析和带类型的 token 读取,和当前仅按 `FileAccess` 读文本的路径不是同一失败/缓存/异步语义。当前已有格式和 `.epk` 单元测试,但没有证明主运行时的模型、音频、UI、proto、locale 都经过同一个资源管理器。
|
||||
- 因此本轮将上述 EterLib 活跃文件归入资源合同的静态源码证据;缓存命中、CRC 失败、后台加载取消、延迟删除和 typed-token 错误分支仍未在当前端形成等价实现,合同继续为 `PARTIAL`。
|
||||
|
||||
### Active EterPack/EterLocale/GameLib resource review
|
||||
|
||||
- `EterPack/EterPack.h`/`EterPackManager.h`/`EterPackCursor.h` 明确了 EPKD v2 header/index、filename CRC 字典、root/dir pack、`SEARCH_FILE_FIRST`/`SEARCH_PACK_FIRST`、静态 cache、mapped read、读写/删除/Extract 和 pack policy;`EterPackPolicy_CSHybridCrypt.h` 还定义 Camellia/Twofish/XTEA、每文件 key、SDB 和密钥流生命周期。当前自定义 XChaCha20+Zstd `.epk` 不能互读这套格式。
|
||||
- `EterLocale/CodePageId.h` 覆盖 CP874/932/936/949/950/1250-1258/65001;`StringCodec.h`、Vietnamese/Arabic/Japanese 头文件分别暴露 code-page 转换、越南语编解码、阿拉伯语 shaping 和 Shift-JIS lead/trail/比较规则。当前 locale 以 UTF-8 `FileAccess` 为主,没有这些非 UTF-8/RTL/Shift-JIS 分支。
|
||||
- `GameLib/Property*`、`ItemManager.h`、`RaceManager.h`、`RaceMotionData.h` 连接 property CRC、物品/种族/动作资源到 pack;当前 proto/MSM/MSA 格式能读不等于完整 `CPropertyManager`/`CRaceManager`/`CItemManager` 资源注册链已一致。
|
||||
- 本轮完成 EterPack、EterLocale 和 GameLib 资源声明/生命周期静态核对,合同保持 `PARTIAL`。
|
||||
|
||||
### Active EterLib image/font resource review
|
||||
|
||||
- `EterLib/Util.cpp` 的 `LoadTextData`/`LoadMultipleTextData` 直接通过 `CEterPackManager` 读文本;同一文件还依据 code page 选择 charset/font face。`TargaResource.cpp`、`JpegFile.cpp`、`GrpImage.cpp`/`GrpImageTexture.cpp`/`GrpSubImage.cpp` 负责从资源字节到图像、纹理、子图和 GPU 纹理的 load/clear 生命周期。
|
||||
- `GrpDIB.cpp`/`DibBar.cpp`/`BlockTexture.cpp` 是 Windows DIB 到分块纹理的路径,涉及 clip、invalidate、锁定/解锁和最大纹理尺寸;当前 UI/资源侧使用 Godot `ImageTexture`/atlas,缺少 DIB/分块上传、纹理丢失重建及 pack 失败分支的等价证据。
|
||||
- 本轮完成 EterLib 文本/图像资源活跃文件静态核对;当前格式解析通过不等于 `CResourceManager`→图像纹理→UI 的同一资源和清理链,合同继续为 `PARTIAL`。
|
||||
|
||||
### Active UserInterface locale/font bridge review
|
||||
|
||||
- `UserInterface/Locale.cpp`/`.h` 与 `Locale_inc_*` 通过编译期区域选择 code page、字体、语言、地图/货币/日期和功能开关;`GameType.cpp` 的 `DefaultFont_Startup/Cleanup/ReloadDefaultFonts` 从 `CResourceManager` 重载默认/斜体字体,`SetGuildSymbolPath` 参与会徽资源入口。
|
||||
- 当前 `locale.gd` 以运行时 lang/UTF-8 文本表为主,中文路径可用,但没有编译期 locale 变体、code-page 字体选择、默认字体资源重载和 locale-specific feature flags 的同一来源。
|
||||
- 本轮完成 UserInterface locale/font bridge 静态核对;资源合同继续为 `PARTIAL`。
|
||||
|
||||
## Status
|
||||
|
||||
`PARTIAL`。proto 和多种格式读取已经有较好基础,但当前运行时资源主链、40250 EterPack 格式/优先级、locale 编码、完整失败分支和统一虚拟读取入口尚未达到一致实现。
|
||||
@@ -0,0 +1,204 @@
|
||||
# skill.cast-effect-timing
|
||||
|
||||
## Scope
|
||||
|
||||
比较技能等级、目标选择、施法前置、动作事件、伤害/状态结果以及粒子和命中特效的时序。
|
||||
|
||||
## Reference call chain
|
||||
|
||||
- `UserInterface/PythonSkill.cpp`
|
||||
- `UserInterface/PythonPlayerSkill.cpp`
|
||||
- `UserInterface/PythonNetworkStreamPhaseGame.cpp`
|
||||
- `UserInterface/InstanceBaseEffect.cpp`
|
||||
- `EffectLib/EffectInstance.cpp`
|
||||
- `EffectLib/ParticleInstance.cpp`
|
||||
- `GameLib/ActorInstanceMotionEvent.cpp`
|
||||
- `UserInterface/InstanceBase.cpp`
|
||||
|
||||
## Current call chain
|
||||
|
||||
- `project/player_skill.gd`
|
||||
- `project/skill_table.gd`
|
||||
- `project/net_play.gd`
|
||||
- `project/net_world.gd`
|
||||
- `project/fx/skill_fx.gd`
|
||||
- `project/fx/effect_player.gd`
|
||||
- `extension/src/net/entity_store.cpp`
|
||||
- `extension/tests/net_state_queue_test.cpp`
|
||||
- `project/combat_fx_test.gd`
|
||||
- `project/skill_test.gd`
|
||||
|
||||
## Initial finding
|
||||
|
||||
当前存在技能表、技能上下文和特效测试,但这些测试主要证明局部数据/材质行为,尚未证明完整 40250 施法链路。技能动作事件、挂点、目标范围、冷却、服务端结果和特效停止/重播仍需要逐分支对照。
|
||||
|
||||
## Current finding
|
||||
|
||||
`combat_fx_test.gd` 的失败是测试选择器使用本地化文本造成的,并非技能业务失败;现已改为按 40250 的控制节点名定位并通过。更重要的是,实体状态机此前没有复刻 40250 的 `eFunc & FUNC_SKILL` 位判断,移动中的技能会在到达目标时回到 WAIT;现已补齐远近两种分支并加入 C++ 回归测试。
|
||||
|
||||
本轮继续追到视觉动作层后,发现还有一个具体的参数丢失:`project/net_world.gd::_apply_anim` 只从 `FUNC_SKILL` 取 `motion_idx` 并调用 `PlayerView.play_skill_motion(motion_name)`,没有把 40250 `NEW_UseSkill`/到达逻辑中的 `uArg & 0x0f` 动作循环次数和 `(uArg >> 4)` 移动技能标志传入视图;`PlayerView.play_skill_motion` 的 `grade` 也没有来自网络状态的真实来源。这样只能证明“技能状态会进入”,不能证明每个技能的动作循环、移动技能动作和结束时机与 40250 一致。
|
||||
|
||||
同时,当前 `SkillFx`/`NetPlay` 仍有固定施法锁定和特效时长路径;40250 的关键帧、挂点、颜色/混合和动作事件来自 MSA/MSE 与 `ActorInstanceMotionEvent`。这些资源驱动事件尚未建立逐技能时间线,因此本合同保持 `PARTIAL`。
|
||||
|
||||
## Deep audit round 2026-09-20
|
||||
|
||||
本轮沿 `CInstanceBase::NEW_UseSkill`、`CActorInstance::ProcessMotionEvent*`、本地施法入口和远端 `FUNC_SKILL` 路径复核,并重新执行技能/战斗/特效回归:
|
||||
|
||||
- 40250 `NEW_UseSkill` 明确接收 `uMotLoopCount` 和 `isMovingSkill`,调用 `InterceptOnceMotion`、`__OnUseSkill`,再设置动作循环次数;当前 `NetPlay.on_use_skill()` 只把 `motion_skill` 和 `arg` 上行,`NetWorld._apply_anim()` 只取 `FUNC_SKILL` 的 motion index,调用远端 `play_skill_motion(mname)` 时没有把 `uArg & 0x0f` 循环次数、`uArg >> 4` 移动技能标志或技能 grade 传入。
|
||||
- 当前 `PlayerView.play_skill_motion()` 支持按 grade 选择 `.msa` 后缀,但正式远端网络动作路径没有 grade 来源;本地 `GameScene._on_skill_cast_started()` 能传 grade 并从 MSA 读取 duration/cancel_enable,两个路径的输入并不统一。
|
||||
- 当前 `SkillFx.spawn_skill()` 只在 `skill_fx_test.gd` 中被直接调用,正式 `GameScene`/`NetPlay` 没有生产调用;正式本地技能特效主要依赖 MSA motion event 和 `fx.spawn_motion`,因此技能表中的主特效/子特效映射没有被证明与 40250 的技能触发链一致。
|
||||
- 40250 的 `MOTION_EVENT_TYPE_FLY`、`EFFECT_TO_TARGET`、`SPECIAL_ATTACKING` 和 `SOUND` 都由动作事件驱动,带有攻击窗口、挂点、跟随目标、碰撞球和一次性/循环生命周期;当前 `effect_playback_test` 只验证通用 MSE 播放/停止/重播,`combat_fx_test` 只验证战斗门和桩,不是逐技能 MSA→MSE→命中结果时间线。
|
||||
- 本轮 `skill_test.gd`、`combat_fx_test.gd`、`effect_playback_test.gd` 和 `net_state_queue_test` 均退出码 0。它们证明现有表格、技能 UI、战斗状态机、通用特效播放和 `FUNC_SKILL` 队列不回归,但不能把动作循环、移动技能、grade、挂点/颜色/混合、飞行/命中事件和服务端结果判定视为 40250 等价。
|
||||
|
||||
本轮没有修改实现;技能基础校验和部分本地动作链可用,但正式技能特效接线、远端动作参数和资源驱动事件仍未闭合,合同保持 `PARTIAL`。
|
||||
|
||||
## Active MSA motion ownership review
|
||||
|
||||
本轮继续对照 `CInstanceBase::NEW_UseSkill`、`CActorInstance::SetMotionLoopCount` 和
|
||||
`CActorInstance::MotionEventProcess` 核对施法锁定的结束源。此前本地入口虽然已经绑定真实
|
||||
MSA 并支持 LoopData,但 `NetPlay` 仍会无条件按 clip duration 清除 `_using_skill`,因此
|
||||
当循环片段或动作尾部晚于名义 duration 时会提前解锁,和 40250 的动作所有权不一致。
|
||||
|
||||
现已修复:`GameScene._on_skill_cast_started()` 将“技能 MSA 是否成功绑定”传入
|
||||
`NetPlay.start_skill_cast()`;有效动作设置为 motion-driven,施法门只在
|
||||
`PlayerView` 的动作尾部发出 `motion_bound("wait")` 时清除;缺失 MSA 或 headless 测试仍使用
|
||||
duration 作为兼容兜底。`is_lock()` 与 `_process()` 两条计时路径均遵守该门,死亡/重置/网络
|
||||
状态失效也会清除 motion-driven 状态,避免下一次施法继承旧门。
|
||||
|
||||
新增回归覆盖“名义时长已过但 MSA 动作未结束仍保持锁定”和“动作尾事件后解锁”。
|
||||
`target_effect_test`、`test_no_auto_move_regression`、`netplay_test`、`skill_test`、
|
||||
`combat_fx_test`、`effect_playback_test`、`test_skill_state_arg_parity`、
|
||||
`net_world_remote_action_test`、`player_motion_test` 及既有战斗/技能循环测试均通过。
|
||||
|
||||
### Active GameLib skill-event review
|
||||
|
||||
- `GameLib/ActorInstanceEvent.cpp` 明确把 `OnAttack`、`OnUseSkill`、`OnHit` 和 affect 清理交给 actor event handler;`OnUseSkill` 的 `uLoopCount` 与 moving-skill bit 组合后才传给上层,不能用固定施法延迟完全替代。
|
||||
- `GameLib/ActorInstance.h` 的 motion queue、`IsUsingMovingSkill`、`CanCancelSkill`、`MotionEventProcess`、`ProcessMotionEventEffectToTargetEvent`、`ProcessMotionEventSpecialAttacking` 和 `ProcessMotionEventFly` 构成技能时序的消费者;当前 `GameScene` 主要处理本地主角 signal,远端/怪物的同一事件分支没有完整接线。
|
||||
- 本轮完成 GameLib actor skill/event 核对;现有 `skill_test`/`effect_playback_test` 仍只是局部资源与 helper 证据,技能合同继续为 `PARTIAL`。
|
||||
|
||||
## Equivalence matrix
|
||||
|
||||
| 项目 | 状态 | 备注 |
|
||||
|---|---|---|
|
||||
| Preconditions | PARTIAL | 等级、职业、目标、冷却已有本地校验;真实服务端权限仍待验证 |
|
||||
| Branch structure | PARTIAL | 单体/范围/辅助及失败分支已有代码映射;中断仍需端到端记录 |
|
||||
| Algorithms/formulas | PARTIAL | 范围/目标选择已映射,技能公式仍由服务端权威 |
|
||||
| State transition order | VERIFIED | `FUNC_SKILL|motion` 远近分支与到达后动作已按 40250 修复并测试 |
|
||||
| Constants/units | UNASSESSED | 范围、持续时间、速度和坐标 |
|
||||
| Timing/event sources | PARTIAL | 有效本地 MSA 的施法门现在由 `motion_bound("wait")` 动作尾释放,缺失动作才使用兼容计时器;逐技能 MSA 关键帧和特效事件仍未完整接线 |
|
||||
| Resource/data sources | UNASSESSED | skilldesc、skilltable、MSA、MSE 和挂点 |
|
||||
| Protocol side effects | PARTIAL | 技能状态上行、动作状态接收已覆盖;实际结果包仍需服务端验证 |
|
||||
| Interruption/failure/cleanup | PARTIAL | 远端不可取消技能/表情会阻止后续 TCP 状态,动作尾、节点剔除和换图会清门;可取消技能、死亡中断、重复施法和特效回收仍未全闭合 |
|
||||
|
||||
## Remaining
|
||||
|
||||
- 逐技能类型建立 40250 → Godot 分支映射。
|
||||
- 用至少一个真实攻击技能和一个辅助技能完成时序测试。
|
||||
- 远端 `FUNC_SKILL` 的动作循环次数、移动技能标志和等级动作索引已完成传递;本地有效 MSA
|
||||
的施法锁定已改为动作尾驱动;逐技能
|
||||
MSA 事件到 MSE 的挂点、颜色/混合和服务端结果仍需继续验证。
|
||||
- 继续补齐未实现的颜色、混合、挂点和动作事件语义。
|
||||
|
||||
## Active UserInterface skill-data review
|
||||
|
||||
- `PythonSkill.h` 还负责从 skill table/desc 注册技能、计算 cooldown/SP/continuation/loop/target/duration 和 motion index;这些值是 `PythonPlayerSkill`、quickslot 与 `ActorInstanceEvent` 的输入,不能由固定 UI 延迟代替。
|
||||
- 本轮确认技能数据桥已被 active Visual Studio 工程引用,但当前端动作事件链仍没有证明使用同一套资源字段驱动循环次数、移动技能标志、grade、飞行/命中/特殊攻击挂点和 cleanup;合同保持 `PARTIAL`。
|
||||
|
||||
## Implementation fix round 2026-09-21T12:30Z
|
||||
|
||||
本轮按 40250 `CActorInstance::__OnUseSkill` 的确切打包规则闭合技能动作参数丢失:
|
||||
`uArg = uLoopCount | (isMovingSkill ? 1 << 4 : 0)`。`SkillTable` 现在从 `.msk`
|
||||
的 `MotionLoopCountFormula` 按 `SkillPoint` 计算低 4 位,并把 `MOVING_SKILL` 属性编码到
|
||||
第 4 位;快捷栏和本地 `GameScene` 都使用同一份值,`M2Client.cast_skill` 不再固定发送
|
||||
零参数。
|
||||
|
||||
远端链路新增 `Entity.func_arg`,TCP state queue 在“走到目标后施法”与“近距离立即施法”
|
||||
两条分支都保留该参数;`NetWorld._apply_anim` 再解出循环次数和移动技能标志传给
|
||||
`PlayerView.play_skill_motion`。新增 `test_skill_state_arg_parity.gd` 与原生
|
||||
`net_state_queue_test` 断言了位编码、近距离分支和远距离到达分支;技能表/快捷栏、场景装配、
|
||||
战斗特效、通用播放及全量 23 项 CTest 均通过。
|
||||
|
||||
这一轮修复的是动作参数和状态链,不宣称已经实现 `SetMotionLoopCount` 的真实循环片段、
|
||||
远端 grade 的权威来源,或完整 MSA→MSE→命中结果时间线;挂点、颜色/混合、飞行/特殊攻击
|
||||
事件、服务端结果和中断清理继续保持 `PARTIAL`。
|
||||
|
||||
## Implementation fix round 2026-09-21 — remote motion gates
|
||||
|
||||
按 40250 `__CanProcessNetworkStatePacket` / `__IsEnableTCPProcess` 补齐远端动作期间的
|
||||
网络状态门:不可取消技能、活动表情和 TCP 状态禁用会保持后续 `GC_MOVE` 在队列中;技能或
|
||||
表情的真实一次性动作通过 `motion_bound("wait")` 尾事件释放,而不是使用固定计时器。`FUNC_EMOTION`
|
||||
的 `uArg` 也会尝试进入实际 `CRaceMotionData` action ID,出生时携带动作状态的实体会立即绑定
|
||||
初始动作。
|
||||
|
||||
`net_state_queue_test`、`net_world_remote_action_test`、`net_world_vis_test`、`netplay_test`、
|
||||
`player_motion_test`、`test_skill_state_arg_parity` 和完整 `ctest`(23/23)通过。技能可取消
|
||||
性的权威 MSA 数据、grade、逐技能关键帧/特效事件、双人表情目标和完整死亡/断线清理仍为
|
||||
`PARTIAL`。
|
||||
|
||||
## Implementation fix round 2026-09-22 — MSA LoopData / SetMotionLoopCount
|
||||
|
||||
按 40250 `CInstanceBase::NEW_UseSkill`、`CActorInstance::SetMotionLoopCount` 和
|
||||
`CurrentMotionProcess` 的实际语义补齐技能动作循环:`Metin2AnimPlayer` 现在读取当前
|
||||
`.msa` 的 `LoopData`,保留动作的 lead-in/tail,只在 `LoopStartTime..LoopEndTime` 片段
|
||||
内回绕;正数 `uMotLoopCount` 在动作绑定后覆盖资源默认次数,`0` 使用资源中的
|
||||
`MotionLoopCount`,`-1` 保持无限片段循环。循环边界会重复派发该段动作事件,最终尾部才发出
|
||||
`playback_finished`,因此 `motion_bound("wait")`/技能网络门不会提前释放。
|
||||
|
||||
`PlayerView.play_skill_motion()` 已把本地与远端统一传入的 loop count 交给原生播放器。
|
||||
新增 `skill_motion_loop_test.gd` 覆盖 PlayerView 参数传递、真实 `yeonsa.msa` 的资源默认值、
|
||||
覆盖为 2 时的一次片段回绕、计数递减、尾部结束和切换普通动作后的清理;技能表、状态参数、
|
||||
远端动作、特效播放和 PlayerView 回归均通过。
|
||||
|
||||
该轮闭合了 `SetMotionLoopCount`/MSA 片段时序差异,但技能 grade 的权威来源、逐技能 MSA
|
||||
事件到 MSE 的挂点/颜色/混合、飞行/特殊攻击、服务端结果和完整中断清理仍保持 `PARTIAL`。
|
||||
|
||||
## Implementation fix round 2026-09-21T03:46Z — skill grade motion index
|
||||
|
||||
本轮先在 `project/skill_test.gd` 加入等级动作回归,修复前复现 1 项失败:技能表没有
|
||||
等级动作索引入口。对照 40250 `PythonSkill.cpp::GetSkillMotionIndex`、
|
||||
`SKILL_EFFECT_COUNT=4`、`SKILL_GRADEGAP=25` 以及当前资源
|
||||
`assets/root/constinfo.py` 的 `USE_SKILL_EFFECT_UPGRADE_ENABLE=1` 后完成修复:
|
||||
|
||||
- `SkillTable::motion_idx_for_grade` 按 `base + grade * 25` 计算 M/G/P 动作索引,
|
||||
超出该技能 `grade_count` 回退基础索引;新增 `motion_name_by_idx` / `motion_grade_by_idx`
|
||||
让远端收到 26/51/76 等等级动作索引时能恢复同一技能和对应 `_2/_3/_4.msa`。
|
||||
- `Quickbar` 普通技能快捷栏和技能窗口直施路径在 `cast_skill` 前读取当前 master grade,
|
||||
不再只发送基础 motion index;公会技能仍按 40250 `UseGuildSkill` 使用基础索引。
|
||||
- `NetWorld::_apply_anim` 从远端 FUNC_SKILL 的等级动作索引反解 grade,再传给
|
||||
`PlayerView.play_skill_motion`;循环次数和 moving-skill bit 保持原有编码。
|
||||
|
||||
修复后通过:`skill_test.gd`、`net_world_remote_action_test.gd`、
|
||||
`test_skill_state_arg_parity.gd`、`skill_motion_loop_test.gd`、`skill_fx_test.gd`、
|
||||
`combat_fx_test.gd`。本轮只闭合了等级动作索引及其本地/远端映射,不宣称逐技能 MSA
|
||||
事件、MSE 挂点/颜色/混合、飞行/特殊攻击、服务端结果和完整中断清理已完全等价,合同继续为
|
||||
`PARTIAL`。
|
||||
|
||||
## Implementation fix round 2026-09-21 — remote SPECIAL_ATTACKING consumer
|
||||
|
||||
本轮按 40250 `ActorInstanceMotionEvent.cpp::ProcessMotionEventSpecialAttacking`、
|
||||
`InstanceBaseBattle.cpp::AttackProcess` 和
|
||||
`ActorInstanceCollisionDetection.cpp::__SplashAttackProcess` 继续核对技能事件消费者。
|
||||
参考端的 `SPECIAL_ATTACKING` 不是只有本地主角才能创建的事件:每个可见
|
||||
`CActorInstance` 都会建立自己的 `m_kSplashArea`,随后由 `AttackProcess` 遍历目标,执行
|
||||
1000cm broad-phase、`ATTACK_TYPE_SNIPE` 目标限制、碰撞球检测、`HittedObjectSet` 去重和
|
||||
`iHitLimitCount` 上限,最后调用 `__ProcessDataAttackSuccess`;只有本地事件处理器才把
|
||||
`isEnableHitProcess` 转成 `CG_ATTACK`。
|
||||
|
||||
修复前 `GameScene._on_remote_motion_event()` 只处理远端特效/飞行/隐藏事件,远端 type 4
|
||||
事件没有消费者;`NetPlay` 只有本地主角的 `start_motion_splash()` 和
|
||||
`_splash_attack_process()`。现在:
|
||||
|
||||
- `NetWorld._apply_anim()` 保存当前远端 `FUNC_SKILL` 的 motion index,供事件恢复 `uSkill`;
|
||||
- `GameScene._on_remote_motion_event()` 将 type 4 送入 `NetPlay.start_remote_motion_splash()`;
|
||||
- `NetPlay._remote_splash_attack_process()` 使用与本地相同的 `MotionSplash` 几何、持续时间、
|
||||
去重、命中上限和攻击可达性判定;
|
||||
- 远端命中复用 `__ProcessDataAttackSuccess` 对应的硬直、无敌时间、受击动作、抖动和击退
|
||||
分支,但把 `InsertDelay` 施加在远端攻击者,且禁止远端命中伪造本地 `CG_ATTACK`。
|
||||
|
||||
新增 `remote_motion_splash_test.gd` 覆盖创建、命中反应、攻击者硬直、无上行攻击包、同 VID
|
||||
去重和 `DuringTime` 到期清理。该回归与 `net_world_remote_action_test.gd`、
|
||||
`net_world_motion_event_test.gd`、`motion_splash_test.gd`、`combat_fx_test.gd`、
|
||||
`skill_test.gd` 均通过。
|
||||
|
||||
本轮只闭合了远端 `SPECIAL_ATTACKING` 的本地消费者;真实服务端伤害/经验结果、完整技能
|
||||
MSA→MSE 挂点/颜色/混合、远端资源矩阵、所有中断/断线清理仍保持 `PARTIAL`。
|
||||
@@ -0,0 +1,150 @@
|
||||
# skill.player_ui_cooldown —— 技能栏、快捷键、等级、冷却和禁止条件
|
||||
|
||||
状态:`PARTIAL`
|
||||
|
||||
本合同覆盖 40250 Windows 客户端的技能数据解析、技能页加点、快捷栏持久化、技能施放前检查、目标/射程预约、冷却和服务端解锁事件。判断标准是调用顺序、禁止条件、数据来源、冷却公式和网络副作用一致,而不是只看当前端是否存在同名 `use_skill` 函数。
|
||||
|
||||
## 40250 参考调用链
|
||||
|
||||
### 技能数据和等级
|
||||
|
||||
- `UserInterface/PythonSkill.cpp`
|
||||
- `RegisterSkillDesc`/`RegisterSkillTable` 读取技能属性、武器限制、动作、等级、目标范围、目标数量、SP/冷却公式和 `.msk` 数据。
|
||||
- `SSkillData::IsCanUseSkill` 只允许非被动主动施放;`GetSkillCoolTime`、`GetTargetCount`、`GetNeedSP` 都以 `fcurEfficientPercentage` 代入公式。
|
||||
- `UserInterface/PythonPlayer.cpp`
|
||||
- `SetSkillLevel_` 将服务端等级/grade 转为当前 slot 的 grade、level 和 `LocaleService_GetSkillPower` 效率百分比。
|
||||
- 快捷栏是服务端 `QUICKSLOT_MAX_NUM=36`,本地显示 4×8;分页、slot/ref 映射、add/del/move 请求均以全局槽位为权威。
|
||||
- affect 的 toggle 技能会调用 `ActivateSkillSlot`/`DeactivateSkillSlot`,而不是由 UI 本地猜测。
|
||||
|
||||
### 施放前检查和副作用顺序
|
||||
|
||||
- `UserInterface/PythonPlayerSkill.cpp::ClickSkillSlot`
|
||||
- 槽位/技能数据 → 公会技能路由 → 被动静默返回 → standing/toggle-off → `__UseSkill`。
|
||||
- `__CanUseSkill`
|
||||
- 主角存在、非观战、坐骑术等级满足、`CInstanceBase::CanUseSkill` 通过。
|
||||
- `CInstanceBase::CanUseSkill`
|
||||
- 拒绝变身 `IsPoly`、穿礼服 `IsWearingDress`、持镐 `IsHoldingPickAxe`、坐骑不可用技能和当前图形动作不可施法。
|
||||
- `__CheckSkillUsable`
|
||||
- 骑乘/骑乘技、攻击技安全区、被动、空瓶/毒瓶、钓鱼、等级/未学习、武器类型、箭矢、冲刺 affect、冷却、HP/SP 按固定顺序检查。
|
||||
- `__UseSkill`
|
||||
- 私人商店静默返回 → `__CanUseSkill` → 特殊技能 → toggle-off → `__CheckSkillUsable` → 施法中检查 → 目标/尸体/结盟/安全区/可攻击/射程预约 → 改朝向 → 扇形/圆形追加目标 → 清预约 → `NEW_UseSkill` 动作 → `SendUseSkillPacket` → `__RunCoolTime`。
|
||||
- `__RunCoolTime`
|
||||
- 以 skill `.msk` 冷却公式计算基础时间,再按 `POINT_CASTING_SPEED` 修正,最后触发 UI `RunUseSkillEvent`。
|
||||
|
||||
### 网络回包
|
||||
|
||||
- `PythonNetworkStreamPhaseGame.cpp`
|
||||
- `RecvSkillLevel`/`RecvSkillLevelNew` 刷新技能和效率百分比。
|
||||
- `RecvSkillCoolTimeEnd` 调用 `EndSkillCoolTime`,按技能 ID 清除本地冷却。
|
||||
- `RecvChangeSkillGroupPacket` 清空旧 skill data 并刷新角色窗口。
|
||||
- `GC_QUICKSLOT_ADD/DEL/MOVE` 更新本地 36 槽数据。
|
||||
|
||||
## 当前端实现映射
|
||||
|
||||
- `project/ui/skill_table.gd`
|
||||
- 已解析 `skilldesc.txt`、属性/武器 bit、`.msk` 的冷却/SP/目标数量/范围公式,并实现技能分类和等级加点谓词。
|
||||
- `project/player_skill.gd`
|
||||
- 已按 `ClickSkillSlot → can_use_skill → check_skill_usable → use_skill` 分层校验,包含
|
||||
`CInstanceBase::CanUseSkill` 的变身、礼服、矿镐、坐骑动作和图形动作五类门,以及安全区、骑乘、被动、瓶子、钓鱼、等级、武器、箭矢、冷却、HP/SP、尸体、结盟、目标和射程预约。
|
||||
- 公会技能按 `GUILD_ROUTED` 转入独立 `use_guild_skill`,补齐公会战状态门和独立冷却入口。
|
||||
- `project/ui/quickbar.gd`
|
||||
- 维护 36 个服务端槽位、4×8 可见页、server quickslot add/del/swap、技能图标/等级/冷却遮罩、移动端快捷栏和 `GC_SKILL_COOLTIME_END` 解锁。
|
||||
- 通过 `M2Client.use_skill`、`use_guild_skill`、`cast_skill` 和 `NetPlay` 发起技能;冷却按 `.msk` 基础值和
|
||||
`POINT_CASTING_SPEED` 的 40250 分段公式本地预测。
|
||||
- `project/ui/skill_ui.gd`
|
||||
- 有主动/辅助/坐骑页、技能等级/grade、加点按钮、技能拖入快捷栏和服务端 skill/group 刷新。
|
||||
- `extension/src/net/entity_store.cpp`、`classic_parser.cpp`、`m2_client.cpp`
|
||||
- 已存储/转发 skill level、master、group、quickslot 和 cooldown-end 包,并暴露相应信号。
|
||||
|
||||
## 分支等价矩阵
|
||||
|
||||
| 参考判据/副作用 | 当前实现 | 结论 |
|
||||
|---|---|---|
|
||||
| 技能属性、武器限制、`.msk` 公式和等级效率 | `SkillTable` 读取 skilldesc/`.msk`,`_skill_point` 有 40250 等级表 | 基本等价;全部语言、所有 `.msk` 和公式异常仍未建立快照 |
|
||||
| `ClickSkillSlot` 分层顺序、被动/公会/特殊/开关技 | `PlayerSkill.click_skill_slot/use_skill/use_guild_skill` + `Quickbar._activate_guild_skill` | 基本等价;公会技已独立走 `CG_GUILD_USE_SKILL`,toggle active 状态主要由调用方传入,未证明与 affect slot 状态始终一致 |
|
||||
| `CInstanceBase::CanUseSkill` 变身、礼服、持镐、坐骑和图形动作门 | `can_use_skill` 按相同顺序拒绝五类动作状态,`NetPlay.skill_context` 从实体快照注入 poly/礼服/矿镐/坐骑/图形门 | 基本等价;礼服和图形层字段依赖服务器快照是否提供 |
|
||||
| 安全区、瓶子、钓鱼、等级、武器、箭矢、HP/SP、尸体和结盟 | `check_skill_usable`/`_resolve_target` | 基本等价;in_safe、持有物、HP/SP 和目标字段均依赖当前端快照,实时/乱序未证实 |
|
||||
| 冷却公式 `GetSkillCoolTime(fcurEfficientPercentage)` | `SkillTable.cooldown_of` | 公式主体基本等价 |
|
||||
| `__RunCoolTime` 的 `POINT_CASTING_SPEED` 修正 | `M2Client.get_points()` 暴露点 21,`Quickbar._skill_cooldown` 按 40250 分段公式换算 | 基本等价;真实服务端点变更与冷却回包乱序仍待 fixture |
|
||||
| 冷却开始时机:动作成功后 `SendUseSkillPacket` → `__RunCoolTime` | 当前先成功发送 `cast_skill`(`FUNC_SKILL`),再发送 `use_skill`(`CG_USE_SKILL`),最后写本地 `cd_end` | 基本等价;没有以服务端/动作成功回执确认本地预测是否应回滚 |
|
||||
| `GC_SKILL_COOLTIME_END` 按 skill ID 解锁 | native parser/store → `skill_cooldown_end` → Quickbar 清理 | 基本等价;重复、过期、断线和槽位重排时序未验证 |
|
||||
| 36 槽 server quickslot 与 4×8 可见页 | Quickbar 36 状态 + 32 可见槽,native 36 槽 | 基本等价;最后 4 个不可见槽的恢复、键盘输入和 UI 可达性未验证 |
|
||||
| 目标/射程预约、自动索敌、扇形/圆形追加目标 | `NetPlay`/`PlayerSkill`/`Quickbar` | 部分等价;`SetFlyTargetInstance`、动作朝向、资源目标数量和发送顺序尚无完整包序证据 |
|
||||
| skill/group/quickslot 回包刷新 UI | native signals → Quickbar/SkillUI | 基本等价;已有单元测试,但没有真实乱序、拒绝和重连场景 |
|
||||
|
||||
## 已执行测试
|
||||
|
||||
- `project/player_skill_test.gd`:PASS;覆盖三层技能合法性校验和 `CanUseSkill` 五类动作门。
|
||||
- `project/skill_test.gd`:PASS;覆盖技能表、技能页和快捷栏基础链。
|
||||
- `project/netplay_test.gd`:PASS;覆盖 `GUILD_WAR_NONE/ON_WAR/WAIT_START` 到公会技状态门的映射。
|
||||
- `project/test_skill_inventory_parity.gd`:45 项 PASS;覆盖技能门控、库存/装备、角色状态和 quickslot 同步。
|
||||
- `project/test_taskbar_quickslot_parity.gd`:34/34 PASS;覆盖任务栏、4×8 页面和鼠标技能模式;有 Godot ObjectDB/RID/resource cleanup 警告。
|
||||
- `project/test_status_skill_reset_parity.gd`:32/32 PASS;覆盖技能重置相关状态变化。
|
||||
- `project/mobile_gesture_test.gd`:PASS;覆盖移动端技能拖放、瞄准和快捷栏交换。
|
||||
- `project/playable_combat_test.gd`:0 failures;覆盖离线可玩技能请求、冷却、目标、资源和断线。
|
||||
- `project/guild_war_skill_test.gd`:PASS;覆盖公会技能页、公会战状态和 `use_guild_skill` UI 入口。
|
||||
- `build/extension/net_entity_test`、`build/extension/net_classic_session_test`:PASS;覆盖 native skill/group/quickslot/cooldown-end 协议存储和会话入口。
|
||||
- `project/test_full_skill_flow.gd`:PASS;共享 `net_client_fake.gd` fixture 可启动 GameScene,并断言 `FUNC_SKILL → CG_USE_SKILL` 动作/意图包序。
|
||||
|
||||
## 已确认差异和未验证项
|
||||
|
||||
1. `Quickbar` 在本地 `use_skill` 返回成功后立即预测冷却,缺少服务端拒绝/动作未起播/断线后的冷却回滚证据;40250 也做客户端预测,但其状态由 `CPythonPlayer` 的 slot 冷却对象统一管理。
|
||||
2. 当前 skill/group/quickslot parser 虽已存在,尚无 `GC_SKILL_LEVEL_NEW → group → quickslot → cooldown-end` 乱序、重复、跨角色和重连生命周期测试。
|
||||
3. 需要建立全部技能 ID/grade/语言/`.msk` 公式和动作资源索引,验证动作事件、目标追加、`SendUseSkillPacket`、冷却和服务端 damage 的顺序。
|
||||
|
||||
## 下一轮验证入口
|
||||
|
||||
- 验证 `POINT_CASTING_SPEED` 在真实 points 包、重复点变更和冷却结束回包中的时序,并补 native/GDScript 端到端证据。
|
||||
- 将 `CanUseSkill` 五类禁止状态接到实体真实 shape/动作字段,并验证拒绝时不发 `CG_USE_SKILL`、`CG_MOVE(FUNC_SKILL)` 或 fly-target 包。
|
||||
- 用真实 `GUILD_WAR_ON_WAR` 回包验证公会技 `ONLY_FOR_GUILD_WAR` 门、`CG_GUILD_USE_SKILL` 和冷却结束包序。
|
||||
- 增加 skill level/group/quickslot/cooldown-end 的乱序、重复、断线、重连和换图测试,再决定是否可提升合同状态。
|
||||
|
||||
## Active UserInterface skill-data bridge review
|
||||
|
||||
- `PythonSkill.h` 的 `TSkillData`/技能描述字段包括 target range、attribute、need weapon、状态上下限、affect description、cooldown、SP、continuation SP、motion loop count、target count、duration、motion index、grade icon 和职业/等级限制;`RegisterSkill/Table/Desc` 是资源权威入口。
|
||||
- 当前端有 `SkillTable`/quickbar/cooldown 基础,已补 casting speed、动作门和
|
||||
`FUNC_SKILL → CG_USE_SKILL` 包序;loop count、grade/motion、技能描述公式和服务端拒绝后的冷却回滚尚未由同一数据链驱动,合同继续 `PARTIAL`。
|
||||
|
||||
## Implementation round 2026-09-21T14:45Z
|
||||
|
||||
本轮按 40250 `CInstanceBase::CanUseSkill()` 和 `CPythonPlayer::__RunCoolTime()` 修复两处实现差异:
|
||||
|
||||
- `PlayerSkill.can_use_skill()` 新增并按参考顺序执行 `POLYMORPHED`、`WEARING_DRESS`、
|
||||
`HOLDING_PICKAXE`、`HORSE_CANNOT_USE`、`GRAPHIC_CANNOT_USE` 五类动作门;
|
||||
`NetPlay.skill_context()` 从实体 `poly/shape/wedding_dress/parts[weapon]` 及服务器动作字段注入这些状态。
|
||||
- native `POINT_CASTING_SPEED=21` 加入协议点位定义,`M2Client.get_points()` 暴露 `casting_speed`;
|
||||
`Quickbar._skill_cooldown()` 按 `iSpd = 100 - casting_speed` 的两段公式计算本地冷却遮罩和阻止时长,
|
||||
默认中性值为 100,与 40250 `char.cpp` 的初始化一致。
|
||||
- `player_skill_test` 新增五类 gate 断言,`skill_test` 新增 `casting_speed=20 → 180%` 数值断言;
|
||||
`skill_test`、`player_skill_test`、`test_skill_inventory_parity`、`playable_combat_test`、
|
||||
`netplay_test` 和 native 目标构建通过。
|
||||
|
||||
仍保持 `PARTIAL`:服务端拒绝/动作未起播后的冷却回滚、skill/group/quickslot/cooldown-end 乱序与跨生命周期、
|
||||
完整动作资源和 damage 包序尚未闭合。
|
||||
|
||||
## Implementation round 2026-09-20T20:45Z
|
||||
|
||||
本轮按 40250 `CPythonPlayer::__UseSkill` 的真实副作用顺序修复技能发送链:
|
||||
|
||||
- `Quickbar._activate_slot()` 和 `activate_skill_direct()` 现在先调用 `cast_skill()` 上行
|
||||
`FUNC_SKILL|motion + arg`,动作成功后再调用 `use_skill()` 上行 `CG_USE_SKILL(skill, target)`;
|
||||
本地 `skill_cast_started` 也在动作状态成功后触发,扇形/圆形追加目标仍在主技能意图之后发送。
|
||||
- 补充共享 `project/net_client_fake.gd`,替换 `test_full_skill_flow.gd` 的缺失夹具;该测试现在实际装配
|
||||
`GameScene`、执行一次技能,并断言动作包先于技能意图包。
|
||||
- `playable_combat_test`、`skill_test`、`mobile_gesture_test`、`player_skill_test` 和
|
||||
`test_full_skill_flow` 均通过。
|
||||
|
||||
仍保持 `PARTIAL`:服务端拒绝/动作未起播后的冷却回滚、skill/group/quickslot/cooldown-end
|
||||
乱序与跨生命周期、完整动作资源和服务端 damage 包序仍需真实网络证据。
|
||||
|
||||
## Implementation round 2026-09-20T20:52Z
|
||||
|
||||
本轮按 40250 `ClickSkillSlot → UseGuildSkill` 分支修复公会技能:
|
||||
|
||||
- `PlayerSkill` 保留 `GUILD_ROUTED` 的入口分流,并新增 `use_guild_skill()`,执行主角动作门、技能数据、冷却和 `ONLY_FOR_GUILD_WAR` 检查。
|
||||
- `NetPlay.skill_context()` 将 native `GUILD_WAR_ON_WAR=6` 映射为 `guild_war_active`;其它公会战通知状态不会误放行公会战限定技能。
|
||||
- `Quickbar` 新增 `_activate_guild_skill()`:先上行 `FUNC_SKILL`(loop=1),再调用
|
||||
`M2Client.use_guild_skill(skill, 0)`,并复用技能冷却/动作信号;普通技能仍走 `CG_USE_SKILL`。
|
||||
- `player_skill_test`、`skill_test`、`guild_war_skill_test` 均通过,覆盖非公会战拒绝、公会战独立包和 UI 公会技能页。
|
||||
|
||||
仍保持 `PARTIAL`:真实 `GUILD_WAR_ON_WAR` 回包、服务端拒绝/冷却回滚、跨生命周期乱序和完整动作/damage 证据尚未闭合。
|
||||
@@ -0,0 +1,129 @@
|
||||
# social.chat_text_tail
|
||||
|
||||
## Scope
|
||||
|
||||
审计公聊、队伍/公会/喊话频道、无 VID 系统消息、私聊收发和私聊窗口、忽略名单、聊天气泡、名字/公会/称号/等级尾标以及尾标的寿命、排布和死亡清理。对比重点是当前端是否复现 40250 的消息类型分支、服务器事件来源、私聊状态机、文本颜色/过滤/表情副作用、尾标覆盖规则和渲染布局;本地有同名 helper 或能显示文本不等于实现统一。
|
||||
|
||||
## Reference call chain
|
||||
|
||||
- `UserInterface/PythonNetworkStreamPhaseGame.cpp`
|
||||
- `RecvChatPacket` 校验聊天类型;`CHAT_TYPE_COMMAND` 进入 `ServerCommand`,不进入普通聊天和聊天尾标。
|
||||
- 有 VID 时,普通/队伍/公会/喊话/客户端私聊从 `"名字: "` 后取正文,执行表情解析、跨帝国文字转换和辱骂过滤;角色不存在时直接忽略;除 `CHAT_TYPE_SHOUT` 外注册 `CPythonTextTail::RegisterChatTail`,PC 消息再进入 `CPythonChat::AppendChat`。
|
||||
- 无 VID 时,NOTICE/BIG_NOTICE 分别进入 `BINARY_SetTipMessage`/`BINARY_SetBigMessage`,系统和喊话按专门分支处理,不是简单追加到同一聊天列表。
|
||||
- `RecvWhisperPacket` 将 mode、发送者和文本交给 `OnRecvWhisper`/错误/系统回调;私聊历史、按钮、闪烁和忽略名单由 `CPythonChat`、`uiwhisper.py` 和 `game.py` 联动。
|
||||
- `UserInterface/PythonNetworkStreamCommand.cpp`
|
||||
- `ServerCommand` 作为 `CHAT_TYPE_COMMAND` 的二次命令总线,解析 cube、observer、StoneDetect、stamina、mobile、gift、情感动作和 `dig_motion`,并把副作用交给 UI、角色或效果管理器;命令不进入普通聊天尾标。
|
||||
- `UserInterface/PythonChat.cpp` / `PythonChat.h`
|
||||
- 普通聊天维护共享聊天行队列和多个 chat set,支持 view/edit/log 三种布局、滚动步长、最大行数、延迟追加、按频道颜色和清理。
|
||||
- 40250 颜色是 talking 白、info 淡红、notice/big notice 淡黄、party 青白、guild 淡紫、command/shout 青绿、whisper 绿色;私聊 mode 包含 chat、not-exist、target-blocked、sender-blocked、error、GM 和 system。
|
||||
- `AppendWhisper` 按目标名维护独立会话;`IgnoreCharacter`/`IsIgnoreCharacter` 只对目标名切换黑名单;`InitWhisper`/`MakeWhisperButton` 恢复已有会话。
|
||||
- `UserInterface/PythonTextTail.cpp` / `InstanceBaseEffect.cpp`
|
||||
- `RegisterChatTail` 和 `RegisterInfoTail` 共用每 VID 一条尾标;聊天尾标为白色、`bNameFlag=true`,信息尾标为淡红、`bNameFlag=false`,新消息覆盖旧消息并重置 `now + 5000ms`。
|
||||
- 聊天气泡会强制显示角色名字,并把名字尾标移动到气泡下方 17px;公会名、称号、等级按实际字体度量排在名字两侧/上下;角色死亡时 `DetachTextTail`。
|
||||
- 公会名只在 guild id 非零时创建,名称查询失败回退 `Noname`;称号/等级受 `EnablePKTitle`、alignment grade、level 和挂马高度影响。
|
||||
- `Client/Eternexus/root/game.py` / `uiwhisper.py`
|
||||
- `OnRecvWhisper` 按官方 whisper mode 分流到 `AppendWhisper`、系统错误文本和界面 `RecvWhisper` 通知;聊天输入、好友/目标板和独立私聊窗口都能打开同一目标会话。
|
||||
- 私聊窗口发送只有网络请求成功后才应关闭/追加本地回显;忽略按钮通过 `/ignore <name>` 与服务端/客户端状态同步。
|
||||
|
||||
## Current call chain
|
||||
|
||||
- `EntityStore::mut_chat`、`mut_whisper` 和 parser 已保存 `GC_CHAT`/`GC_WHISPER` 为 chat queue;`M2Client` 发出 `chat`、`whisper_received` 信号,基础 classic 动态包布局和长度限制存在;`GC_WHISPER` 缺少固定 type/name 字段时会拒绝,不再静默吞包。
|
||||
- `project/ui/chat_ui.gd` 只有四个本地 tab(全部/私聊/系统/战斗),按 Godot `RichTextLabel` 重建整段文本;公共输入支持普通、公会、队伍、喊话和 `/w`,但普通/喊话发送失败没有统一错误回显,私聊窗口也无视 `client.whisper()` 返回值而立即本地回显并关闭。
|
||||
- 当前入站私聊 `_on_whisper` 已按 40250 区分 `0=CHAT`、`1=NOT_EXIST`、`2=TARGET_BLOCKED`、`3=SENDER_BLOCKED`、`4=ERROR`、`5=GM`、`0xFF=SYSTEM`;1..4 的空文本会生成本地化错误兜底文案。live 会话已接入,颜色/字体和服务端错误正文仍未完整闭环。
|
||||
- `project/whisper_chat_system.gd` 实现了独立的本地 conversation/最小化/闪烁/忽略 helper,`ChatUI.setup` 现在把它接入 `M2Client.whisper_received`;live 私聊仍没有完整的独立窗口历史渲染、任务栏按钮、ignore 命令和发送结果状态。
|
||||
- `project/chat_tail.gd` 已复现单 VID 覆盖、5000ms 生命周期、聊天/信息颜色和名字跟随规则;`project/net_world.gd` 用 `Label3D` 气泡、名字和子标签实现,死亡/过期会清理节点。
|
||||
- `project/text_tail.gd`、`text_tail_arrange.gd`、`name_tail_layout.gd` 已实现部分颜色、称号、等级、公会名、挂马高度和像素近似排布;但运行时仍是 Godot billboard/估算字符宽度,不是 40250 的 `CGraphicText` 实际字体度量和 `CPythonTextTail` 渲染对象。
|
||||
- `ClassicParser::on_gc(GC_CHAT)` 现按 40250 先拒绝非法 chat type,并在非系统消息的 VID 不存在于 `EntityStore` 时消费但不产生 chat event;live `project/net_world.gd::_on_chat` 仍没有复现表情解析、跨帝国转换、辱骂过滤和无 VID NOTICE/BIG_NOTICE 专用 UI。
|
||||
|
||||
## Branch matrix
|
||||
|
||||
| 场景 | 40250 | 当前端 | 结论 |
|
||||
|---|---|---|---|
|
||||
| 普通/队伍/公会聊天 | 解析类型、角色和前缀,过滤/表情后写聊天行,必要时注册气泡 | 基础 `GC_CHAT` queue、UI tab 和气泡存在 | PARTIAL:过滤/表情/未知实体/真实显示布局缺失 |
|
||||
| 喊话 | 公共聊天颜色和公共区显示,不能注册角色聊天尾标 | 当前写入公共 tab;`ChatTail` 也能保留,但 net_world 连接路径未证明按 ref 排除 | PARTIAL |
|
||||
| 无 VID INFO/NOTICE/BIG_NOTICE | 专用 tip/big notice 分支,按类型显示 | 统一塞入 System/All `RichTextLabel`,没有专用 tip/big notice | GAP |
|
||||
| COMMAND | 进入 `ServerCommand`,不显示普通聊天/气泡 | native 有部分 command 解析;非已识别 command 仍缺完整 side effect 矩阵 | PARTIAL |
|
||||
| 表情/帝国文字/辱骂过滤 | 先解析表情或转换/过滤,再显示文本/动画 | 有发送表情 token,但没有入站 `ParseEmoticon`、帝国转换和辱骂过滤链 | GAP |
|
||||
| 私聊正常收发 | mode/name/text 进入独立目标会话,按钮/历史/通知联动 | packet queue、独立输入和 live conversation 接入存在;窗口历史/按钮/通知仍不完整 | PARTIAL |
|
||||
| 私聊错误/GM/system mode | 0..5/0xFF 具有不同错误/字体/颜色和通知 | mode 数值与错误/GM/system 分支已对齐;颜色、字体和 live 通知仍不等价 | PARTIAL |
|
||||
| 忽略目标 | `IgnoreCharacter` 按名称切换,收到私聊不进入会话并有服务端 ignore 命令 | live ChatUI 收到普通/GM 私聊前经过会话 ignore 判断;仍无 UI 忽略按钮/服务端命令连接 | PARTIAL |
|
||||
| 聊天气泡 | 每 VID 单槽覆盖,白/淡红,5000ms,聊天气泡强显名字并上移名字 17px | 单槽、颜色、生命周期和本地布局 helper 存在 | PARTIAL:实际字体/屏幕投影/已知实体和资源表现不同 |
|
||||
| 名字/称号/等级/公会名 | 由角色同步、alignment/guild/item/挂马更新,死亡 detach,真实字体度量排布 | 快照+Label3D 子标签和近似字符宽度存在 | PARTIAL:权威更新源和渲染度量不完全同链 |
|
||||
| 换图/重连/死亡/重复消息 | CPythonChat/TextTail 清理与重新注册,旧会话状态按客户端生命周期处理 | 本地节点过期/死亡清理存在;chat queue、私聊会话和重连恢复未端到端验证 | PARTIAL |
|
||||
|
||||
## Implementation-equivalence matrix
|
||||
|
||||
| 项目 | 状态 | 证据/限制 |
|
||||
|---|---|---|
|
||||
| Preconditions | PARTIAL | native 长度/阶段边界存在;缺入站 actor/type/name、ignore、输入门控和真实 UI 状态前置条件 |
|
||||
| Branch structure | PARTIAL | 普通/私聊/尾标主干和 live ignore 分支存在,但无 VID notice、表情/过滤和完整私聊通知分支 |
|
||||
| Algorithms/formulas | PARTIAL | 5000ms 生命周期和 17px 名字偏移有静态镜像;频道颜色、前缀、字符宽度、会话/通知算法存在明确差异 |
|
||||
| State transition order | GAP | 参考是网络回包→过滤/表情/尾标→聊天/窗口/提示;当前 conversation 已随 live signal 更新,但本地回显/窗口通知和服务端结果顺序仍不同 |
|
||||
| Constants/units | PARTIAL | EChatType 和尾标 5000ms/17px/颜色部分存在;多个公共/私聊颜色与 40250 不同,字体宽度是 7px 估算 |
|
||||
| Timing/event sources | PARTIAL | 气泡过期和 UI 信号存在;延迟聊天、真实服务器命令、重连/后台通知和私聊闪烁未接通 |
|
||||
| Resource/data sources | GAP | 40250 使用角色/公会/本地化/字体资源及服务器消息;当前标题本地化可注入但 chat 文本/颜色/宽度多为脚本常量和 Godot 默认字体 |
|
||||
| Protocol side effects | PARTIAL | classic CG_CHAT/CG_WHISPER 与 GC queue 有;入站类型副作用、SendSequence/错误文案、ignore 命令和 notice/big notice UI 未完整复现 |
|
||||
| Interruption/failure/cleanup | GAP | 私聊发送失败仍本地显示并关闭;会话/忽略/聊天行重连清理、未知 VID、重复/乱序回包和专用提示清理缺端到端证据 |
|
||||
|
||||
## Regression evidence
|
||||
|
||||
- `project/chat_test.gd`:公共/私聊输入路由、拾取/钓鱼消息、气泡和名字/子标签排布通过;使用 fake client,未验证真实 `GC_CHAT` 分支和发送失败。
|
||||
- `project/chat_tail_test.gd`:`RegisterChatTail`/`RegisterInfoTail`、单 VID 覆盖和 5000ms 生命周期通过。
|
||||
- `project/text_tail_test.gd`、`project/text_tail_arrange_test.gd`、`project/name_tail_layout_test.gd`、`project/guild_mark_tail_test.gd`:静态颜色、称号/等级、公会名、排列和 guild mark 通过;未验证真实字体资源和 C++ 尾标对象。
|
||||
- `project/test_whisper_chat_parity.gd`:26/26 通过,覆盖会话、闪烁、忽略和历史;`project/chat_test.gd` 另验证 live ChatUI 的普通/GM 入站信号进入同一 conversation。
|
||||
- `project/chat_mobile_ui_test.gd`、`project/test_chat_hyperlink_dice_parity.gd`:移动聊天窗口、私聊输入和额外链接/dice helper 通过;不能证明 40250 的入站消息分支。
|
||||
- `build/extension/net_entity_test`、`build/extension/net_classic_session_test`:GC_CHAT/GC_WHISPER 解析和发送包基础通过;session 回归覆盖未知 VID、非法 type、已知 VID 的 chat event 矩阵;`project/chat_test.gd` 另覆盖 whisper mode 1/5/0xFF 的错误、GM、system 显示映射,但仍没有完整 0..5/0xFF live 回调、失败回显和重连测试。
|
||||
|
||||
## Deep audit round 2026-09-21
|
||||
|
||||
### Fresh regression results
|
||||
|
||||
- `godot --headless --path project --script chat_test.gd`、`chat_tail_test.gd`、`text_tail_test.gd`、`text_tail_arrange_test.gd`:均退出码 0;公共/私聊路由、气泡覆盖、5000ms 生命周期、颜色和尾标排布通过,但使用的是脚本/fake client 与近似字体度量。
|
||||
- `godot --headless --path project --script name_tail_layout_test.gd`、`guild_mark_tail_test.gd`:均退出码 0;名字/公会标记和尾标布局 helper 通过。
|
||||
- `godot --headless --path project --script test_whisper_chat_parity.gd`:退出码 0,26/26 通过;覆盖独立的 `WhisperChatSystem` 会话、闪烁、忽略和历史;`chat_test.gd` 已补 live `ChatUI` 入站网络状态验证。
|
||||
- `godot --headless --path project --script chat_mobile_ui_test.gd`、`test_chat_hyperlink_dice_parity.gd`:均退出码 0;移动 UI、链接/骰子 helper 通过。
|
||||
- `build/extension/net_entity_test` 与 `build/extension/net_classic_session_test`:均退出码 0;GC_CHAT/GC_WHISPER 基础解析和发送包通过,未覆盖完整消息分支矩阵。
|
||||
|
||||
### Static parity findings
|
||||
|
||||
- 40250 `RecvWhisperPacket` 的 mode 是 `0=CHAT`、`1=NOT_EXIST`、`2=TARGET_BLOCKED`、`3=SENDER_BLOCKED`、`4=ERROR`、`5=GM`、`0xFF=SYSTEM`;当前 `ChatUI::_on_whisper` 把 `1` 显示为系统、`2` 显示为 GM,且其余错误/屏蔽分支没有对应 UI 语义。
|
||||
- 当前 `ChatUI` 在 `client.whisper()` 返回前就本地追加回显并关闭私聊窗口;网络发送失败时仍显示成功,违背 40250 的请求结果驱动状态转换。
|
||||
- `WhisperChatSystem` 已有独立会话/忽略/闪烁实现,live `ChatUI` 现在把普通/GM `M2Client.whisper_received` 接入该状态源;独立窗口渲染、任务栏按钮和服务端 ignore 命令仍未闭环。
|
||||
- 40250 `RecvChatPacket` 对未知 VID 的普通消息直接忽略,并处理表情、跨帝国转换、辱骂过滤以及无 VID 的 NOTICE/BIG_NOTICE 专用提示;当前 `_on_chat` 对未知 VID 仍能显示原文,NOTICE/BIG_NOTICE 统一进入 RichTextLabel,未接专用提示和过滤链。
|
||||
- 40250 聊天气泡/尾标依赖 `CGraphicText` 实际字体、角色/公会/称号/等级权威同步并在死亡时 detach;当前 Label3D/脚本近似布局虽有局部测试,但没有真实字体资源、网络更新和重连/死亡/重复消息端到端证据。
|
||||
|
||||
### Active command-bus source review
|
||||
|
||||
- 本轮逐项核对 `PythonNetworkStreamCommand.cpp` 与当前 `EntityStore::apply_server_command`→`M2Client::poll` 信号链:当前端已经覆盖 `cube`、observer、StoneDetect、stamina、mobile、gift、combo、block mode、lover、password/refine/private-shop 以及 emotion/dig 的主要事件枚举,`net_entity_test` 对其中一组事件通过。
|
||||
- 仍存在参考语义差异:40250 未识别/参数数量错误命令会走日志或特定 UI 回调;当前端对命令的统一 token/参数校验、效果资源创建、所有 emotion/observer/gift 回调和命令失败清理没有完整矩阵。`ClientCommand()` 在参考版本本身返回 false,不能把当前端已有 server-command helper 测试解释为完整 GM/命令 parity。
|
||||
|
||||
### Round conclusion
|
||||
|
||||
本轮仍为 `PARTIAL`:已按 40250 收口 native `GC_CHAT` 的非法类型/未知 VID 丢弃分支,修正 ChatUI 的 whisper mode 映射与空错误文案兜底,并把 live 普通/GM whisper 接入独立会话和 ignore 判断;窗口通知、NOTICE 分支、表情/过滤/跨帝国文字和真实尾标渲染仍未与 40250 统一。
|
||||
|
||||
### Active EterLib text-render review
|
||||
|
||||
- `EterLib/GrpText.cpp`/`GrpTextInstance.cpp`/`TextBar.cpp`/`TextTag.cpp` 以及 `GrpFontTexture.cpp`、`GrpMarkInstance.cpp` 共同负责真实字体资源、字宽/行数/字符位置、光标/描边/颜色、标记和池化对象生命周期;这正是 40250 聊天气泡、名字/公会/称号尾标的渲染基础。
|
||||
- 当前 `Label3D`、`RichTextLabel` 和脚本宽度估算已覆盖局部可见结果,但没有复用同等字体纹理度量、字符定位、mark 生命周期和资源失败分支。已有布局测试使用 helper/fake client,不能替代真实字体资源与网络角色更新。
|
||||
- 因此本轮完成上述 EterLib 文本核心静态核对;尾标的单槽/5000ms/17px 行为仍有镜像证据,但渲染资源和清理链不一致,合同继续为 `PARTIAL`。
|
||||
|
||||
## Active UserInterface chat and text-tail review
|
||||
|
||||
- `InsultChecker.cpp/h` 维护可清空/追加的禁词集合,按 locale-aware case-insensitive 比较处理多字节字符;`FilterInsult` 用 `*` 替换完整词,`IsInsultIn` 只报告命中,不能用 ASCII `find` 近似替代。
|
||||
- `PythonChatModule.cpp` 暴露 chat set 的创建、位置/高度/步长/模式、延迟消息、忽略角色、whisper 会话、whisper 清理和 item hyperlink 解析;hyperlink 通过 `CItemManager` 查 item 数据,不是只显示原字符串。
|
||||
- `PythonTextTailModule.cpp` 的生产顺序是 `UpdateAllTextTail`、`UpdateShowingTextTail`、`RegisterCharacterTextTail`、`RegisterChatTail`/`RegisterInfoTail`、标题/拾取/排列和投影位置查询;`Pick`、隐藏/显示和角色删除清理是同一状态机的一部分。
|
||||
- 当前端聊天/尾标 helper 已有局部测试,live ChatUI 已将普通/GM whisper 接入独立会话和 ignore 判断;仍未形成 locale 禁词、多字节边界、chat-set 排列、item hyperlink、未知/删除 VID、投影失败和真实字体资源的同一调用链,已有 Godot 宽度估算和本地文本状态不能证明 40250 等价,合同保持 `PARTIAL`。
|
||||
|
||||
## Implementation fix round 2026-09-22
|
||||
|
||||
- `project/ui/chat_ui.gd` 已按 `PythonChat.h`/`game.py` 修正 whisper mode:`1..4` 作为错误系统消息,`5` 作为 GM,`0xFF` 作为 system;空错误正文使用本地化兜底文案。
|
||||
- `extension/src/net/classic/classic_parser.cpp` 已按 `RecvWhisperPacket` 的固定头前置条件,将缺少 type/name 的 `GC_WHISPER` body 从静默成功改为失败;`net_classic_session_test` 增加对应直接 parser 回归。
|
||||
- `project/chat_test.gd`、`project/test_whisper_chat_parity.gd`、`project/chat_mobile_ui_test.gd`、`project/test_chat_hyperlink_dice_parity.gd` 与 native session 回归通过;live conversation/ignore 接入已有证据,完整通知/颜色、忽略命令和公共聊天副作用仍未闭环。
|
||||
|
||||
## Static review evidence
|
||||
|
||||
已将 `InsultChecker.cpp/h`、`PythonChatModule.cpp`、`PythonTextTailModule.cpp` 纳入 active source 证据;这些文件对应的过滤、chat-set、whisper、hyperlink、尾标更新和拾取分支仍记录为实现差异或未验证分支。
|
||||
|
||||
## Status
|
||||
|
||||
`PARTIAL`。当前端已有聊天协议骨架、公共输入、私聊基础发送、聊天气泡和名字尾标的可测试镜像,但并非与 40250 统一实现:live 私聊已接入独立会话,但窗口历史/任务栏通知/服务端 ignore 命令仍未闭环;公共颜色/notice 分支不一致、入站表情/过滤/跨帝国文本缺失,且尾标渲染仍使用 Godot billboard 与宽度估算。本轮已修正 whisper mode 映射、空错误文案、截断 whisper 拒绝逻辑并接通 live conversation/ignore 判断,其他差异继续保持 PARTIAL。
|
||||
@@ -0,0 +1,99 @@
|
||||
# social.exchange_safebox
|
||||
|
||||
## Scope
|
||||
|
||||
审计玩家面对面交易、交易确认/防调包、仓库(Safebox)、仓库密码与分页、商城(Mall)只读取出、金币变化和窗口距离/关闭清理。判断标准是当前端是否复现 40250 的客户端请求门控、服务器回包权威状态、物品 socket/属性传递、错误分支和窗口时序;本地脚本能够独立完成一次交易或仓库存取,不能作为与 40250 实现统一的证明。
|
||||
|
||||
## Reference call chain
|
||||
|
||||
- `UserInterface/PythonNetworkStreamPhaseGame.cpp`
|
||||
- `RecvExchangePacket` 按 `START/ITEM_ADD/ITEM_DEL/ELK_ADD/ACCEPT/END` 更新 `CPythonExchange`,每个物品同时保存 socket 与 attribute,并在物品变化后刷新交易和背包窗口。
|
||||
- `SendExchangeStartPacket`、`SendExchangeElkAddPacket`、`SendExchangeItemAddPacket`、`SendExchangeAcceptPacket`、`SendExchangeExitPacket` 均先经过 `__CanActMainInstance()`,再发送 `CG_EXCHANGE` 并追加 sequence;客户端不直接结算交易。
|
||||
- `UserInterface/PythonExchange.cpp` / `Client/Eternexus/root/uiexchange.py`
|
||||
- `CPythonExchange` 保存双方姓名、12 个展示槽、socket/attribute、金币和接受状态;交易结束由 `GC_END` 驱动 `EndExchange`,成功/失败详情通过服务器聊天提示提供。
|
||||
- 交易 UI 只提交物品、金币、接受或退出请求,等待服务器回包刷新;物品源位置、anti flag、背包和金币结果由服务端确认。
|
||||
- `UserInterface/PythonNetworkStreamPhaseGameItem.cpp`
|
||||
- Safebox checkin/checkout/move、密码错误、大小、金币变化和 Mall open/set/del/checkout 都是协议路径;Safebox/Mall item set 回包复制 vnum/count/flags/anti_flags、socket 和 attribute 后刷新窗口。
|
||||
- Safebox 金币主动修改包在该版本明确未启用;Mall checkout 还触发对应掉落音效。
|
||||
- `Client/Eternexus/root/uisafebox.py` / `game.py`
|
||||
- 仓库由服务器返回页数和物品状态,5×9 每页 45 格;拖放/右键发送 checkin、checkout 或 move,关闭和密码命令走服务器流程。商城只允许取出,不允许向 Mall 存入。
|
||||
|
||||
## Current call chain
|
||||
|
||||
- native `M2Client`/`ClassicSession` 已提供 exchange start/item/gold/accept/cancel、safebox checkin/checkout/move/password/change/close、mall checkout/password,以及 `EntityStore` 的 exchange/safebox/mall parser、dirty 标记和 signal。
|
||||
- `EntityStore` 能保存交易双方 12 槽、金币、接受标志、notice,能够复制交易物品 socket/attribute;Safebox/Mall parser 能复制 item flags、anti_flags、socket 和 attribute,并处理 open/set/del/close/wrong-password 基础事件。
|
||||
- `project/ui/exchange_ui.gd`、`safebox_ui.gd`、`mall_ui.gd` 已提供 12 槽交易 UI、45 格分页仓库、商城只读出、tooltip、密码和距离自动关闭;主要 UI 已从 native getter/signal 刷新,而不是只依赖本地物品列表。
|
||||
- `project/player_trade_exchange_system.gd` 和 `project/safebox_storage_keeper_system.gd` 是另外一套本地状态机:它们直接保存玩家字典、交易物品、金币、仓库密码、库存和金条熔铸结果。这些 helper 可用于单元测试,但不是 40250 客户端的网络 authority 链。
|
||||
|
||||
## Branch matrix
|
||||
|
||||
| 场景 | 40250 | 当前端 | 结论 |
|
||||
|---|---|---|---|
|
||||
| 开始交易 | `__CanActMainInstance`→`CG_EXCHANGE START`→服务器 `GC_START` | native 能发 start,UI/helper 能打开交易 | PARTIAL:native 缺同等动作门控和 active/partner 状态限制;helper 可绕开服务器 |
|
||||
| 放入物品 | 客户端只发源位置和展示槽,服务器回 `GC_ITEM_ADD`;变化时双方接受状态由服务器清除 | UI 做 anti-give/重复源位置检查后立即记录 `_offered_cells`,native 只做位置边界 | PARTIAL:本地 optimistic 集合没有按 `GC_ITEM_ADD/ITEM_DEL` 重建,拒绝或删除后可能仍阻塞源物品 |
|
||||
| 放入金币 | `SendExchangeElkAddPacket` 只负责请求,服务器决定金额并回包 | UI 限制 1..9,999,999,native 只做 u32/in-game 检查 | PARTIAL:本地上限和已放金币判断不是 40250 客户端 authority;native 缺交易 active/拥有金币/动作门控 |
|
||||
| 接受/取消/结束 | 接受和退出发包,窗口等 `GC_END` 关闭;交易结果由服务器聊天和回包提供 | 接受有本地 pending;取消和距离超限会发包后立即 `close()` | PARTIAL:取消/距离分支不等待权威 `GC_END`,真实拒绝、断线和乱序关闭未覆盖 |
|
||||
| 交易安全/原子结算 | 客户端不计算交换、不改双方背包/金币;服务端负责锁定、空间、金额和最终交付 | `player_trade_exchange_system.gd` 本地计算 ready、金币、空间和原子转移 | GAP(本地 helper):这不是 40250 客户端实现,不能作为 live parity 依据 |
|
||||
| Safebox 打开/关闭 | `GC_SAFEBOX_SIZE` 打开,服务端控制大小;关闭走 `/safebox_close`,窗口状态由服务端同步 | native/UI 有 open/close、页数和 10m 自动关闭;close 后本地立即隐藏 | PARTIAL:基础链存在,但本地先隐藏/请求失败回滚、乱序重开和真实服务端错误缺测试 |
|
||||
| Safebox 存取/移动 | checkin/checkout/move 请求后由 `GC_SAFEBOX_SET/DEL` 更新 | 拖放使用目标槽,移动/右键取出存在;移动端 deposit 先找本地首个空位并记录 pending | PARTIAL:pending 没有与回包关联;本地 helper 直接存取/堆叠,不能证明服务器拒绝、堆叠和物品属性结果一致 |
|
||||
| Safebox 密码/金币 | wrong-password 和 size/money change 由服务器回包;客户端不保存真实密码 | native 通过命令发送密码;本地 `safebox_storage_keeper_system.gd` 保存密码、冷却、锁定和金条规则 | PARTIAL/GAP:helper 的密码/冷却/金条熔铸是自定义本地 authority,不能替代 40250 server command/GC 结果 |
|
||||
| Mall 打开/取出 | `GC_MALL_OPEN/SET/DEL` 驱动只读窗口,checkout 后服务器更新背包/Mall | native parser/UI/getter/checkout 存在,UI 禁止插入 | PARTIAL:主路径接近,但服务端失败、背包回包、重连清理和掉落音效没有端到端证据 |
|
||||
|
||||
## Implementation-equivalence matrix
|
||||
|
||||
| 项目 | 状态 | 证据/限制 |
|
||||
|---|---|---|
|
||||
| Preconditions | PARTIAL | 位置、展示槽、u32、窗口和部分 anti flag 检查存在;exchange native 缺 `__CanActMainInstance` 等阶段/动作门控,真实库存/交易 active/拥有金币由服务端决定且未在客户端等价复现 |
|
||||
| Branch structure | PARTIAL | exchange/safebox/mall 的协议分支和 UI 分支基本存在;取消立即关闭、移动端首空位 deposit、local helper 结算和密码/金条旁路与参考客户端不一致 |
|
||||
| Algorithms/formulas | GAP | 40250 客户端不实现交易结算、库存空间、仓库密码存储或金条熔铸公式;当前本地 helper 复制这些服务器职责,不能宣称与客户端统一 |
|
||||
| State transition order | PARTIAL | native store 大体是请求→GC→dirty/signal→UI;交易 `_offered_cells`、safebox pending 和 close 先隐藏造成本地 optimistic 状态,拒绝/乱序时序尚未闭合 |
|
||||
| Constants/units | PARTIAL | 12 交易槽、45 格/页、socket/attribute 槽和距离限制存在;金币 9,999,999 UI 上限、helper 45 格/单页和自定义冷却/金条常量需避免冒充 40250 client 常量 |
|
||||
| Timing/event sources | PARTIAL | exchange/safebox/mall dirty signal、10m 距离关闭和密码错误 UI 存在;真实 GC_END、GC_ITEM_SET/DEL、断线、重复请求和重新打开时序缺回归 |
|
||||
| Resource/data sources | PARTIAL | live item tooltip 使用协议 item 数据并保留 socket/attribute;本地交易/仓库 helper 使用脚本 Dictionary,移动端 pending/首空位选择可能在服务器结果前改变显示 |
|
||||
| Protocol side effects | PARTIAL | CG exchange、safebox、mall 基础布局和 store parser 存在;exchange 的动作门控、完整错误结果、商城掉落音效和实际服务器回包副作用不完整 |
|
||||
| Interruption/failure/cleanup | GAP | helper 可无服务器回包直接成功;UI 的本地关闭、optimistic 源物品/pending 集合、密码失败、背包满、断线和乱序恢复没有完整回滚/重放证明 |
|
||||
|
||||
## Regression evidence
|
||||
|
||||
- `project/test_player_trade_exchange_parity.gd`:28/28 通过;覆盖的是本地交易 helper 的距离、不可交易物品、双方锁定、金币、空间和原子交付,不是 `GC_EXCHANGE` live 回包。
|
||||
- `project/test_safebox_mall_parity.gd`:37/37 通过;覆盖 fake client 的 45 格、距离、socket/attribute、checkin/checkout/move、商城禁止插入和密码命令。
|
||||
- `project/test_safebox_storage_keeper_parity.gd`:37/37 通过;覆盖本地密码/冷却、库存堆叠、移动、金条熔铸/承兑和非法格位,不能证明 40250 server authority。
|
||||
- `project/shop_cube_mall_test.gd`、`project/test_mall_affect_parity.gd`、`project/popup_windows_parity_test.gd`、`project/test_security_lock_parity.gd`、`build/extension/net_entity_test`、`build/extension/net_classic_session_test`:通过;主要验证 UI/helper、item-mall 辅助效果或基础 packet/store,没有完整 exchange/safebox/mall 服务器拒绝、乱序、断线和回包回滚。
|
||||
- `project/complex_item_drop_test.gd`:当前 1 项失败(购买模式仍允许把物品拖到出售目标),说明统一拖放门控仍有可复现差异;该失败属于相邻商店合同,但会影响交易/仓库共用拖放边界审计。
|
||||
|
||||
## Deep audit round 2026-09-21
|
||||
|
||||
### Fresh regression results
|
||||
|
||||
- `godot --headless --path project --script test_player_trade_exchange_parity.gd`:退出码 0,28/28 通过;覆盖本地交易距离、anti-give、锁定、金币、空间、原子转移和取消 helper。
|
||||
- `godot --headless --path project --script test_safebox_mall_parity.gd`:退出码 0,37/37 通过;覆盖 fake client 的仓库/商城窗口、45 格、距离关闭、属性显示、存取移动和密码命令。
|
||||
- `godot --headless --path project --script test_safebox_storage_keeper_parity.gd`:退出码 0,37/37 通过;覆盖本地密码/冷却、堆叠、45 格和金条 helper。
|
||||
- `godot --headless --path project --script shop_cube_mall_test.gd`、`test_mall_affect_parity.gd`、`popup_windows_parity_test.gd`、`test_security_lock_parity.gd`:均退出码 0;商城效果、窗口和安全锁局部测试通过,其中安全锁为 48/48。
|
||||
- `build/extension/net_entity_test` 与 `build/extension/net_classic_session_test`:均退出码 0;基础 exchange/safebox/mall parser/session 通过。
|
||||
- `godot --headless --path project --script complex_item_drop_test.gd`:退出码 1;购买模式出售目标门控失败,属于相邻 shop/drag contract 的持续回归。
|
||||
|
||||
### Static parity findings
|
||||
|
||||
- 40250 交易请求全部先经过 `__CanActMainInstance`,客户端只发送源位置/金币/接受/退出,`GC_EXCHANGE_*` 决定展示和最终结束;当前 native exchange 请求主要做阶段/范围校验,`player_trade_exchange_system.gd` 仍可脱离网络直接完成双方金币、空间和物品转移。
|
||||
- 当前 `exchange_ui.gd` 的 `_offered_cells`、pending 和取消/距离关闭逻辑没有统一由 `GC_ITEM_ADD/DEL`、`GC_ACCEPT`、`GC_END` 重建;拒绝、乱序、断线和重新打开可能保留乐观状态。
|
||||
- 40250 Safebox/Mall item 回包保留 vnum/count/flags/anti_flags/socket/attribute,并由服务端决定页数、堆叠、空间和金币;当前 live native/store 路径能承接这些字段,但 `safebox_storage_keeper_system.gd` 仍保存密码、冷却、堆叠和金条 authority。
|
||||
- Safebox/Mall UI 的距离自动关闭、密码命令和商城只出路径有局部验证;没有真实 `GC_SAFEBOX_SIZE/SET/DEL/WRONG_PASSWORD`、`GC_MALL_*` 与背包回包的成功/失败/重连/断线顺序 fixture,也没有 Mall checkout 音效的端到端验证。
|
||||
- `M2Client`/`ClassicSession` 的 exchange/safebox/mall 包布局基础存在,但 native action gate、交易 active/partner/金币拥有量、服务器拒绝文案、SendSequence 和关闭幂等性未形成统一合同。
|
||||
|
||||
### Round conclusion
|
||||
|
||||
本轮仍为 `PARTIAL`:交易/仓库/商城 native 骨架和局部 UI 测试通过,但本地结算/密码/库存 helper 旁路、乐观关闭/pending 状态、动作门控与真实 GC 回包回滚仍未与 40250 统一。本轮未修改实现代码。
|
||||
|
||||
## Active UserInterface exchange, safebox and shop review
|
||||
|
||||
- `PythonExchange.h`/`PythonExchangeModule.cpp` 的状态读取覆盖双方名称、金钱、接受状态、槽位 vnum/count、metin socket、attribute 和 elk mode;这些值由交易回包写入,UI getter 不承担结算。
|
||||
- `PythonSafeBox.h` 区分 safebox 与 mall 的打开尺寸、slot item、删除、金钱和当前大小;`PythonShop.h` 另外维护普通商店 tab/coin/name/item,以及私有商店 stock/display position/price/build/close,不能把它们合并为一个背包字典。
|
||||
- 当前 native/store 已覆盖部分字段,但交易、仓库和商城 helper 仍有本地 optimistic/persistent 状态;真实 GC 添加/删除/接受/错误、密码失败、页尺寸、私店重建、断线和重复关闭顺序没有逐包等价测试,合同保持 `PARTIAL`。
|
||||
|
||||
## Static review evidence
|
||||
|
||||
已将 `PythonExchange.h`、`PythonExchangeModule.cpp`、`PythonSafeBox.h`、`PythonShop.h` 纳入 active source 证据;本轮确认了交易/仓库/商城的独立 owner 和回包字段边界,未修改实现。
|
||||
|
||||
## Status
|
||||
|
||||
`PARTIAL`。当前端已经具备 exchange/safebox/mall 的 native 协议骨架、回包存储和主要 UI,交易物品属性与仓库商城的基本网络状态能够展示;但 native exchange 请求缺 40250 动作门控,交易/仓库 UI 存在 optimistic 状态和提前关闭,本地 helper 直接承担交易结算、密码、库存和金条 authority,且真实错误、乱序、断线和回包回滚证据不足。本轮完成审计并记录,未修改实现。
|
||||
@@ -0,0 +1,108 @@
|
||||
# social.party_guild_messenger
|
||||
|
||||
## Scope
|
||||
|
||||
审计组队邀请/接受/离队、队员状态/HP/角色、经验分配模式和组队技能;公会信息、成员/权限、技能、公告、公会战和邀请;好友/Messenger 在线与移动状态;以及会徽/战旗下载、缓存、上传和角色头顶徽章刷新。判断标准是当前端是否以 40250 的网络回包和资源服务为权威,并复现 UI 分支、协议副作用、失败清理和角色显示更新,而不是本地系统类能否独立模拟同样数值。
|
||||
|
||||
## Reference call chain
|
||||
|
||||
- `UserInterface/PythonNetworkStreamPhaseGame.cpp` / `PythonNetworkStreamPhaseGameActor.cpp`
|
||||
- 解析 `GC_PARTY_*`、`GC_GUILD_*`、`GC_MESSENGER`、`GC_CHARACTER_*`;组队邀请、加入、移除、角色/HP/affect、分配模式由服务器回包驱动。
|
||||
- 公会 `INFO/LIST/GRADE/GRADE_NAME/GRADE_AUTH/CHANGE_EXP/SKILL_INFO/COMMENTS/WAR/WAR_POINT/GUILD_NAME/MONEY_CHANGE` 都只更新 `CPythonGuild`/窗口状态,客户端不计算经验、战绩奖励或权限结果。
|
||||
- Messenger 通过 `CPythonMessenger` 维护好友、公会和家族三组,登录/登出/移动回调 UI;添加/删除和邀请应答走 `CG_MESSENGER` 或 `/messenger_auth`。
|
||||
- `UserInterface/PythonGuild.cpp` / `PythonMessenger.cpp` / `PythonCharacterManager.cpp`
|
||||
- 公会成员、等级名称/权限、公告、技能、战争列表和角色 guild id 由网络同步;`ChangeGVG`/`RefreshAllGuildMark` 使角色关系和会徽资源刷新。
|
||||
- `CPythonCharacterManager` 不负责本地结算公会战奖励或自动增长公会技能点;这些结果由服务器 `GC_GUILD`/聊天提示给出。
|
||||
- `UserInterface/GuildMarkDownloader.cpp` / `GuildMarkUploader.cpp` / `MarkManager.cpp`
|
||||
- 会徽是独立连接和资源链:登录握手→mark index→本地 CRC 列表→压缩 block→拼接 mark image;符号另有 CRC/数据包;完成后通知角色刷新。
|
||||
- 上传前检查会长/权限、BMP/尺寸和数据;成功以 mark server 回包为准,不是把像素写入一个脚本字典就完成。
|
||||
- `Client/Eternexus/root/uiparty.py` / `uiguild.py` / `uimessenger.py` / `uiuploadmark.py`
|
||||
- UI 只展示网络状态、发送请求和等待回包;队伍角色/affect、经验和公会战积分不能由客户端本地 authoritative 计算。
|
||||
|
||||
## Current call chain
|
||||
|
||||
- native `M2Client`/`ClassicSession` 已提供 party invite/answer/remove/state/skill/parameter,friend add/remove/answer,guild 成员/权限/公告/技能/战争/邀请和 mark download/upload 等接口;`EntityStore` 也有对应的 party/friend/guild/war/mark 状态、dirty 标记和事件队列。
|
||||
- `project/ui/party_ui.gd`、`guild_ui.gd`、`friend_ui.gd` 已连接 live native signal/getter,主要操作会发送 `M2Client` 请求;这部分是当前实现中最接近 40250 的路径。
|
||||
- `project/party_management_system.gd` 是独立本地系统:直接创建队伍、计算按等级/均分经验、分配角色光环和数值加成;`project/guild_skill_system.gd` 本地计算公会升级/技能点/技能 buff;`project/guild_war_system.gd` 本地计算比分、目标分、天梯变化和 1,000,000 公会金币奖励。这些算法不是 40250 客户端的 authority 链,主要由 parity 测试使用。
|
||||
- `project/messenger_system.gd` 直接修改 friends/guild/family 字典并记录事件;live `FriendUI` 则使用 native `get_friends`、`add_friend`、`remove_friend` 和 `friend_answer`,两套状态源未统一。
|
||||
- `project/guild_mark_uploader_system.gd` 直接把上传数据写入本地 `cached_marks`;live `game_scene.gd`/`GuildUI` 使用 native mark server 方法,且默认图源可能是 `res://ui/default_guild_mark.png` 或运行时生成的 16x12 占位图。
|
||||
- `extension/src/net/classic/classic_mark_client.*` 和 `M2Client` 已有独立 mark 连接/缓存/图片接口,`app_flow.gd` 会在登录/选人后启动下载并在 `guild_mark_updated` 后重试;但现有自动测试主要用 fake client,缺真实 mark packet/压缩 block/CRC/断线恢复证明。
|
||||
|
||||
## Branch matrix
|
||||
|
||||
| 场景 | 40250 | 当前端 | 结论 |
|
||||
|---|---|---|---|
|
||||
| 组队邀请/应答/移除 | CG 请求→GC 邀请/加入/移除;失败由服务器提示 | native 包、store、UI 对应存在 | PARTIAL:真实拒绝/重复/断线序列不足 |
|
||||
| 队员状态/HP/affect/VID | `GC_PARTY_ADD/UPDATE/LINK/UNLINK/PARAMETER` 驱动 UI | EntityStore 与 PartyUI 能展示 | PARTIAL:unordered map 顺序与重连恢复未验证 |
|
||||
| 经验分配/角色光环 | 客户端只显示 server state/affects,结算在服务器 | 本地 helper 直接计算 exp 与 buff | GAP(helper 不等于 40250 implementation) |
|
||||
| Messenger 好友 | server list/login/logout/mobile/remove,邀请用 auth 流程 | native live 路径完整,独立 helper 直接改字典 | PARTIAL:两套状态源及真实拒绝/恢复缺证据 |
|
||||
| 公会 info/member/grade/auth | `GC_GUILD` 回包更新,权限由服务器执行 | native parser/getter/UI 操作链存在 | PARTIAL:请求结果、顺序、权限拒绝和重连未完整覆盖 |
|
||||
| 公会技能/捐献/公告 | client 发请求,skill/exp/point/comments 由 GC 回包提供 | live UI/native 请求存在;本地 `GuildSkillSystem` 另算升级/技能点 | PARTIAL/GAP:本地 helper 不能作为 parity 证明 |
|
||||
| 公会战声明/状态/列表/积分 | GC_WAR/WAR_LIST/WAR_POINT/END 驱动展示,结算权威在服务端 | native status/event/score 存在;本地 war system 自己结算奖励 | PARTIAL:live native 近似,helper 分支为 GAP |
|
||||
| 公会邀请/创建 | 对话/确认后 CG_GUILD,结果 GC/command | UI/native 应答和创建请求存在 | PARTIAL:失败文案和回包清理不足 |
|
||||
| 会徽下载 | 独立 mark session、index/CRC/block、解压/缓存、角色刷新 | classic mark client/native API/app_flow 与 Sprite3D 刷新存在 | PARTIAL:真实资源协议和损坏 block 恢复未测 |
|
||||
| 会徽/战旗上传 | 权限、BMP/尺寸/实际数据→mark server 回包 | native upload 存在;本地 uploader 另有直接缓存 helper,默认可上传占位图 | PARTIAL/GAP:本地 helper 不是服务端上传结果 |
|
||||
| 角色徽章刷新/移除 | `RefreshAllGuildMark` 与 guild id/mark 数据共同驱动 | `NetWorld` 根据 mark image 创建/刷新 Sprite3D | PARTIAL:实际角色模型/换图/死亡/重连资源生命周期未端到端验证 |
|
||||
|
||||
## Implementation-equivalence matrix
|
||||
|
||||
| 项目 | 状态 | 证据/限制 |
|
||||
|---|---|---|
|
||||
| Preconditions | PARTIAL | native 对 VID/PID/阶段和部分字段做边界校验;本地 party/guild/mark helper 可绕过服务端权限、当前角色和会长身份 |
|
||||
| Branch structure | PARTIAL | live party/guild/friend/mark 主干存在;helper 本地结算、公会战奖励、技能升级、Messenger 缓存和上传缓存形成额外旁路 |
|
||||
| Algorithms/formulas | GAP | 40250 客户端不拥有经验分配、公会技能升级、公会战奖励公式;当前 helper 的计算不能宣称与 40250 客户端实现统一 |
|
||||
| State transition order | PARTIAL | live native 是请求→GC 回包→dirty/signal→UI;本地系统是立即改字典/发 signal,未按权威回包顺序运行 |
|
||||
| Constants/units | PARTIAL | PARTY 角色/affect槽、guild grade/skill、mark 16x12/64x128 和协议布局部分存在;本地 buff、奖励、等级阈值需由 server 数据证明 |
|
||||
| Timing/event sources | PARTIAL | live signal/dirty/mark retry 存在;好友/公会/队伍登录、重连、mark block 分片和战争连续事件未完整验证 |
|
||||
| Resource/data sources | PARTIAL | native mark store 与角色资源接口存在;默认上传图源和本地缓存 helper 可能取代真实用户资源/服务器 mark 数据 |
|
||||
| Protocol side effects | PARTIAL | classic party/guild/friend/mark wire 基础存在;服务器错误、权限拒绝、命令提示和 mark 独立连接所有副作用缺完整测试 |
|
||||
| Interruption/failure/cleanup | GAP | 本地 helper 可在无回包时进入成功状态;断线/换图/离会/好友清空/重复 mark 下载、坏 block、战争结束和 UI 弹窗清理未端到端覆盖 |
|
||||
|
||||
## Regression evidence
|
||||
|
||||
- `project/test_party_management_parity.gd`:30/30 通过,但验证的是本地队伍创建、经验和角色 buff helper,未启动服务端或消费 `GC_PARTY_*`。
|
||||
- `project/test_messenger_parity.gd`:27/27 通过,但验证的是独立 `messenger_system.gd` 字典/事件;live native Messenger list/login/logout/mobile/邀请回包未被此测试覆盖。
|
||||
- `project/test_guild_mark_uploader_parity.gd`:12/12、`project/guild_mark_test.gd` 和 `project/guild_mark_tail_test.gd` 通过;主要使用本地 uploader/fake client,未覆盖真实 mark server index/CRC/compressed block 和坏数据清理。
|
||||
- `project/test_guild_war_parity.gd`:25/25、`test_guild_war_observer_arena_parity.gd`:21/21、`test_guild_skill_parity.gd`:31/31、`guild_creation_ui_test.gd`:通过;公会战/技能测试主要验证本地公式和状态机,不等价于 40250 `GC_GUILD` 权威结果。
|
||||
- `project/test_privilege_empire_guild_parity.gd`、`project/p8_test.gd`:通过;覆盖 UI/辅助系统,不覆盖所有 social 回包拒绝/乱序/重连。
|
||||
- `build/extension/net_entity_test`、`build/extension/net_classic_session_test`:party/messenger/guild 基础 parser/session 检查通过;没有完整 `GC_GUILD`、`GC_MESSENGER`、`GC_PARTY` 错误序列和 mark client packet 测试。
|
||||
|
||||
## Deep audit round 2026-09-21
|
||||
|
||||
### Fresh regression results
|
||||
|
||||
- `godot --headless --path project --script test_party_management_parity.gd`:退出码 0,30/30 通过;覆盖本地队伍创建、离队、角色光环和经验分配 helper。
|
||||
- `godot --headless --path project --script test_messenger_parity.gd`:退出码 0,27/27 通过;覆盖独立 messenger 字典/事件状态。
|
||||
- `godot --headless --path project --script test_guild_mark_uploader_parity.gd`:退出码 0,12/12 通过;覆盖本地尺寸、权限和缓存 helper。
|
||||
- `godot --headless --path project --script guild_mark_test.gd`、`guild_mark_tail_test.gd`:均退出码 0;会徽 UI/尾标局部路径通过。
|
||||
- `godot --headless --path project --script test_guild_war_parity.gd`:退出码 0,25/25 通过;`test_guild_war_observer_arena_parity.gd`:21/21 通过;均主要验证本地战争/观察者状态机。
|
||||
- `godot --headless --path project --script test_guild_skill_parity.gd`:退出码 0,31/31 通过;覆盖本地公会技能等级、消耗、buff 和 cooldown 公式。
|
||||
- `godot --headless --path project --script guild_creation_ui_test.gd`、`test_privilege_empire_guild_parity.gd`、`p8_test.gd`:均退出码 0;UI/权限辅助路径通过。
|
||||
- `build/extension/net_entity_test` 与 `build/extension/net_classic_session_test`:均退出码 0;基础 party/messenger/guild/mark parser/session 检查通过。
|
||||
|
||||
### Static parity findings
|
||||
|
||||
- 40250 的队伍成员、经验分配、角色光环和公会技能/战争结果都由 `GC_PARTY_*`/`GC_GUILD_*` 与服务器状态驱动;当前 `PartyManagementSystem`、`GuildSkillSystem`、`GuildWarSystem` 仍可在无回包时直接计算并发成功信号,不能作为等价实现。
|
||||
- live `FriendUI` 使用 native friend list/请求,`MessengerSystem` 却直接改本地 friends/guild/family 字典;两套状态源在拒绝、重复、离会、重连时可能分叉。
|
||||
- 会徽 native mark client 已有独立连接、index/CRC/block 和缓存骨架,但当前测试没有真实 mark server 分片、压缩/CRC 失败、断线恢复和完成后所有角色刷新 fixture;本地 uploader helper 直接写 `cached_marks` 仍是旁路。
|
||||
- 公会创建、邀请、权限、公告、技能和战争的 live native 入口存在,但错误文本、权限拒绝、事件顺序、换图/离会清理和重复回包没有形成统一状态合同。
|
||||
- 角色徽章/公会关系刷新当前由 NetWorld 的 Sprite3D 适配承接,没有证明与 40250 `RefreshAllGuildMark`、真实角色模型、死亡/重连/换图资源生命周期保持一致。
|
||||
|
||||
### Round conclusion
|
||||
|
||||
本轮仍为 `PARTIAL`:native 社交协议和 UI 主干较完整,局部测试全部通过;但本地结算/缓存旁路、真实权限与错误回包、队伍/公会/好友重连清理以及会徽独立连接仍未与 40250 统一。本轮未修改实现代码。
|
||||
|
||||
## Active UserInterface guild-mark and social-state review
|
||||
|
||||
- `GuildMarkDownloader.h` 的可达状态机是独立 `CNetworkStream`:离线/登录/握手/ping/mark-index/mark-block/symbol/key-agreement/完成,并在请求前发送本地 mark CRC/index 列表;`GuildMarkUploader.h` 还要求读取并校验 BMP/符号、发送 mark 或 symbol、等待完成后断开。
|
||||
- `MarkImage.cpp/h` 不是单个图片缓存:它按 mark/block 网格保存压缩块,使用 LZO 安全解压、CRC、差异 block 列表、空槽分配和本地持久化;`MarkManager.h` 维护 guild id→mark id、index、图片文件和 block CRC 的持久映射。
|
||||
- `PythonGuild.h` 明确区分 guild 基础信息、等级/权限、成员 grade/job/level/offer、公告、技能点和敌对公会;`PythonMessenger.h` 暴露好友、公会成员、登录/登出、移动状态和删除回调。这些都属于回包驱动状态,不是本地公会战或 messenger 字典的 authority。
|
||||
- 当前端虽有 `classic_mark_client` 和 native guild/messenger 入口,但本地 `GuildMarkUploaderSystem`、`MessengerSystem` 仍可旁路直接写缓存/字典;真实分片、CRC/LZO 失败、权限拒绝、重连清理和完成后的全部角色刷新仍无等价证据,合同保持 `PARTIAL`。
|
||||
|
||||
## Static review evidence
|
||||
|
||||
已将 `GuildMarkDownloader.h`、`GuildMarkUploader.h`、`MarkImage.cpp/h`、`MarkManager.h`、`PythonGuild.h`、`PythonMessenger.h` 纳入本合同的 active source 证据;未因此提升状态。
|
||||
|
||||
## Status
|
||||
|
||||
`PARTIAL`。当前端的 native 社交协议和主要 UI 已有较完整骨架,组队、公会、好友和会徽 live 主路径大体接上 40250;但本地 party/guild skill/war/messenger/mark helper 仍把服务端权威逻辑复制成直接状态机,且真实权限拒绝、回包顺序、重连清理和独立会徽连接缺端到端证据。因此不能把现有 parity 测试通过等同于实现统一。本轮完成审计并记录,未修改实现代码。
|
||||
@@ -0,0 +1,97 @@
|
||||
# ui.equipment-tooltip
|
||||
|
||||
## Scope
|
||||
|
||||
比较查看装备、装备属性计算、普通提示框与查看装备窗口的创建/销毁、定位、内容和重复打开行为。
|
||||
|
||||
## Reference call chain
|
||||
|
||||
- `UserInterface/PythonNetworkStreamPhaseGameItem.cpp`
|
||||
- `UserInterface/PythonItem.cpp`
|
||||
- `UserInterface/PythonItemModule.cpp`
|
||||
- `UserInterface/PythonNetworkStreamPhaseGame.cpp`
|
||||
- `UserInterface/PythonTextTail.cpp`
|
||||
|
||||
说明:40250 `ClientVS22/source` 树中没有 Python UI 脚本;迁移目录中的脚本只能作为历史/资产线索,不能替代 C++ 参考实现。
|
||||
|
||||
## Current call chain
|
||||
|
||||
- `project/ui/view_equipment_ui.gd`
|
||||
- `project/ui/item_tooltip.gd`
|
||||
- `project/ui/item_tooltip_view.gd`
|
||||
- `project/ui/ui_manager.gd`
|
||||
- `project/ui/inventory_ui.gd`
|
||||
- `project/ui/shop_ui.gd`
|
||||
- `project/ui/equip_model.gd`
|
||||
- `project/ui/player_view.gd`
|
||||
- `project/view_equipment_ui_test.gd`
|
||||
- `project/item_tooltip_test.gd`
|
||||
|
||||
## Initial finding
|
||||
|
||||
当前已经针对“两个属性面板”和查看装备窗口增加了实现与测试,但尚未完成 40250 的窗口生命周期、属性来源、槽位顺序、装备/时装分层和关闭清理的 1:1 证明。重点检查重复创建、普通 tooltip 与查看装备窗口同时存在、远程角色更新和切换目标。
|
||||
|
||||
## Audit findings (2026-09-20)
|
||||
|
||||
`ViewEquipmentUI` 本身已经使用 `UiManager` 提供的单例 `ItemTooltipView`,测试也确认查看装备窗口的尺寸、11 个槽位和共享属性面板通过。但全局提示框路径仍存在明确的重复面板风险:
|
||||
|
||||
- `project/ui/inventory_ui.gd` 在 `_fill_cell` 中给每个格子写入 Godot 原生 `cell.tooltip_text`,同时在 `mouse_entered` 中显示共享 `ItemTooltipView`。
|
||||
- `project/ui/shop_ui.gd` 在商店格子中同样写入 `slot.tooltip_text`,并在悬停时显示共享 `ItemTooltipView`。
|
||||
- 40250 的 `uiToolTip.py` 是自绘/自管理的 `ItemToolTip` 窗口语义;当前端同时保留原生 tooltip 和自定义 tooltip,因此“查看装备窗口自身只有一个面板”的测试不能证明背包、商店和装备查看的全局单面板约束。
|
||||
|
||||
这解释了为什么局部查看装备测试可以通过,但实际切换到背包或商店仍可能出现两个属性面板。另一个未决点是 `ViewEquipmentUI` 的固定位置与 40250 窗口定位/屏幕边界处理尚未完成对照。当前仅记录差异,本轮不把它误标为已修复。
|
||||
|
||||
## Baseline tests
|
||||
|
||||
`view_equipment_ui_test.gd` 当前通过,确认已覆盖的 40250 对话框尺寸、11 个装备槽、共享属性面板和单实例约束正常;属性数据来源及完整窗口生命周期仍未完成对照。
|
||||
|
||||
## Equivalence matrix
|
||||
|
||||
| 项目 | 状态 | 备注 |
|
||||
|---|---|---|
|
||||
| Preconditions | UNASSESSED | 目标有效性、物品存在和查看权限 |
|
||||
| Branch structure | UNASSESSED | 普通物品、装备、时装、空槽和未知物品 |
|
||||
| Algorithms/formulas | UNASSESSED | 属性和附加属性展示来源 |
|
||||
| State transition order | UNASSESSED | request → data → panel → tooltip → cleanup |
|
||||
| Constants/units | UNASSESSED | 槽位、尺寸、偏移和格式化规则 |
|
||||
| Timing/event sources | UNASSESSED | 网络查看结果和鼠标悬停更新 |
|
||||
| Resource/data sources | UNASSESSED | item_proto、itemdesc、UI `.sub` 和 locale |
|
||||
| Protocol side effects | UNASSESSED | 查看装备请求/响应及重复请求 |
|
||||
| Interruption/failure/cleanup | UNASSESSED | 关闭、切换目标、重复打开和节点回收 |
|
||||
|
||||
## Remaining
|
||||
|
||||
- 对照 40250 的 `uiToolTip.py`/`uiCharacter.py` 及查看装备协议处理。
|
||||
- 证明背包、商店、交易、仓库、商城仓库、个人摊位和查看装备共用同一个 UiManager 实例,而不是只在各自 headless fixture 中各自创建 fallback。
|
||||
- 增加切换目标、快速打开关闭、空装备、远程更新和窗口边界定位测试。
|
||||
|
||||
## Next audit round 2026-09-20T18:19Z
|
||||
|
||||
本轮继续沿 40250 的 `PythonSlotWindow`/`uiToolTip.ItemToolTip` 生命周期核对全局 tooltip,而不是只检查 `ViewEquipmentUI`:
|
||||
|
||||
- 40250 的装备查看窗口与背包/商店格都通过 ItemToolTip 的 `ShowToolTip`/`HideToolTip` 事件管理自绘窗口;当前 `ViewEquipmentUI` 确实通过 `UiManager` 复用单个 `ItemTooltipView`,局部测试中的“只有一个属性面板”成立。
|
||||
- 当前 `InventoryUI::_fill_cell`(`project/ui/inventory_ui.gd:195-209`)仍给格子写入 `cell.tooltip_text`,随后 `_on_cell_mouse_entered`(`:282-300`)又显示共享 `ItemTooltipView`。`ShopUI::_fill_slot`(`project/ui/shop_ui.gd:420-426`)同样同时设置 `slot.tooltip_text` 并连接 `_show_slot_tooltip`。鼠标悬停时存在 Godot 原生 tooltip 与自绘属性面板并存的路径,这正是“查看装备时出现两个面板”在背包/商店入口仍可能复现的结构性差异。
|
||||
- `view_equipment_ui_test.gd` 通过并且退出时仍报告 5 个 ObjectDB 泄漏;该测试只覆盖查看装备窗口自身,不覆盖背包/商店 tooltip 互斥,也不能作为完整生命周期清理证据。
|
||||
- `ViewEquipmentUI` 的固定 `870,300` 起始位置与 `ItemTooltipView::update_position` 的鼠标跟随/底部翻转策略仍不是同一定位算法;当前尚无屏幕边界和快速切换目标的回归测试。
|
||||
|
||||
结论:40250 语义的查看装备单例路径已经有局部实现,但全局 tooltip 互斥和窗口清理尚未闭合。本轮没有修改实现代码,合同保持 `PARTIAL`;下一步应由 `UiManager` 统一负责 item tooltip 的显示/隐藏,并清除物品格上的原生 `tooltip_text`,再补背包↔商店↔查看装备快速切换测试。
|
||||
|
||||
## Implementation fix round 2026-09-21
|
||||
|
||||
- `InventoryUI` 和 `ShopUI` 的物品格现在清空 Godot 原生 `tooltip_text`,属性内容保存在格子 metadata,并统一交给共享 `ItemTooltipView`;移动端详情读取同一份内容,不再产生第二个原生面板。
|
||||
- `inventory_ui_test.gd`、`view_equipment_ui_test.gd` 和 `test_npc_shop_flow.gd` 已覆盖背包/装备/商店的单面板路径、属性/价格内容和刷新后的共享面板。
|
||||
|
||||
本合同继续为 `PARTIAL`:远程查看协议、窗口边界定位、快速切换和完整销毁生命周期仍需按 40250 继续审计。
|
||||
|
||||
## Implementation fix round 2026-09-21 — all item windows use the custom tooltip owner
|
||||
|
||||
继续按 40250 `PythonSlotWindow`/`ItemToolTip.ShowToolTip` 的单实例语义收口物品入口:新增
|
||||
`ItemTooltipBridge`,并将交易、仓库、商城仓库和个人摊位的物品行/物品格接入
|
||||
`UiManager.get_item_tooltip_view()`;没有 UiManager 的 headless fixture 使用同一 bridge 的
|
||||
局部 fallback。所有这些 item control 现在清空 Godot 原生 `tooltip_text`,悬停进入/离开只
|
||||
切换自绘 `ItemTooltipView`,关闭窗口时主动隐藏。
|
||||
|
||||
`test_safebox_mall_parity.gd` 新增原生重复面板和自绘悬停回归,40 项通过;同时回归
|
||||
`p8_test.gd`、`test_npc_shop_flow.gd`、`private_shop_ui_test.gd` 和 `item_tooltip_test.gd`
|
||||
均通过。真实运行时多窗口共享 UiManager、边界定位、快速切换和完整销毁顺序仍保持
|
||||
`PARTIAL`。
|
||||
@@ -0,0 +1,98 @@
|
||||
# ui.hud_target_minimap_status —— 角色状态、目标框、小地图、文本尾标和快捷栏
|
||||
|
||||
状态:`PARTIAL`
|
||||
|
||||
本合同检查 HUD 的数据源、UI 实例注册、刷新顺序、目标类型分派、HP/EXP/等级、名字尾标、聊天气泡、小地图标记/缩放、快捷栏和死亡/删除/换图清理。静态模块测试通过,只能证明局部 helper;正式 `GameScene` 中实际创建了哪些控件、信号是否接通、旧控件是否重复显示,必须单独核对。
|
||||
|
||||
## 40250 参考调用链
|
||||
|
||||
- `UserInterface/PythonApplication.cpp` / `PythonApplication.h`
|
||||
- 每帧按应用渲染顺序更新 PythonPlayer、TextTail、Effect 和 UI;TextTail 先刷新全部尾标,再显示目标/拾取/全显尾标,最后更新显示和排布。
|
||||
- `UserInterface/PythonPlayer.cpp` / `PythonPlayer.h`
|
||||
- `SetStatus` 写入带保护的点数组;等级变化同步 `CInstanceBase::SetLevel` 和等级尾标,战斗点变化触发派生战斗状态重算。
|
||||
- `SetQuickPage` 按 `QUICKSLOT_MAX_LINE` 环绕,快捷栏按全局槽位保存,使用时区分 inventory/skill/emotion 并经网络发送。
|
||||
- 目标死亡、删除和不可选中时清理目标;`UpdatePartyMemberInfo/Affect` 更新队伍状态和头像/尾标数据。
|
||||
- `UserInterface/PythonTextTail.cpp` / `.h`
|
||||
- 维护角色、聊天、信息、掉落尾标的 map/list/pool;角色尾标由 `RegisterCharacterTextTail` 建立,按 guild/title/level 资源更新;聊天尾标 5000ms 到期,`bNameFlag` 会强显名字并把名字上移 17px;物品尾标按屏幕 AABB 互相下推。
|
||||
- `DeleteCharacterTextTail`、死亡分支和地图清理必须同时删除角色名、公会/称号/等级、聊天和物品 owner 子实例。
|
||||
- `UserInterface/PythonMiniMap.cpp` / `.h`
|
||||
- 默认 `m_fScale=2.0`,`SetScale` 范围 `[0.5,4.0]`,ScaleUp/Down 分别乘 2/0.5;以 terrain cell/cm 坐标计算玩家、PC、怪物、NPC/warp、observer、guild area、waypoint/target mark 和玩家朝向。
|
||||
- `__SetPosition`、`Render`、地图中心及点击拾取都使用同一 scale/terrain-cell 单位,并按边缘半径处理超出地图的 waypoint。
|
||||
- `UserInterface/PythonPlayer.cpp`、`PythonApplication.cpp`、`uitaskbar.py` / `uicharacter.py` / `uitarget.py`
|
||||
- TaskBar/CharacterWindow/TargetBoard 由统一 UI 入口创建并接收 points、target、quickslot、party 和 entity 删除事件,不允许只保留“存在但未注入”的独立控件。
|
||||
|
||||
## 当前端实现映射
|
||||
|
||||
- `project/net_play.gd` / `project/hud.gd` / `project/ui/quickbar.gd`
|
||||
- native `points_changed`/`vitals_changed`/`target_info` 已接入 NetPlay;NetPlay 将 HP/SP/ST/EXP/等级写入 HUD,Quickbar 自己也订阅 `points_changed` 并绘制 4 段经验珠、HP/SP/ST 和 36 槽服务端快捷栏。
|
||||
- `hud.gd::_build_status()` 已经是空实现,旧 HUD 的 setters 只有在 Quickbar 存在时才产生可见效果;这是一次“数据链存在但显示 owner 分散”的适配层。
|
||||
- `project/ui/target_board.gd` / `project/ui/target_ui.gd`
|
||||
- `target_board.gd` 已镜像 `RecvTargetPacket` 的 CLOSE/CLOSE_IF_DIFFERENT/SET_HP/NONE 判定,`target_ui.gd` 已实现宽度、等级/阶级前缀、HP、PC 操作按钮和关闭回调。
|
||||
- 但正式 `project/game_scene.gd` 只把 `target_changed` 连接到 `net_world.set_target_vid`,没有创建 `target_ui.gd` 实例,也没有设置 `net_play.target_ui`;实际运行的目标面板退回 `hud.gd` 的简化 200×40 面板,缺少 40250 TargetBoard 的运行链。
|
||||
- `project/net_world.gd` / `project/text_tail.gd` / `project/text_tail_arrange.gd` / `project/chat_tail.gd`
|
||||
- 已实现角色名字、guild/title/level、guild mark、聊天/信息尾标、5000ms 生命周期、目标/悬停强显和掉落名条 AABB 排布;测试覆盖了静态颜色、标题、聊天和物品尾标 helper。
|
||||
- 当前 `CanPickInstance` 仍用存活/墙体近似,尾标强制显示和名字可见性整表刷新约 2Hz,而 40250 在渲染帧内按实例资格和 list 重建;真实字体/Label3D 像素度量也有 fallback 近似。
|
||||
- `project/ui/minimap.gd` / `project/hud.gd` / `project/game_scene.gd`
|
||||
- `minimap.gd` 已绘制实体、NPC/warp、observer、guild area、quest signal、target marker、坐标和玩家箭头,并连接 channel/world reset;`hud.gd` 另有一套基于 `minimap.dds` 的旧地图贴图与玩家点。
|
||||
- 正式 `GameScene` 同时调用 `hud.setup()`(创建旧 minimap)和 `minimap.setup()`(创建新雷达),没有销毁/隐藏其中一套,存在重复小地图;新雷达 scale 默认 0.25、范围 0.10–1.25、按钮步进 ±0.05/滚轮 1.15,与 40250 默认 2.0、范围 0.5–4.0、按钮 ×2/×0.5 的行为和单位不一致。
|
||||
- `project/ui/char_status_ui.gd`
|
||||
- 已支持状态/技能/表情/任务页、points 读取、装备/技能资源、加点命令和移动端页;`char_status_ui_test.gd` 证明基础布局/刷新/Tab/加点路径。
|
||||
|
||||
## 分支等价矩阵
|
||||
|
||||
| 40250 判据/副作用 | 当前实现 | 结论 |
|
||||
|---|---|---|
|
||||
| points/status → TaskBar HP/SP/ST/EXP/等级刷新 | NetPlay 和 Quickbar 都可接收 points,Quickbar 绘制主要仪表 | 部分等价;HUD 状态 owner 分散,`_build_status()` 空实现,等级/旧 setter 的可见链没有统一快照 |
|
||||
| QuickSlot 全局槽位、页切换、inventory/skill/emotion 分派 | `_state` 36 槽、4×8 页、服务端恢复和激活分支存在 | 基础路径通过;最后 4 个服务端槽、物品引用失效、死亡/变身门控和资源图标完整时序仍需 live packet 证据 |
|
||||
| `RecvTargetPacket` 按 PC/building/stone/enemy/NPC/死亡分派 | `target_board.gd` 和 `net_play._on_target_info` 静态/单测通过 | 判定 helper 基本等价;生产 `target_ui` 未实例化注入,实际 UI 运行链不等价 |
|
||||
| TargetBoard 宽度、等级/阶级、HP、PC 私聊/交易/装备按钮 | `ui/target_ui.gd` 单测通过 | 代码存在但正式场景未挂载;当前可见 HUD 面板是简化实现 |
|
||||
| 角色 guild/title/level、聊天/信息尾标、5000ms 回收 | `net_world` + TextTail/ChatTail + 2Hz 可见性刷新 | 部分等价;`CanPickInstance`、逐帧 list 生命周期、字体真实度量和死亡/删除全链仍有近似 |
|
||||
| 掉落物 owner 名、AABB 去重叠和拾取 | `ground_items` + `text_tail_arrange` | helper 通过;实际投影、字体、删除和鼠标拾取尚未与 native 渲染逐像素对照 |
|
||||
| 小地图 scale、地形单位、玩家/实体/observer/waypoint/target/guild mark | 新 `minimap.gd` 覆盖大部分标记和 observer 插值 | 不等价;scale 常量/步进/单位不同,且 HUD 与新 Minimap 同时显示两套地图 |
|
||||
| 场景创建、目标/小地图/尾标的统一 owner | GameScene 分散创建 HUD、Minimap、NetWorld、NetPlay | 不等价;TargetUI 漏接入,MiniMap 重复创建,跨图/退出时 UI 节点清理未形成统一证据 |
|
||||
| 角色死亡、实体删除、换图清理 | NetWorld 清理 HP bars、尾标、target、实体;NetPlay 清理 target | 部分等价;需补生产场景级删除/换图后无重复 UI、无悬挂 signal 的测试 |
|
||||
|
||||
## 已执行测试
|
||||
|
||||
- `project/test_target_board_parity.gd`:13 passed、0 failed;覆盖 TargetBoard 尺寸、HP、关闭回调和手工注入 `target_ui` 的 NetPlay 分派。
|
||||
- `project/target_board_test.gd`:通过;覆盖目标分类、死亡、异阵营和 LCONTROL 私聊分支。
|
||||
- `project/test_minimap_radar_parity.gd`:6 passed、0 failed;`project/p9_test.gd` 通过;只证明新雷达坐标、按钮、Atlas 信号和基础 world/observer 路径,不检测 GameScene 同时存在两套 minimap,也没有校验 40250 scale 常量。
|
||||
- `project/text_tail_test.gd`、`project/text_tail_arrange_test.gd`、`project/chat_tail_test.gd`、`project/chat_test.gd`、`project/name_show_test.gd`:均通过;这些是 helper/路由测试,未证明完整 native 渲染帧顺序和 `CanPickInstance` 等价。
|
||||
- `project/char_status_ui_test.gd`:通过;覆盖布局、状态刷新、加点和分页。
|
||||
- `project/test_taskbar_quickslot_parity.gd`:34 passed、0 failed;Quickbar/TaskBar 断言通过,但进程报告 221 个 ObjectDB、91 个 CanvasItem RID、3 个资源仍在使用,不能作为稳定清理证据。
|
||||
- `project/taskbar_dock_test.gd`:通过;覆盖任务栏按钮、角色窗 Tab 和安全箱窗口。
|
||||
|
||||
## Deep audit round 2026-09-21
|
||||
|
||||
### Fresh regression results
|
||||
|
||||
- `test_target_board_parity.gd` 13/13、`target_board_test.gd`、`test_minimap_radar_parity.gd` 6/6、`p9_test.gd`、`text_tail_test.gd`、`text_tail_arrange_test.gd`、`chat_tail_test.gd`、`chat_test.gd`、`name_show_test.gd`、`char_status_ui_test.gd`、`taskbar_dock_test.gd` 均退出码 0。
|
||||
- `test_taskbar_quickslot_parity.gd` 34/34,但退出时仍有 221 个 ObjectDB、91 个 CanvasItem RID、3 个资源、56 个 shaped-text RID 和 1 个 font RID 泄漏。
|
||||
|
||||
### Static parity conclusion
|
||||
|
||||
- TargetBoard 单测和手工注入的 `target_ui` 分派通过,但正式 `GameScene` 仍未实例化/注入唯一 TargetUI,实际显示路径仍可能回落到 HUD 简化目标条。
|
||||
- HUD 与新 minimap 仍是两套生产 owner;新雷达的 scale 默认/范围/步进与 40250 的 2.0、0.5–4.0、×2/×0.5 不一致。
|
||||
- TextTail、血条、快捷栏和角色状态 helper 可用,但逐帧显示顺序、真实字体度量、删除/死亡/换图清理和统一 points/target/quickslot owner 仍未闭合。
|
||||
|
||||
本轮仍为 `PARTIAL`,完成 HUD/目标框/小地图/尾标第二轮证据登记,未修改实现。
|
||||
|
||||
## 已确认差异和未验证项
|
||||
|
||||
1. 在正式 GameScene 创建并注入唯一的 `TargetUI`,将 `NetPlay.target_ui` 的 open/set_hp/close 与 TargetBoard 分派接通;再移除/隐藏 HUD 简化目标条,避免两个目标面板 owner。
|
||||
2. 只保留一套生产小地图,统一参考端的 scale/terrain-cell 单位、默认值、上下限、按钮倍率、滚轮和地图/雷达切换行为。
|
||||
3. 将 points、quickslot、target、party、entity delete、world reset 的 UI 更新集中到可观测 owner,补生产场景级重复创建、换图、死亡、重连和退出清理测试。
|
||||
4. 逐帧对照 TextTail 的 `UpdateAllTextTail → Show* → UpdateShowingTextTail → ArrangeTextTail`,消除 `CanPickInstance` 近似、2Hz 可见性延迟和 Label3D 字体度量 fallback。
|
||||
5. 补真实资源/网络包驱动的 HP/EXP/等级/快捷栏/小地图 marker 测试;修复 Quickbar 测试的 ObjectDB/RID/resource cleanup。
|
||||
|
||||
## 下一轮验证入口
|
||||
|
||||
- 先修复/验证 GameScene 的 TargetUI 单例注入和旧 HUD 目标条去重。
|
||||
- 再以一个地图、一个远端 PC、一个敌对怪、一个 NPC/warp、一个 observer 和一条 quest waypoint 采集新旧客户端同帧 UI 快照。
|
||||
- 最后验证 mini-map scale 边界、实体删除/死亡/换图/重连后的尾标、血条、目标框和小地图无残留。
|
||||
|
||||
## Active UserInterface minimap and text-tail bridge review
|
||||
|
||||
- `PythonMiniMapModule.cpp` 暴露小地图 scale/center/size、创建/销毁/更新/拾取、atlas、waypoint、guild area 和地图 marker;这些状态依赖地图坐标、地形/atlas 资源和实体删除顺序。
|
||||
- `PythonTextTailModule.cpp` 暴露角色/聊天/信息尾标注册、标题颜色、投影位置、显示/隐藏、排列和 pick;生产更新必须先更新全部尾标,再更新可见集合和排列,不能由单个 Label3D 的定时器替代。
|
||||
- 当前端 minimap/text-tail 有 helper 和局部布局测试,但生产 owner、marker/waypoint 删除、实体重置、投影失败和资源字体度量未闭合;本轮纳入 active source 证据,合同继续 `PARTIAL`。
|
||||
@@ -0,0 +1,129 @@
|
||||
# ui.input_camera_mobile —— 鼠标、相机、光标、IME、触屏和移动端窗口
|
||||
|
||||
状态:`PARTIAL`
|
||||
|
||||
本合同检查 40250 的输入优先级、键盘状态、鼠标按钮映射、智能拾取、相机连续控制、事件相机、光标状态、IME 生命周期,以及当前端的触屏/移动端适配。判断标准是状态机、调用顺序、拒绝条件、清理副作用和可观察结果一致;“有一个可用的 Godot 控件”不能单独视为与 40250 等价。
|
||||
|
||||
## 40250 参考调用链
|
||||
|
||||
- `UserInterface/PythonPlayerInputKeyboard.cpp`
|
||||
- `NEW_SetSingleDIKKeyState` 先取消钓鱼,再处理方向键;`NEW_SetSingleDirKeyState` 更新方向按键状态并立即进入多方向计算。
|
||||
- `NEW_SetMultiDirKeyState` 在 `__CanMove` 允许时按 WASD/方向键组合计算唯一旋转角,支持精确的四方向和四个对角方向;无方向时调用 `NEW_Stop`。
|
||||
- 攻击键在钓鱼状态下走钓鱼分支,否则更新攻击状态。
|
||||
- `UserInterface/PythonPlayerInputMouse.cpp`
|
||||
- 移动、智能、相机、自动攻击、普通攻击和技能按钮由统一的 `NEW_SetMouseState` 分派。
|
||||
- 智能点击优先级为物品→角色→地面,并受私店、表情、眩晕等状态门控;相机按下进入拖拽,点击结束拖拽或选中目标并打开角色菜单。
|
||||
- 中键相机、Ctrl 行为、技能按钮和拾取/地面射线均有独立状态清理。
|
||||
- `UserInterface/PythonApplicationCamera.cpp`
|
||||
- 普通、站立、混合三类相机状态按帧更新;事件相机保存默认相机,混合期间锁定直接控制,结束后恢复普通相机。
|
||||
- 相机旋转、俯仰和缩放使用连续速度;缩放按 `1.0 + zoomSpeed * direction` 的比例更新,而不是固定距离步进。
|
||||
- `UserInterface/PythonApplicationCursor.cpp` / `EterLib/Input.cpp`
|
||||
- 光标有连续光标状态、硬件光标显隐和相机旋转等光标形状;DirectInput 键盘维护 256 字节状态并支持重新获取。
|
||||
- `UserInterface/PythonIME.cpp`
|
||||
- 文本输入显式处理左右/Home/End/删除、Tab/Return/Escape、字符输入、候选窗口和输入法状态回调。
|
||||
|
||||
## 当前端实现映射
|
||||
|
||||
- `project/player_controller.gd` / `project/game_scene.gd`
|
||||
- 已实现桌面 WASD/方向键状态、按键释放清理、地面/物品/角色点击优先级,以及触屏点击与拖拽分离;移动端摇杆复用同一移动入口。
|
||||
- 当前按键分为 `_input` 处理释放、`_unhandled_input` 处理按下,和 40250 的统一即时输入回调不是同一时序;角色碰撞/拾取也由 Godot 射线和简化碰撞半径适配。
|
||||
- `project/game_camera.gd`
|
||||
- 已有桌面右键拖拽、滚轮缩放、触屏单指旋转、双指缩放、Q/E/R/F/T/G 连续控制、普通/站立/混合事件相机、地形遮挡和镜头震动。
|
||||
- 当前默认距离/俯仰角、固定缩放步进、相机目标高度和事件相机锁定/恢复语义没有与 `CCamera` 的常量和比例缩放逐项统一。
|
||||
- `project/ui/mouse_controller.gd` / `project/ui/cursor_manager.gd`
|
||||
- 已实现物品/技能/表情拖拽、右键/ESC/失焦取消,以及 `.sub` 光标资源加载和 Godot 光标映射。
|
||||
- 没有完整复刻 `NEW_SetMouseState` 的所有按钮分派;没有显式连续光标编号状态、硬件光标显隐封装和相机拖拽时隐藏硬件光标的等价链路。
|
||||
- `project/ui/mobile/*`
|
||||
- 已有安全区、横屏布局、虚拟摇杆、触屏按钮、长按技能瞄准、菜单返回和窗口焦点/暂停时触摸清理;这是移动平台适配层,不是 40250 Windows 输入实现本身。
|
||||
- Godot `LineEdit`/聊天 UI
|
||||
- 当前依赖引擎默认文本输入,没有独立的 `PythonIME` 等价封装来记录光标移动、删除、候选窗口、编码页和输入法回调。
|
||||
|
||||
## 分支等价矩阵
|
||||
|
||||
| 40250 判据/副作用 | 当前实现 | 结论 |
|
||||
|---|---|---|
|
||||
| WASD/方向键组合、对角角度、无键停止和按键释放清理 | `_wasd` 优先级、归一化、角色朝向和焦点清理已实现,输入按下/释放分属不同 Godot 阶段 | 部分等价;基础行为通过,时序和全部 `__CanMove`/钓鱼门控未形成同链证据 |
|
||||
| 鼠标 MOVE/SMART/CAMERA/AUTO/ATTACK/SKILL 统一分派 | 物品、角色、地面点击和相机拖拽分别分布在 PlayerController/GameCamera/Quickbar | 部分等价;完整按钮映射、Ctrl/中键/角色菜单和拒绝分支未统一证明 |
|
||||
| 智能点击物品→角色→地面及状态门控 | 当前 ground item→实体→地面顺序存在,触摸仲裁也存在 | 部分等价;私店、表情、眩晕、可拾取资格和 native 射线语义仍为适配近似 |
|
||||
| 普通相机按帧跟随、旋转/俯仰/比例缩放 | 当前有跟随、拖拽、滚轮和连续键控制 | 不等价;缩放为固定距离步进,默认值/目标高度/连续速度尚未与参考公式统一;`game_camera_test` 3 项失败 |
|
||||
| 事件相机保存、混合、锁定和恢复 | 有 NORMAL/STAND/BLEND 与字典插值/恢复 | 部分等价;缺少完整 CameraManager 状态锁、roll/视线/交叉线和异常中断清理证据 |
|
||||
| 连续光标、相机旋转光标、硬件光标显隐 | 有资源加载和形状映射 | 部分等价;没有连续光标状态,拖拽期间未证明隐藏硬件光标,运行时自定义光标形状映射也不完整 |
|
||||
| IME 光标/删除/候选/字符回调 | 依赖 Godot LineEdit | GAP;没有可审计的独立等价实现和回调测试 |
|
||||
| 触屏、摇杆、双指缩放、移动窗口和安全区 | mobile UI/gesture/window 测试通过 | 平台适配通过;不应误标为 Windows 40250 输入链等价 |
|
||||
| 失焦、暂停、重连、退出时输入状态清理 | PlayerController、MouseController、MobileOverlay 分别清理 | 部分等价;缺少 GameScene 统一清理和相机/鼠标/IME 组合状态的生产级测试 |
|
||||
|
||||
## 已执行测试
|
||||
|
||||
- `project/mouse_controller_test.gd`:通过;覆盖全局物品拖拽状态和取消。
|
||||
- `project/input_key_test.gd`:通过;覆盖 ClientVS22 桌面按键映射、Alt、快捷栏和模态 UI;进程有 20 个 ObjectDB 和 1 个 Camera RID 泄漏警告。
|
||||
- `project/mobile_gesture_test.gd`:通过;覆盖长按、技能瞄准、快捷栏切换和物品触摸拖拽。
|
||||
- `project/mobile_ui_test.gd`:通过;覆盖平台配置、横屏 HUD、菜单和控件。
|
||||
- `project/mobile_window_host_test.gd`:通过;覆盖安全区、触摸桥和模态返回。
|
||||
- `project/game_camera_test.gd`:失败 3 项:期望的默认距离 1550cm、默认俯仰角 27° 和 `RESTORE_CAMERA` 恢复保存普通视图均未满足;因此相机不能标记为等价。
|
||||
|
||||
上述三项是修复前的基线失败,已在本轮修正;后续结果见本合同末尾的实现修复记录。
|
||||
|
||||
## Deep audit round 2026-09-21
|
||||
|
||||
### Fresh regression results (修复前基线)
|
||||
|
||||
- `mouse_controller_test.gd`、`mobile_gesture_test.gd`、`mobile_ui_test.gd`、`mobile_window_host_test.gd` 均退出码 0;覆盖拖放取消、长按/技能瞄准、横屏控件、安全区、触摸桥和模态返回。
|
||||
- `input_key_test.gd` 断言通过,但有 20 个 ObjectDB 和 1 个 Camera RID 泄漏。
|
||||
- `game_camera_test.gd` 仍失败 3 项:默认距离 1550cm、默认俯仰 27°、`RESTORE_CAMERA` 恢复保存普通视图。
|
||||
|
||||
### Static parity conclusion (修复前基线)
|
||||
|
||||
- WASD/触屏和基础鼠标拖放路径可运行,但输入按下/释放分散在不同 Godot 阶段,尚未形成 40250 `NEW_SetSingleDIKKeyState`/统一鼠标状态分派的同一时序。
|
||||
- 相机缩放、默认常量和恢复快照仍与参考不一致;事件相机的异常中断、相机/鼠标/IME 组合清理没有生产级证据。
|
||||
- IME 仍依赖 Godot LineEdit,没有可审计的候选窗口、字符/删除/Home/End/Return/Escape 回调链;移动端测试通过不等于 Windows 40250 输入链一致。
|
||||
|
||||
修复前本轮仍为 `PARTIAL`;随后已针对相机默认值、缩放和目标高度完成实现修复,整体合同仍保持 `PARTIAL`。
|
||||
|
||||
### Active EterLib core review
|
||||
|
||||
- `EterLib/Camera.cpp` 的 `CCamera` 维护普通/事件相机状态、Begin/EndDrag/Drag、滚轮和按比例 `Zoom(float ratio)`;相机管理器还负责当前相机、事件相机和恢复路径。当前 `game_camera.gd` 虽有同名交互入口,但使用固定距离步进,默认距离/俯仰和恢复快照已有 3 项测试失败,不能视为公式或状态机一致。
|
||||
- `EterLib/IME.cpp` 不只是文本框包装:它维护输入法启停、键盘捕获、组合字符串、候选/reading 窗口、code page、TSF 回调和字符串编辑。当前依赖 Godot `LineEdit`,没有对应的候选窗口、组合提交、编码页和回调生命周期;这是 Windows 输入链的明确缺口。
|
||||
- 因此本轮确认 `Camera.cpp/.h`、`IME.cpp/.h`、`Input.h`、`msctf.h` 已完成活跃源码静态核对,但实现仍为 `PARTIAL`,静态核对不等于运行时一致。
|
||||
|
||||
## 已确认差异和未验证项
|
||||
|
||||
1. 已统一参考端与当前端的相机默认常量、单位、比例缩放、滚轮阻尼、连续控制速度、相机目标高度和恢复快照;`game_camera_test.gd` 的修复前 3 项失败已通过。
|
||||
2. 建立鼠标按钮分派矩阵,覆盖 MOVE/SMART/CAMERA/AUTO/ATTACK/SKILL、Ctrl、中键、技能/物品/角色/地面优先级和每个拒绝分支。
|
||||
3. 增加硬件光标显隐、连续光标、相机拖拽结束、失焦、暂停和异常退出的组合测试。
|
||||
4. 为聊天/输入框补充 IME 适配合同:光标移动、Home/End、删除、Return/Escape、候选窗口和字符回调;不能以 LineEdit 存在代替功能等价。
|
||||
5. 增加生产 GameScene 级输入清理测试,确认切图、断线、重连、窗口失焦和移动端触摸取消后没有粘滞按键、相机拖拽或拖放状态。
|
||||
|
||||
## 下一轮验证入口
|
||||
|
||||
- 先定位 `game_camera.gd` 默认值和 `RESTORE_CAMERA` 失败的具体断言,按 40250 的 Camera 状态快照建立边界测试。
|
||||
- 再审计 `audio.bgm_ambience_3d`,核对 BGM、区域环境音、角色音效、3D 监听器和前后台恢复调用链。
|
||||
|
||||
## Active UserInterface IME bridge review
|
||||
|
||||
- `PythonIME.h`/`PythonIMEModule.cpp` 是 Windows IME 的完整桥:光标左右/Home/End/删除/设置位置、Create/Destroy、Tab/Return/Escape、`WM_CHAR`、候选/reading 更新、codepage 和输入模式都通过 `CPythonIME`/`EterLib::IME` 暴露给 Python。
|
||||
- 当前端 `LineEdit` 能完成基本 UTF-8 文本输入,但没有证明多字节 codepage、候选窗口、reading string、组合态删除、焦点切换和系统 IME 回调的状态顺序一致;因此不能把普通键盘 smoke test 视为 IME 等价。
|
||||
|
||||
## Static review evidence
|
||||
|
||||
已将 `PythonIME.h`、`PythonIMEModule.cpp` 纳入 active source 证据;相机/输入合同仍为 `PARTIAL`,需补真实输入法和失焦/取消清理测试。
|
||||
|
||||
## Implementation fix round 2026-09-21
|
||||
|
||||
### Reference chain
|
||||
|
||||
- `source/UserInterface/CameraProcedure.cpp::CCamera::Update`:距离边界为 200/2500cm,滚轮速度按帧应用并以 0.5 阻尼,目标高度按距离从 100cm 插值到 150cm。
|
||||
- `source/UserInterface/PythonApplication.cpp`:默认相机缩放速度为 `0.05`。
|
||||
- `source/UserInterface/PythonApplicationCamera.cpp`:`ZoomCamera` 使用 `1.0 + speed * direction` 的比例缩放。
|
||||
- `source/EterLib/Camera.cpp::CCamera::Wheel`:Win32 滚轮量按 0.3 resistance 转换为距离速度。
|
||||
|
||||
### Current implementation
|
||||
|
||||
- `project/game_camera.gd` 已将默认值固定为 1550cm/27°,加入 200/2500cm 对应的目标高度插值、0.05 比例键盘缩放、36cm/刻度的滚轮速度和 0.5 阻尼;事件相机锁定时不消费普通缩放输入。
|
||||
- `project/game_camera_test.gd` 已覆盖默认值、`RESTORE_CAMERA`、目标高度的最小/最大距离边界、滚轮缩放阻尼和键盘 0.05 比例。
|
||||
|
||||
### Verification
|
||||
|
||||
- `game_camera_test.gd`:通过;默认值、事件相机恢复、动态目标高度、滚轮阻尼和比例缩放均通过。
|
||||
- `input_key_test.gd`、`mobile_gesture_test.gd`、`mobile_ui_test.gd`、`mobile_window_host_test.gd`:通过;`input_key_test.gd` 仍报告既有的 20 个 ObjectDB 与 1 个 Camera RID 泄漏警告。
|
||||
|
||||
该修复只闭合了相机默认值和缩放/目标高度的已确认差异;IME、完整鼠标按钮分派、硬件/连续光标和异常清理仍保持 `PARTIAL`,不能据此宣称输入系统整体等价。
|
||||
@@ -0,0 +1,88 @@
|
||||
# world.dungeon_activity
|
||||
|
||||
## Scope
|
||||
|
||||
审计 `GC_DUNGEON` 目标点、地下城事件/计时、`DUNGEON_RESULT` 结果窗口、地图导航提示、镜头/屏幕效果,以及当前端额外提供的 Boss、排行和具体副本 helper。判断标准是当前端是否复现 40250 Windows 客户端的协议字段、目标点状态、到达/重复提示、结果展示和中断清理;服务端才拥有副本奖励、Boss 血量、排行和副本规则,当前端本地计算这些内容不能作为 40250 客户端 parity 证明。
|
||||
|
||||
## Reference call chain
|
||||
|
||||
- `UserInterface/PythonNetworkStreamPhaseGame.cpp`
|
||||
- `RecvDungeon` 读取 `TPacketGCDungeon`;`TIME_ATTACK_START` 分支在 40250 中不启动本地计时,`DESTINATION_POSITION` 读取两个坐标并调用 `CPythonPlayer::SetDungeonDestinationPosition`。
|
||||
- `PythonEventManager.cpp` 的 `DUNGEON_RESULT` 分支把九个整数交给 `interface.ShowDungeonResult`;客户端展示结果,不在此处结算奖励。
|
||||
- `UserInterface/PythonPlayer.cpp` / `PythonPlayer.h`
|
||||
- `SetDungeonDestinationPosition` 保存全局像素/厘米目标,立即调用 `AlarmHaveToGo`;更新时用整数 Manhattan 距离判断 `< 10000` 到达,未到达且超过 20 秒再次提示,并在到达时清空目标。
|
||||
- `AlarmHaveToGo` 创建 `effect/etc/compass/appear_middle.mse`,朝目标方向旋转罗盘提示。
|
||||
- `UserInterface/PythonEventManager.cpp` / `GameLib/GameEventManager.cpp`
|
||||
- 事件窗口负责结果 UI、等待和清理;`GameEventManager` 的相关路径是屏幕波动/运动效果,不是 Boss 或排行 authority。
|
||||
- `GameLib/MapManager.cpp` / `Packet.h` / `PythonNetworkStreamPhaseGame.h`
|
||||
- 提供地图/副本协议常量和包布局;副本目标与结果由服务端包驱动,客户端没有本地奖励、Boss HP 或排行榜结算。
|
||||
|
||||
## Current call chain
|
||||
|
||||
- native `EntityStore::apply` 已解析 `GC_DUNGEON` 的 subheader 和目标坐标;`M2Client::poll` 发出 `dungeon_event`,`M2Client::request_dungeon`/`ClassicSession::send_dungeon` 能发送请求。
|
||||
- `project/dungeon_state.gd` 已实现 `TIME_ATTACK_START` 不启动本地计时、目标坐标转换、立即提示、10,000 cm Manhattan 到达阈值、20 秒重复 alarm、到达清理和 `world_reset` 清理。
|
||||
- `project/ui/dungeon_result_ui.gd` 已接收九个字段并呈现击杀、隐藏目标、药水、复活、完成、时间和经验等结果;`game_scene.gd` 已连接 `dungeon_result_requested`。
|
||||
- `project/world_boss_system.gd`、`project/dungeon_ranking_system.gd`、`project/spider_dungeon_system.gd`、`project/flame_dungeon_razador_system.gd`、`project/endless_spire_dungeon_system.gd` 是当前端额外的本地规则/authority helper。40250 Windows 客户端没有对应的本地 Boss HP、掉落、排行或副本奖励实现,不能把这些 helper 的单元测试当成 parity PASS。
|
||||
|
||||
## Branch matrix
|
||||
|
||||
| 场景 | 40250 | 当前端 | 结论 |
|
||||
|---|---|---|---|
|
||||
| 地下城请求 | `__CanActMainInstance`/游戏阶段检查后发送对应请求;结果由服务端包驱动 | native 只做 `is_in_game` 和 session send;没有完整动作门控、请求去重和拒绝回滚证据 | PARTIAL |
|
||||
| `TIME_ATTACK_START` | `RecvDungeon` 消费包但分支为空,不凭客户端包启动计时 | `DungeonState` 记录 subheader 但不启动本地计时 | PASS(已覆盖的分支) |
|
||||
| 目标点回包 | 读取 x/y→`SetDungeonDestinationPosition`→立即 compass alarm | `GC_DUNGEON`→`dungeon_event`→坐标转换→`destination_changed` 和 `alarm_requested` | PARTIAL:协议/状态接近,但 alarm 没有在 `game_scene.gd` 接到实际 compass/effect 消费者 |
|
||||
| 目标点重复提示 | 未到达时每 20 秒 `AlarmHaveToGo`,使用参考 `.mse` 资源并按目标方向旋转 | `DungeonState` 每 20 秒发 `alarm_requested`,但当前没有实际资源效果调用 | GAP:信号存在,视觉副作用缺失 |
|
||||
| 目标到达 | Manhattan `< 10000` 后清除目标并停止提示 | 同阈值和清理信号已实现 | PASS(状态算法) |
|
||||
| 地图切换/断线 | 玩家状态重置并清理目标提示 | `world_reset` 清理目标;实际断线、重连、旧包乱序未完整验证 | PARTIAL |
|
||||
| 副本结果 | `DUNGEON_RESULT` 九个整数交给 `ShowDungeonResult`,仅展示服务端结果 | `QuestDialog` 发 `dungeon_result_requested`,`DungeonResultUI` 展示九字段 | PARTIAL:展示链存在,但真实 `DUNGEON_RESULT`→UI 回包和关闭/重复/中断未端到端覆盖 |
|
||||
| 奖励、Boss、排行 | 客户端不计算副本奖励、Boss HP、掉落或排行榜权威 | 本地 helper 直接推进 Boss 阶段/掉落/库存奖励和排行统计 | GAP(custom authority):与 40250 客户端职责不一致 |
|
||||
| 屏幕波动/地图事件 | `GameEventManager` 处理屏幕波动和运动事件 | 当前副本合同没有证明该引用链的实际资源/中断效果 | PARTIAL:不能用 Boss/排行 helper 代替原版事件效果 |
|
||||
|
||||
## Implementation-equivalence matrix
|
||||
|
||||
| 项目 | 状态 | 证据/限制 |
|
||||
|---|---|---|
|
||||
| Preconditions | PARTIAL | native 游戏阶段、包长度和目标字段检查存在;请求动作门控、重复请求、服务器拒绝和活动阶段限制未与 40250 等价证明 |
|
||||
| Branch structure | PARTIAL | dungeon subheader、目标、到达、结果主分支存在;alarm 没有实际 compass 消费者,结果回包和中断分支证据不足 |
|
||||
| Algorithms/formulas | PARTIAL | 目标坐标转换、10,000 cm Manhattan 阈值、20 秒间隔与参考一致;实际 `.mse` 方向/资源效果缺失,本地 Boss/排行公式属于额外 authority |
|
||||
| State transition order | PARTIAL | 包→native event→目标状态→HUD 的顺序接近;alarm 只到 signal,world reset/重连/旧包清理和结果窗口生命周期未闭合 |
|
||||
| Constants/units | PARTIAL | subheader 0/1、10,000 cm、20 秒和九字段结果存在;server cm→Godot world 的轴/地图基准需实机验证,Boss/排行常量不能视为 40250 客户端常量 |
|
||||
| Timing/event sources | PARTIAL | 目标初次与 20 秒 alarm、到达检测存在;实际 compass effect、结果重复包、断线/切图/重连和计时事件未覆盖 |
|
||||
| Resource/data sources | PARTIAL | 结果字段来自服务端包,目标坐标来自 `GC_DUNGEON`;参考 `appear_middle.mse` 没有实际接入,Boss/排行使用本地硬编码表 |
|
||||
| Protocol side effects | PARTIAL | `GC_DUNGEON` parser、请求和结果 UI 入口存在;未证明请求被正确门控,alarm/结果的完整 UI/音效/资源副作用不完整 |
|
||||
| Interruption/failure/cleanup | GAP | 目标 alarm、结果 UI、Boss/排行 helper 在拒绝、断线、乱序、地图切换、重复结果和重连时的恢复没有完整回归;本地 helper 可绕开服务器 authority |
|
||||
|
||||
## Regression evidence
|
||||
|
||||
- `project/dungeon_state_test.gd`:通过,覆盖目标状态、坐标、到达阈值和 alarm 计数;没有证明实际 compass `.mse` 消费者已连接。
|
||||
- `project/dungeon_result_test.gd`:通过,覆盖九字段结果窗口的本地展示;没有真实 `DUNGEON_RESULT` native 回包、重复结果、断线和关闭时序。
|
||||
- `project/test_dungeon_ranking_parity.gd`、`project/test_spider_dungeon_parity.gd`、`project/test_spider_dungeon_key_parity.gd`、`project/test_flame_dungeon_razador_parity.gd`、`project/test_endless_spire_dungeon_parity.gd`:通过,验证当前端自定义 helper;40250 客户端没有同等本地 authority,因此不能证明 Windows 客户端 parity。
|
||||
- `build/extension/net_entity_test`、`build/extension/net_classic_session_test`、`build/extension/net_classic_wire_test`:通过基础 parser/session/wire 检查;未覆盖 alarm 视觉副作用、目标乱序/旧包、结果 live UI、断线重连和 server rejection。
|
||||
|
||||
### Active GameLib dungeon-block review
|
||||
|
||||
- `GameLib/DungeonBlock.cpp`/`.h` 以资源 `CGraphicThing` 建立多个 `CDungeonModelInstance`,维护 deformable vertex buffer、bounding sphere、model intersection、static collision data、height instance、Update/Render/Shadow 和 Destroy;它是地图对象碰撞/渲染生命周期的一部分。
|
||||
- 当前副本 helper 与 dungeon HUD 主要实现目标、结果和本地状态,没有 40250 `DungeonBlock` 的多模型/碰撞/高度/阴影对象链;`DUNGEON_RESULT` 测试通过不能覆盖该地图对象资源语义。
|
||||
- 本轮完成 `DungeonBlock` 活跃源码静态核对,地下城合同继续为 `PARTIAL`。
|
||||
|
||||
## Status
|
||||
|
||||
## Deep audit round 2026-09-21
|
||||
|
||||
### Fresh regression results
|
||||
|
||||
- `dungeon_state_test.gd`、`dungeon_result_test.gd`、`test_dungeon_ranking_parity.gd`、`test_spider_dungeon_parity.gd`、`test_spider_dungeon_key_parity.gd`、`test_flame_dungeon_razador_parity.gd`、`test_endless_spire_dungeon_parity.gd` 均退出码 0;对应本地断言为 30/30、32/32、34/34、31/31、22/22。
|
||||
- `net_entity_test`、`net_classic_session_test`、`net_classic_wire_test` 均退出码 0。
|
||||
|
||||
### Static parity conclusion
|
||||
|
||||
- `TIME_ATTACK_START`、目标坐标转换、10,000 cm Manhattan 到达阈值和 20 秒 alarm 状态算法已接近参考实现;但 `alarm_requested` 没有实际消费 `appear_middle.mse`、方向旋转和音效的生产链。
|
||||
- `DUNGEON_RESULT` 九字段展示窗口只有本地 UI/假回包证据,尚未证明真实 native 回包到窗口的重复、关闭、断线和重连时序。
|
||||
- Boss、排行、蜘蛛/火焰/无限塔 helper 在本端直接维护阶段、掉落、奖励或统计;这些不是 40250 Windows 客户端 authority,不能把其单元测试计为 parity 通过。
|
||||
- 请求门控、拒绝回滚、旧包/乱序、地图切换和重连清理仍缺 live 证据。
|
||||
|
||||
### Status update
|
||||
|
||||
本轮仍为 `PARTIAL`;完成三个地下城/活动方向的合同审计并登记,未修改实现。
|
||||
|
||||
`PARTIAL`。当前端已经具备 `GC_DUNGEON` 目标点的主要 parser、坐标状态和与 40250 一致的 Manhattan/20 秒算法,也有结果窗口骨架;但 40250 的 compass `.mse` alarm 没有实际消费者,结果链缺少 live 回包时序证据,且 Boss、排行和具体副本 helper 是当前端本地 authority,不是 40250 Windows 客户端实现。本轮完成审计并记录,未修改实现。
|
||||
@@ -0,0 +1,210 @@
|
||||
# world.entity_spawn_state_sync
|
||||
|
||||
## Scope
|
||||
|
||||
审计 Actor 的 Add、AdditionalInfo、Add2、Update、Move、Delete、主角色切换、VID 重用、可见性和实体节点/网络数据同步。目标不是比较函数名,而是确认 40250 的字段、前置条件、分支、调用顺序、资源副作用和清理行为在当前客户端中保持相同;Godot 的节点/信号只是平台适配,不能掩盖网络实体状态的缺失。
|
||||
|
||||
## Reference implementation and evidence
|
||||
|
||||
- `../40250/Server Client TMP4/ClientVS22/source/UserInterface/NetworkActorManager.cpp`
|
||||
- `AppendActor`, `UpdateActor`, `MoveActor`, `RemoveActor`, `SetMainActorVID`, `__AppendCharacterManagerActor`, `__RemoveCharacterManagerActor`, `__RemoveDynamicActors`。
|
||||
- Add 时保存完整 `SNetworkActorData`;创建失败时删除数据行;主角色使用立即删除,远端 Delete 使用 `DeleteInstanceByFade`。
|
||||
- `UpdateActor` 的副作用顺序为 armor、weapon、hair、guild、affect、move speed、attack speed、alignment、PK、state flags。
|
||||
- 同 VID 重新创建时,只有 mount 状态发生切换才保留旧实例位置;否则使用新 Actor 的当前坐标。
|
||||
- `../40250/Server Client TMP4/ClientVS22/source/UserInterface/PythonNetworkStreamPhaseGameActor.cpp`
|
||||
- `RecvCharacterAppendPacket`:旧 Add 的两包链;PC/NPC 暂存,其他种族直接完成 Add。
|
||||
- `RecvCharacterAdditionalInfo`:合并暂存 Actor;VID 不匹配时记录错误,不新建 Actor。
|
||||
- `RecvCharacterAppendPacketNew`:Add2 完整填充字段,并在 `IsInvisibleRace` 时丢弃。
|
||||
- `RecvCharacterUpdatePacket`:传递 state、affect、装备、速度、guild、alignment、PK、mount。
|
||||
- `RecvCharacterDeletePacket`:删除 Actor 并清理相关附属对象。
|
||||
- `../40250/Server Client TMP4/ClientVS22/source/UserInterface/PythonCharacterManager.cpp`
|
||||
- `DeleteInstance` 立即移除;`DeleteInstanceByFade` 进入 dead instance list 后延迟销毁。
|
||||
- `../40250/Server Client TMP4/ClientVS22/source/UserInterface/InstanceBase.cpp`
|
||||
- `SCreateData` 承接 race、parts、移动/攻击速度、guild、state、affect、mount 等创建字段。
|
||||
|
||||
## Current implementation and evidence
|
||||
|
||||
- `extension/src/net/classic/classic_parser.cpp`
|
||||
- Add 的 PC/NPC 使用 `m_pending_actor` 等待 AdditionalInfo;非 PC/NPC 直接 `mut_spawn`。
|
||||
- Add2 现在先执行与 40250 相同的隐身种族过滤,再一次性构造完整 `Entity` 并调用 `mut_spawn_full`;旧 Add 的非 PC/NPC 也保留 `state_flag`、`affect_flag`。
|
||||
- Update 现在把 state、affect、装备、速度、guild、alignment、PK、mount 一次传入 `mut_char_update`,不再拆出独立 affect mutation。
|
||||
- pending 现在与 40250 一样只有一个静态暂存记录;后一个旧 Add 覆盖前一个,AdditionalInfo 只能完成最后暂存 VID。Delete 后迟到 AdditionalInfo、map reset 后迟到包仍需继续核对。
|
||||
- `extension/src/net/entity_store.cpp`
|
||||
- `apply(GC_CHARACTER_ADD/ADD2/UPDATE)` 现在保留 state、affect;Add2 还完整保存 empire、guild、alignment、PK、mount,并过滤三种隐身 race。
|
||||
- `apply(GC_CHAR_ADD_INFO)` 现在必须命中已有 Entity,未知 VID 只记录忽略,不再通过 `touch` 创建部分 Entity。
|
||||
- `mut_spawn_main` / generic `GC_MAIN_CHARACTER` 现在统一产生一次 `MainSet` 边沿;首次主角为 `Spawn -> MainSet`,重复主角包仅为一次 `MainSet`,不再依赖重复事件或场景轮询。
|
||||
- `project/net_world.gd`
|
||||
- `_on_spawn` 会替换旧节点;已按 40250 改为仅在 mount 状态切换时保留旧 global position,普通重建使用新包坐标,并始终使用新包朝向。
|
||||
- `_fading` 只跟踪旧节点的 blend-out;收到同 VID 新 Spawn 时会立即解除门控并重建 live 节点,旧 tween 独立完成。
|
||||
- `_on_info` 只更新已有节点,不会修复 generic AdditionalInfo 误创建的部分节点。
|
||||
- `_on_main_set` / `set_local_vid` 没有完全复刻 `SetMainActorVID` 对远端 Actor 字典和动态实例的清理范围。
|
||||
- `extension/src/net/m2_client.cpp`
|
||||
- classic Add2 现在由 `mut_spawn_full` 产生一条完整 Spawn,再由 `pump_classic` 统一向 Godot 发出;不会再拆成 Add2 Spawn + Info + affect 三条事件。
|
||||
|
||||
## Branch matrix
|
||||
|
||||
| 场景 | 40250 行为 | 当前端行为 | 结论 |
|
||||
|---|---|---|---|
|
||||
| 旧 Add 新 VID | 保存 Add 字段;PC/NPC 等待 AdditionalInfo,其他种族直接 AppendActor | PC/NPC 使用单个 static-equivalent pending;非 PC/NPC 现在完整保存 state/affect | PARTIAL |
|
||||
| AdditionalInfo 命中 | 合并暂存记录后只创建完整 Actor | classic parser 命中 pending 后完整 Spawn;generic apply 命中已有 Entity 后发一次 Info | PARTIAL |
|
||||
| AdditionalInfo 未命中 | 记录错误,不创建 Actor | classic parser 与 generic apply 均不创建 Entity | MAPPED |
|
||||
| Add2 新 VID | 隐身种族直接丢弃;其他分支一次性填满字段后 AppendActor | 两条 parser/apply 路径均过滤隐身 race,并一次性填满字段后 `mut_spawn_full` | PARTIAL |
|
||||
| Update 已有 VID | 按固定顺序更新装备、guild、affect、速度、alignment、PK、state、mount | shared mutator 一次更新完整字段并发一次 Info;平台渲染副作用仍需核对 | PARTIAL |
|
||||
| Update 未知 VID | 记录错误,不创建 | mutator 丢弃;generic update 丢弃 | MAPPED(字段完整性仍需补测) |
|
||||
| Delete 远端 | `DeleteInstanceByFade`,并清理附属引用 | Godot 远端 fade + queue_free;同 VID 新 Spawn 可立即建 live replacement | PARTIAL:dead-list 与 Godot 双节点短时并存仍是平台适配 |
|
||||
| Delete 主角色 | 立即删除 | main/local 由 game scene 和 net world 分层清理 | PARTIAL |
|
||||
| 同 VID 重用 | 非 mount 切换使用新坐标;mount 切换可保留旧位置,朝向使用新包 | `_on_spawn` 已按 mount 状态决定是否保留位置,朝向统一取新包;淡出期间允许立即替换 live 节点 | PARTIAL:旧 dead-list/tween 的跨帧可见性仍是平台适配 |
|
||||
| Main VID 切换 | 清空 Actor 字典、重置位置,并清理动态实例 | 只设置 main/local VID 并移除本地节点;通常依赖 map reset | PARTIAL |
|
||||
| 可见性 | distance、wall、AFFECT_SHOW_ALWAYS、动态实例规则 | data/node 两层可见性和 fade 已有对应测试 | MAPPED(frame/淡出时序为平台适配) |
|
||||
|
||||
## Equivalence matrix
|
||||
|
||||
| 项目 | 状态 | 证据/缺口 |
|
||||
|---|---|---|
|
||||
| Preconditions | MAPPED | unknown AdditionalInfo/update 不创建;Add/Add2 隐身 race 在 parser/apply 两条入口均提前丢弃 |
|
||||
| Branch structure | PARTIAL | Add/AdditionalInfo/Add2/Update 的隐身、PC/NPC、主角色、未知 VID 和交错旧 Add 已覆盖主要分支;Delete/map reset 后迟到包仍缺 fixture |
|
||||
| Algorithms/formulas | PARTIAL | 可见距离和 EntityStore tick 有对应实现;同 VID 的 mount 位置保留和淡出期间立即重建条件已对齐,旧 dead-list/tween 的跨帧并存仍是平台适配 |
|
||||
| State transition order | PARTIAL | Add2 已变为单次完整 Spawn,Update 已合并为一次 shared mutator;渲染桥接的逐字段副作用顺序仍需验证 |
|
||||
| Data completeness | MAPPED | Add/Add2/Update/AdditionalInfo 的协议字段已落入 EntityStore,包含 state/affect/empire/guild/alignment/PK/mount |
|
||||
| Constants/units | PARTIAL | VID、位置、yaw、移动速度和 mount 标志要在 Add/Update/Move 组合包中逐字段验证 |
|
||||
| Timing/event sources | PARTIAL | 40250 manager update/dead list 与 Godot frame、signal、queue_free 的交错未完成证明 |
|
||||
| Resource/data sources | PARTIAL | 40250 `SCreateData`/`CInstanceBase` 与当前 model factory 的失败、替换、升级 ownership 未完全覆盖 |
|
||||
| Protocol side effects | PARTIAL | Add/Info/Update/Delete 入口已映射;Add2/unknown AdditionalInfo 已对齐,渲染层删除/更新副作用仍需证明 |
|
||||
| Interruption/failure/cleanup | PARTIAL | 模型创建失败、升级中 Delete、pending stale、Delete/map reset 后迟到包和主 VID 清理仍需自动测试 |
|
||||
|
||||
## Required tests
|
||||
|
||||
- 旧 Add -> AdditionalInfo 的命中、未知 VID、交错两个 Add、Delete 后迟到 AdditionalInfo、map reset 后迟到 AdditionalInfo。
|
||||
- Add/Add2 的完整字段快照:race/type/position/yaw/speeds/state/affect/empire/guild/alignment/PK/parts/mount/name。
|
||||
- 隐身种族在旧 Add 和 Add2 中均不产生 Entity/node。
|
||||
- Update 前后 state、affect、装备、guild、速度、alignment、PK、mount 的字段和事件顺序。
|
||||
- Add -> Info -> Update -> Delete -> 同 VID Add,分别覆盖普通远端、mount 切换、非 mount 切换、淡出未完成和模型升级中;淡出未完成时新 Add 必须立即建 live replacement。
|
||||
- 主 VID 切换时旧 Actor、HP、尾标、target、hover、模型和动态实例的清理顺序。
|
||||
- 模型工厂返回失败、map reset 中断、重复 Delete、重复 Add 和跨帧 signal 的一致性。
|
||||
|
||||
## Deep audit round 2026-09-20
|
||||
|
||||
本轮补做了参考实现的创建/重建/可见性时序核对,并运行实体层与 Godot 节点层回归:
|
||||
|
||||
1. 40250 `CNetworkActorManager::Update()` 在 `CPythonNetworkStream::OnProcess()` 中每次网络处理调用 `__UpdateMainActor()`,再遍历 Actor 字典;当前 `NetWorld._process()` 已按每个 Godot frame 调用 `_update_visibility()`,不再使用 100ms 节流。`net_world_vis_test.gd` 已覆盖远端进入范围、单次建节点、离开范围淡出、数据层保留和重新进入,当前通过;旧版本的 4 项可见性失败已不再复现。
|
||||
2. 参考 `__AppendCharacterManagerActor()` 对已有 VID 重建时读取旧坐标,但只有从骑乘到非骑乘或反向切换才覆盖新创建数据的坐标;当前 `_on_spawn()` 已改为只在 mounted 布尔状态切换时保留旧位置,普通同 VID Add/换装使用新坐标,朝向始终使用新包。
|
||||
3. 参考显式 `RemoveActor()` 对普通远端 actor 走 `DeleteInstanceByFade`,主 actor 走立即 `DeleteInstance()`;`_on_despawn()` 现已按 `_main_vid` 区分这两个分支。`_on_spawn()` 仍在同 VID 新 Spawn 时解除 `_fading` 并立即创建 live replacement,旧 tween 只负责旧节点。重复 Delete 和跨帧双节点生命周期仍需继续核对。
|
||||
4. 本轮已修复 Add/Add2/Update 的字段和分支差异:classic parser 的旧 Add 非 PC/NPC、Add2、Update 与 generic `EntityStore::apply` 均通过完整字段结构或 shared mutator 保留 state/affect 等字段;Add2 过滤 20025/20038/20039;未知 generic AdditionalInfo 不再 `touch()` 创建半实体。随后又将 classic pending 从 keyed map 收敛为 40250 的单个 static-equivalent 槽,并覆盖交错 Add;Delete/map reset 后迟到 AdditionalInfo 和渲染层副作用顺序仍需验证。
|
||||
5. `SetMainActorVID` 参考实现会清空 NetworkActor 字典、重置主坐标;当前 `NetWorld::_on_main_set()` 只改 `_main_vid` 并重刷名字色,旧远端节点、动态实例和 UI 引用的清理依赖外层 map reset,主 VID 切换的独立语义未证明。
|
||||
|
||||
## Evidence run
|
||||
|
||||
- `./build/extension/net_entity_test`:PASS,证明 EntityStore 的部分 Add/Info/Update/Delete、字段和状态队列夹具仍可运行。
|
||||
- `godot --headless --path project --script net_world_vis_test.gd`:PASS,覆盖主角移动后可见实体创建、远端节点一次性构建、离开范围剔除、`entity_removed` 一次性事件、强制可见实体和重新进入。
|
||||
- `project/gamescene_test.gd`:PASS,但该测试验证的是场景装配/实体镜像/初始定位,不覆盖上述可见性节流和同 VID 淡出竞态。
|
||||
- 因此本合同继续 `PARTIAL`,可见性和字段回归已通过,但 Delete/map reset 后迟到包、主角色清理和渲染层副作用顺序仍未完全证明。
|
||||
|
||||
### Active UserInterface actor-sync review
|
||||
|
||||
- `NetworkActorManager.h` 保存每个 actor 的服务器时间、目标位置/持续时间、当前/目标坐标、owner/victim、affect flags 和 `UpdateActor/MoveActor/Update`;`PythonCharacterManager` 负责将其映射到可渲染实例和删除列表。
|
||||
- 当前 `EntityStore`/`NetWorld` 以 packet 快照和 Godot interpolation 驱动 remote view,基础移动同步可用,但没有参考端 actor manager 的 server-time movement、owner/victim 和统一删除/fade/update transform 入口;这也是远端动作事件、VID 复用和跨图旧包清理的剩余风险。
|
||||
- 本轮完成 NetworkActorManager 与角色 manager 接口静态核对,实体同步合同继续为 `PARTIAL`。
|
||||
|
||||
## Implementation fix round 2026-09-22T04:30Z
|
||||
|
||||
沿 40250 `NetworkActorManager::__AppendCharacterManagerActor()` 和
|
||||
`CPythonNetworkStream::RecvMainCharacter()` 复核同 VID 重建及主实例注册边沿,补齐了两处已证实差异:
|
||||
|
||||
- `NetWorld._on_spawn()` 现在只在旧/新实例的 mounted 布尔状态发生变化时保留旧 global position;普通重生、换装或 VID 重用使用新 Add 数据的坐标。无论是否上下马,旋转都使用新包的 `angle_deg`。
|
||||
- `EntityStore::mut_spawn_main()` 和 `EntityStore::apply(GC_MAIN_CHARACTER)` 现在在首次主角建立时按 `Spawn -> MainSet` 发出一次注册边沿,重复主角包只发一次 `MainSet`,消除了旧实现的缺失/重复通知。
|
||||
|
||||
新增回归:
|
||||
|
||||
- `project/net_world_respawn_test.gd`:普通同 VID 重建坐标/朝向、上下马重建保留旧位置但使用新朝向。
|
||||
- `extension/tests/net_entity_test.cpp`:首次主角注册和重复主角包的 `MainSet` 次数。
|
||||
|
||||
修复前上述测试分别能够复现普通重建位置/朝向错误、上下马朝向错误以及 `MainSet` 缺失/重复;修复后
|
||||
`build/extension/net_entity_test` 和 `net_world_respawn_test.gd` 通过。Add2 隐身种族、AdditionalInfo 未命中、旧 Add 字段完整性已在后续修复轮补齐;淡出期间同 VID 重用和主 VID 切换清理当时仍保持 `PARTIAL`。
|
||||
|
||||
## Implementation fix round 2026-09-22T05:10Z
|
||||
|
||||
按 40250 `PythonNetworkStreamPhaseGameActor::RecvCharacterAppendPacket*`、
|
||||
`RecvCharacterAdditionalInfo`、`RecvCharacterUpdatePacket` 和
|
||||
`NetworkActorManager::AppendActor/UpdateActor` 的完整字段链复核并修复:
|
||||
|
||||
- classic 旧 Add 的即时实体现在以完整 `Entity` 写入 state/affect;PC/NPC pending 合并继续一次完整 Spawn。
|
||||
- classic Add2 和 generic Add2 都执行 `IsInvisibleRace` 等价过滤,并一次性保存 name、parts、state、affect、empire、guild、alignment、PK、mount 等全部字段。
|
||||
- classic/generic Update 统一经 `mut_char_update` 更新 state、affect、装备、速度、guild、alignment、PK、mount,并在发生变化时只发一次 Info;mount 变化仍发 mount cue。
|
||||
- generic AdditionalInfo 仅接受已存在的 actor;未知 VID 被忽略,不创建半实体,并补齐 empire 和字段变更检测。
|
||||
|
||||
新增/扩展回归:
|
||||
|
||||
- `extension/tests/net_entity_test.cpp`:Add/Add2/Update 全字段、隐身 Add2、未知 AdditionalInfo、单次 Spawn/Info。
|
||||
- `extension/tests/net_classic_session_test.cpp`:40250 Add2 全字段、三种隐身 race、未知 AdditionalInfo 和清理。
|
||||
|
||||
修复前新增回归可复现 state/affect 丢失、Add2 元数据丢失、隐身实体错误出现、unknown AdditionalInfo 半实体和事件计数差异;修复后两个 native 测试均通过。淡出期间同 VID 重用、pending 交错清理、主 VID 切换清理和 Godot 可见性时序仍保持 `PARTIAL`。
|
||||
|
||||
## Implementation fix round 2026-09-22T05:35Z
|
||||
|
||||
按 40250 `RemoveActor -> DeleteInstanceByFade` 与后续 `AppendActor ->
|
||||
__AppendCharacterManagerActor` 的实例生命周期复核并修复:
|
||||
|
||||
- `_on_despawn()` 的旧节点继续渐隐;如果渐隐未结束就收到同 VID 的新 Spawn,`_on_spawn()` 解除旧 `_fading` 门控并立即创建新的 live 节点,符合参考端 dead-list 不占用 `GetInstancePtr()` 的行为。
|
||||
- 旧 fade tween 只释放旧节点,不会删除新节点;新节点使用新的权威位置和旋转。
|
||||
|
||||
扩展 `project/net_world_respawn_test.gd`,修复前可复现淡出期间同 VID Spawn 被直接丢弃;修复后覆盖立即重建、位置以及旧 tween 完成后的新节点存活。
|
||||
|
||||
## Implementation fix round 2026-09-22T05:55Z
|
||||
|
||||
按 40250 `PythonNetworkStreamPhaseGameActor::RecvCharacterAppendPacket` 的
|
||||
`static SNetworkActorData s_kNetActorData` 和
|
||||
`RecvCharacterAdditionalInfo` VID 匹配分支复核并修复:
|
||||
|
||||
- `ClassicParser::m_pending_actor` 从 keyed map 改为单个 `std::optional<Entity>`,后一个 PC/NPC 旧 Add 覆盖前一个,避免当前端比 40250 多出可交错完成的 pending actor。
|
||||
- AdditionalInfo 只有在 VID 命中最后暂存记录时才调用一次 `mut_spawn_full`;被覆盖 VID 的迟到 AdditionalInfo 只记录未处理 header,不创建实体。
|
||||
|
||||
新增 `net_classic_session_test.cpp` 的交错旧 Add 回归,修复前可复现两个 pending 和第一个 VID 错误生成;修复后 `net_classic_session_test` 与 `net_entity_test` 通过。Delete 后迟到 AdditionalInfo、map reset 后迟到包、主 VID 清理和渲染副作用顺序仍保持 `PARTIAL`。
|
||||
|
||||
## Evidence reconciliation 2026-09-22T06:15Z
|
||||
|
||||
重新以当前工作树运行可见性回归:`net_world_vis_test.gd` 通过。当前 `_process()` 已逐帧执行 `_update_visibility()`,与 40250 `CNetworkActorManager::Update()` 的检查频率一致;因此旧审计中关于 100ms 节流和 4 项可见性失败的描述已更新为历史发现,不再作为当前缺陷。Add2 的当前 pump 也已改为单次完整 Spawn,旧的“Spawn + Info + affect 拆分”描述同步更正。
|
||||
|
||||
## Implementation fix round 2026-09-21T07:08Z
|
||||
|
||||
按 `CNetworkActorManager::__RemoveCharacterManagerActor` 的主/远端分支修正
|
||||
`NetWorld._on_despawn()`:`_main_vid` 现在走立即删除,普通远端 actor 继续走
|
||||
`DeleteInstanceByFade` 等价的渐隐路径。新增 `net_world_respawn_test.gd` 断言主角
|
||||
不会进入 `_fading`,同时保留普通远端 actor 的同 VID 重建和旧 tween 隔离验证。
|
||||
|
||||
`net_world_respawn_test`、`net_world_reset_test`、`net_world_vis_test`、
|
||||
`fly_target_lifecycle_test`、`netbridge_test` 均通过。主 VID 切换、Delete/map reset
|
||||
后的迟到包和完整渲染副作用顺序仍保持 `PARTIAL`。
|
||||
|
||||
## Implementation fix round 2026-09-21T07:15Z
|
||||
|
||||
继续核对 `RecvCharacterAdditionalInfo` 的单槽暂存生命周期时发现:旧 Add
|
||||
等待 AdditionalInfo 期间如果先收到 Delete,或随后进入 Loading/warp,参考端的
|
||||
静态 `SNetworkActorData` 已失效;当前端原先仍保留 `m_pending_actor`,迟到的
|
||||
AdditionalInfo 可以错误生成旧角色。现已在匹配 VID 的 Delete 中清槽,并新增
|
||||
`ClassicSession::reset_for_map_change()`,将 parser 暂存和 EntityStore 一起清理。
|
||||
|
||||
新增回归覆盖 Delete 后 AdditionalInfo、map reset 后 AdditionalInfo;
|
||||
`net_classic_session_test`、`net_entity_test` 和 `mtgodot` 构建通过。主 VID
|
||||
切换、跨连接迟到 Update/Move 和渲染副作用顺序仍保持 `PARTIAL`。
|
||||
|
||||
## Status
|
||||
|
||||
`PARTIAL`。本轮已完成 Add/Add2/Update/AdditionalInfo 的主要协议字段、前置条件、事件粒度、单槽 pending、逐帧可见性和淡出期间同 VID 立即重建;仍必须补齐 Delete/map reset 后迟到包、主角色清理和 Godot 渲染副作用顺序的独立证明,不能宣称与 40250 完全等价。
|
||||
|
||||
## Implementation fix round 2026-09-21 — SetMainActorVID lifecycle boundary
|
||||
|
||||
本轮继续核对 40250 `CNetworkActorManager::SetMainActorVID`。参考实现收到新的主角色
|
||||
VID 时会重置主坐标并清空 `m_kNetActorDict`,旧角色的 live instance、目标和临时 actor
|
||||
状态不会带入新的主角色生命周期。当前端此前的 `NetWorld._on_main_set()` 只更新
|
||||
`_main_vid` 并重算名字颜色;如果没有先经过完整 `world_reset`,旧远端节点、target/
|
||||
hover、HP/击退同步、伤害队列和聊天尾标仍可能残留。
|
||||
|
||||
现在主 VID 发生实际变化时,`_on_main_set()` 会先解除目标特效,立即删除所有旧 live
|
||||
actor,清空模型升级队列、sync-push/push、伤害队列、聊天尾标和渐隐索引,再注册新的
|
||||
主 VID;相同 VID 的重复通知不触发清理。新增 `net_world_main_switch_test.gd` 覆盖旧
|
||||
远端节点、target/hover 和临时表现状态在切换边沿全部清除,并与
|
||||
`net_world_reset_test.gd`、`net_world_respawn_test.gd`、`net_world_vis_test.gd` 一起通过。
|
||||
|
||||
本轮只闭合了 Godot presentation 层对应 `SetMainActorVID` 的清理边沿;EntityStore 中
|
||||
已有快照在切换后重新可见的生成时序、旧渐隐节点的最终销毁、跨连接迟到 Move/Update
|
||||
以及完整渲染副作用顺序仍保持 `PARTIAL`。
|
||||
@@ -0,0 +1,104 @@
|
||||
# world.environment_weather —— 天空、环境、光照、昼夜、雪和天气
|
||||
|
||||
状态:`PARTIAL`
|
||||
|
||||
本合同检查 40250 的 `.msenv` 环境加载、背景/角色光照、线性雾、天空六面、云层、滤色、lens flare、风、雪粒子和地图环境生命周期。判断标准是同一份环境数据是否经过同一类调用链、在同一时序产生相同的可见状态和清理副作用;“字段已解析”或“测试能创建粒子”不能代替渲染与正式场景接入等价。
|
||||
|
||||
## 40250 参考调用链
|
||||
|
||||
- `GameLib/MapOutdoorRender.cpp` / `MapOutdoorRenderHTP.cpp` / `MapOutdoorRenderSTP.cpp`
|
||||
- `OnSetEnvironmentDataPtr` 将环境数据分派到 screen filter、SkyBox 和 lens flare;渲染顺序是天空、云、环境开始、地形/角色/水/雪/特效、环境结束和 lens flare。
|
||||
- `OnBeginEnvironment` 将 `.msenv` 的 fog near/far/color、背景方向光和角色方向光分别应用到 SpeedTree、terrain patch 和角色渲染;风强度进入森林渲染器。
|
||||
- `SetEnvironmentSkyBox` 使用 scale、gradient、texture render mode、六面纹理、云纹理、云高度/缩放/颜色/滚动速度,并通过 SkyBox transition 更新环境。
|
||||
- `GameLib/SnowEnvironment.cpp` / `SnowParticle.cpp`
|
||||
- 雪环境由专用粒子池、相机/世界范围、风/重力和粒子生命周期驱动,随 outdoor 渲染阶段更新和绘制,不是一次性创建的通用屏幕粒子节点。
|
||||
- `Client/Eternexus/root/servercommandparser.py` / `game.py`
|
||||
- 服务器 `xmas_snow 0/1` 经 `ServerCommandParser` 进入 `__XMasSnow_Enable`,先按 `__IsXMasMap` 过滤,再调用 `background.EnableSnow(0/1)`;该命令不是按地图名称自动推导天气。
|
||||
- `EterLib/EnvironmentMap.cpp`
|
||||
- 环境对象作为 outdoor 环境资源的生命周期边界参与加载/卸载;环境切换必须同步释放旧天空、雾、滤色、lens flare 和粒子状态。
|
||||
|
||||
## 当前端实现映射
|
||||
|
||||
- `formats/environment.*` / `extension/src/environment_builder.*`
|
||||
- 已解析 `.msenv` 的方向光、fog、sky gradient/六面/云、filter 和 lens flare 字段;`apply_environment` 已创建背景 `Sun`、角色 `CharacterLight` 和 `WorldEnvironment`,texture-mode sky 已用 shader 采样六面和滚动云层。
|
||||
- filter、lens flare、wind 目前主要保存在 `WorldEnvironment` metadata;没有对应运行时后处理、光晕节点或 SpeedTree 风更新。fog 使用 Godot depth fog,并主动 clamp/push begin/end、curve/density 与参考端 D3D 线性雾不相同。
|
||||
- `extension/src/metin2_world.cpp` / `project/game_scene.gd`
|
||||
- `.msenv` 在正式地图加载时接入 `Metin2World`,世界卸载会移除环境节点;GameScene 会优先采用 `.msenv` 环境,避免兜底光照覆盖它。
|
||||
- 生产场景创建 `fx/weather.gd`,并由 `EntityStore::apply_server_command -> M2Client.weather_changed -> GameScene._on_weather_changed` 接收 `xmas_snow`;场景按 40250 四张排除地图过滤,并在换图后重新应用当前开关。
|
||||
- `WeatherDayNightSystem` 仍未实例化到正式场景;它的按地图自动天气/昼夜预设不作为 40250 生产行为。
|
||||
- `project/fx/weather.gd`
|
||||
- 使用跟随相机的单个 `GPUParticles3D` 实现 snow/rain/none,粒子数量、重力、速度和颜色可切换;它没有 40250 `SnowEnvironment/SnowParticle` 的池、风向、范围、渲染阶段或资源失败回退链。
|
||||
- `project/weather_daynight_system.gd`
|
||||
- 是当前端独立的昼夜/地图天气 helper,能计算 phase、天气和光照字典;但其地图预设与 40250 `.msenv` 静态环境不是同一数据源,且正式 GameScene 未消费它。
|
||||
|
||||
## 分支等价矩阵
|
||||
|
||||
| 40250 判据/副作用 | 当前实现 | 结论 |
|
||||
|---|---|---|
|
||||
| `.msenv` 解析、地图加载、环境节点创建和卸载 | `formats/environment` + `Metin2World::load_map` + `apply_environment` | 基础链路通过;复杂地图/重入清理仍需扩展证据 |
|
||||
| 背景/角色两套方向光和可见层 | `Sun` layer 1、`CharacterLight` layer 2 | 部分等价;光照色/能量和角色渲染消费仍需逐场景比对 |
|
||||
| fog near/far/color、density/linear 分支、terrain/HTP/STP 使用 | Godot depth fog,begin/end 被 clamp,curve=0.5、density=0 | 不等价;参考的线性 D3D 雾公式和 LOD 分支没有 1:1 保持 |
|
||||
| 六面天空、gradient、scale、texture mode、cloud plane/UV | Godot sky shader/ProceduralSkyMaterial,六面和云测试通过 | 部分等价;shader 采样与参考 quad/transition 仍需像素和边界比对 |
|
||||
| screen filter alpha blend 和 lens flare | 字段只写 metadata,无运行时后处理/flare 渲染节点 | GAP |
|
||||
| 风强度进入森林/树木更新 | 固定 `msenv_wind_strength=0.2` metadata,无 SpeedTree/树节点风更新 | GAP |
|
||||
| SnowEnvironment/SnowParticle 池、范围、风/重力和 outdoor render 时序 | 单个相机跟随 GPUParticles3D | 部分等价;粒子可切换不代表池/时序/清理等价 |
|
||||
| 服务器 `xmas_snow 0/1`、`__IsXMasMap` 过滤和雪开关 | typed command -> `weather_changed` -> GameScene map filter -> `Weather.set_weather` | 部分等价;命令/过滤时序已对齐,但粒子池/渲染阶段不是 SnowEnvironment 1:1 |
|
||||
| `.msenv` 静态地图环境 vs 昼夜/天气切换 | 正式链消费服务器 `xmas_snow`;`WeatherDayNightSystem` helper 仍独立存在且不驱动生产地图 | 部分等价;40250 的 `DayMode/PRESERVE_DayMode` 和环境切换仍未接入 |
|
||||
| 地图切换、暂停、重载和失败清理 | world 环境节点 queue_free;天气节点未按地图环境重置/验证 | 部分等价;缺少环境/粒子/云资源失败和快速切图组合证据 |
|
||||
|
||||
## 已执行测试
|
||||
|
||||
- `project/environment_test.gd`:通过;A1 `.msenv`、双方向光、WorldEnvironment、云纹理和 wind/filter metadata 可读取。
|
||||
- `project/skybox_test.gd`:通过;CapeDragonHead/DawnMistWood texture-mode 六面天空、云层、scale 和 UV speed 断言通过。
|
||||
- `project/p9_test.gd`:通过;通用 weather snow/rain/none 粒子开关路径通过,但不是参考雪环境池测试。
|
||||
- `project/test_weather_daynight_parity.gd`:22 passed、0 failed;仅覆盖 `WeatherDayNightSystem` helper 的默认时相、时间推进、天气预设和信号,不覆盖正式 GameScene 接入或 40250 `.msenv` 渲染。
|
||||
- `extension/tests/net_entity_test.cpp` / `build/extension/net_entity_test`:通过;覆盖 `GC_CHAT/COMMAND` 的 `xmas_snow 1/0` typed event 顺序。
|
||||
- `project/gamescene_test.gd`:通过;覆盖正式 GameScene 对 eligible map、四张 40250 排除地图和关闭命令的雪开关处理。
|
||||
- `project/netbridge_test.gd`:通过;确认 `M2Client.weather_changed` 信号已注册。
|
||||
|
||||
## Audit evidence round 2026-09-22T09:30Z
|
||||
|
||||
- 重新运行 `environment_test.gd`、`skybox_test.gd`、`p9_test.gd` 和 `test_weather_daynight_parity.gd`,结果分别通过、通过、通过、22/22 通过。
|
||||
- `environment_test.gd` 实际加载 A1 `.msenv` 地图,`skybox_test.gd` 实际加载 CapeDragonHead/DawnMistWood 六面天空;这些结果确认 `.msenv`/天空基础链路仍可用,但不改变 filter、lens flare、wind、线性 fog 和 SnowEnvironment 池的差异结论。
|
||||
- `test_weather_daynight_parity.gd` 仍然只实例化独立 `WeatherDayNightSystem`;对照 `GameScene::_build_world` 当前只创建 `fx/weather.gd`,未发现正式地图加载时消费该 helper 的调用边沿,因此不能将 22/22 记为生产天气接入通过。
|
||||
- 本轮修复了服务器 `xmas_snow` 的命令接入和正式场景地图过滤;合同继续保持 `PARTIAL`,下一实现入口是 `DayMode/PRESERVE_DayMode`、SnowEnvironment 池以及已确认的 filter/lens flare/wind GAP。
|
||||
|
||||
## Deep audit round 2026-09-21
|
||||
|
||||
### Fresh regression results
|
||||
|
||||
- `environment_test.gd`、`skybox_test.gd`、`p9_test.gd` 均退出码 0;真实 A1/CapeDragonHead/DawnMistWood 环境、六面天空、云参数和基础天气切换断言通过。
|
||||
- `test_weather_daynight_parity.gd` 22/22 通过,但它验证的是独立 `WeatherDayNightSystem` helper,不证明正式 `GameScene` 已实例化并消费天气系统。
|
||||
|
||||
### Static parity conclusion
|
||||
|
||||
- `.msenv` 解析、背景/角色光、SkyBox 和地图环境节点的基础链路存在;filter、lens flare、wind 仍主要停留在 metadata,未形成 40250 的运行时后处理、光晕和树木风更新。
|
||||
- Godot depth fog 的 clamp/curve/density 与 40250 线性 D3D 雾不等价;六面天空/云 shader 有结构测试,但没有与参考 transition/render order 的像素 oracle。
|
||||
- 生产场景已接入 40250 的服务器 `xmas_snow` 命令和地图排除过滤,但没有接入 `DayMode/PRESERVE_DayMode`;单个 GPUParticles3D 也不等价于 SnowEnvironment/SnowParticle 池和 outdoor render 时序。
|
||||
|
||||
本轮仍为 `PARTIAL`,完成服务器天气命令的实现和第三轮环境/天气证据登记。
|
||||
|
||||
### Implementation fix round 2026-09-22T10:00Z
|
||||
|
||||
- 修复前 `gamescene_test.gd` 的生产链回归无法让 `xmas_snow=1` 启用雪;native command-bus 也没有天气事件。
|
||||
- 按 40250 `ServerCommandParser.__XMasSnow_Enable -> game.__XMasSnow_Enable -> background.EnableSnow`,新增 `EntityStore::WeatherChanged` typed event、`M2Client.weather_changed(bool)` 信号,以及 `GameScene._on_weather_changed -> _apply_server_weather`。
|
||||
- `_apply_server_weather` 复刻 `__IsXMasMap` 的四张排除地图:`metin2_map_n_flame_01`、`metin2_map_n_desert_01`、`metin2_map_spiderdungeon`、`metin2_map_deviltower1`;地图切换后重新计算开关,避免服务器命令状态污染新地图。
|
||||
- 修复后 native `net_entity_test`、`gamescene_test.gd`、`netbridge_test.gd`、`p9_test.gd`、`environment_test.gd` 均通过。Godot 粒子仍只是平台适配,SnowEnvironment 池、DayMode、filter/lens flare/wind 和线性 fog 差异继续保留。
|
||||
|
||||
### Active EterLib environment-render review
|
||||
|
||||
- `EterLib/SkyBox.cpp` 实际维护天空六面纹理、天空/云纹理、gradient、scale、scroll、天气/颜色过渡和 render 顺序;`ScreenFilter.cpp`、`LensFlare.cpp` 是独立的屏幕滤镜与镜头光晕渲染对象,不只是环境字典字段。
|
||||
- 当前 `WorldEnvironment`/sky shader 能提供天空和雾的部分视觉结果,但 `filter`、lens flare 和风更新主要停留在 metadata,没有对应运行时后处理/光晕对象;Godot depth fog 的 clamp、curve 和 density 也不等价于参考端的 D3D 线性雾分支。
|
||||
- 因此本轮完成 `SkyBox`、`ScreenFilter`、`LensFlare` 活跃源码静态核对;天空结构测试通过不能证明 transition/render order、滤镜、光晕和地图环境切换副作用一致,合同保持 `PARTIAL`。
|
||||
|
||||
## 已确认差异和未验证项
|
||||
|
||||
1. 保持 filter/lens flare/wind 的明确 GAP,或实现对应的后处理、光晕和树木风更新,并为每个资源失败/禁用分支补测试。
|
||||
2. 以 40250 的 fog near/far、天空 transition、云 plane、背景/角色光照和 render order 建立截图/参数 oracle,禁止只用 metadata 断言代替渲染证据。
|
||||
3. 补齐 `DayMode/PRESERVE_DayMode` 的正式环境切换和场景生命周期;`WeatherDayNightSystem` 的自动地图预设继续隔离,不能替代 40250 服务器命令。
|
||||
4. 建立真实地图快速切换、暂停恢复、环境资源缺失、重复加载/卸载和粒子/天空无悬挂节点的组合测试。
|
||||
5. 对 SnowEnvironment 的池化、世界范围、风/重力、相机边界和 outdoor render 时序补专门实现或明确平台适配合同。
|
||||
|
||||
## 下一轮验证入口
|
||||
|
||||
- 进入实现阶段后,优先修复正式 GameScene 的天气/地图环境接入和 fog/filter/lens flare/wind 中的已确认 GAP;修复后重新运行本合同四组测试并增加真实渲染证据。
|
||||
@@ -0,0 +1,88 @@
|
||||
# world.fishing_mining
|
||||
|
||||
## Scope
|
||||
|
||||
审计 40250 Windows 客户端的钓鱼输入门控、`GetFishingRot` 水面扫描、`CG_FISHING/GC_FISHING` 状态与角色动作、`GC_DIG_MOTION` 采矿动作、地图水面/水高和当前端扩展的钓鱼、开鱼、烹饪、营火、采矿、熔炼脚本。判断标准是当前端是否统一复现 40250 客户端的输入条件、坐标/单位、协议、角色动画、事件顺序和资源副作用;服务端概率、掉落、库存、熔炼与增益规则不能因被写入 Godot helper 就当成 Windows 客户端 parity。
|
||||
|
||||
## Reference call chain
|
||||
|
||||
- `UserInterface/PythonPlayerInput.cpp`
|
||||
- `NEW_Fishing`:无主角色则返回;已在钓鱼时发送 `CG_FISHING(0)`;否则要求 `CanFishing()`,调用 `GetFishingRot`,水面无效时只触发 `OnFishingWrongPlace`,有效时发送角度。
|
||||
- `NEW_CancelFishing` 只在 `IsFishing()` 时发送取消包,并有 500ms 节流。
|
||||
- `UserInterface/InstanceBaseMotion.cpp` / `InstanceBase.cpp`
|
||||
- `GetFishingRot` 以角色当前位置、朝向和 600cm 距离,按左右 0..180 度、每 10 度扫描 `ATTRIBUTE_WATER`,优先返回右侧,再返回左侧;钓鱼/持镐状态参与动作门控。
|
||||
- `StartFishing`、`StopFishing`、`ReactFishing`、`CatchSuccess`、`CatchFail` 只切换角色动作/表情,不在客户端结算鱼或库存。
|
||||
- `UserInterface/PythonNetworkStreamPhaseGame.cpp` / `Packet.h`
|
||||
- `SendFishingPacket` 发送角度/5 的 byte 并追加 sequence;`RecvFishing` 对 START/STOP/REACT/SUCCESS/FAIL 按 `info` 查角色并切换动作,FISH 按 item vnum 走 `OnFishingNotify/OnFishingSuccess`;未知角色包直接忽略,未知物品也不凭空入包。
|
||||
- `RecvDigMotionPacket` 查主角色和目标,朝目标转身,再按 `count` 重复 `NAME_DIG` 动作。
|
||||
- `GameLib/AreaTerrain.cpp` / `MapOutdoorWater.cpp`
|
||||
- 地形通过水属性图和水高表提供 `ATTRIBUTE_WATER`/water height;水面渲染使用地图 water 数据和循环纹理,不负责钓鱼收益。
|
||||
|
||||
## Current call chain
|
||||
|
||||
- native `M2Client::fishing`、`ClassicSession::send_fishing`、`GameClient::send_fishing` 已能在游戏阶段发送角度/5;classic stream 会为已登记的 CG 包追加 sequence。
|
||||
- `EntityStore`/classic parser 已保存 `GC_FISHING` 和 `GC_DIG_MOTION`,`M2Client::poll` 发出 `fishing_event`/`dig_motion`;`NetWorld` 会更新实体方向/动作,`NetPlay` 会更新本地主角钓鱼状态,`ChatUI` 展示 FISH/FAIL 文本。
|
||||
- `Metin2World::get_fishing_rotation` 已按 600cm、左右 10 度扫描 `ATTRIBUTE_WATER`,`fishing_water_test.gd` 已在 A1 地图验证样本;water builder/animation 已存在。
|
||||
- `project/fishing_mining_system.gd`、`fishing_rod_reel_system.gd`、`fishing_cooking_camp_system.gd`、`mining_refinery_smelt_system.gd` 直接实现概率、物品消耗、烤鱼 buff、矿石熔炼、宝石孔和库存写入。这些是本端 custom/server-like authority,40250 Windows 客户端本身不做这些结算。
|
||||
|
||||
## Branch matrix
|
||||
|
||||
| 场景 | 40250 | 当前端 | 结论 |
|
||||
|---|---|---|---|
|
||||
| 开始钓鱼 | `NEW_Fishing`→`CanFishing`→`GetFishingRot`→有效才发送;无水只提示 | `activate_fishing` 有状态/死亡/技能门控并调用 `get_fishing_rotation`,再调用 native fishing | PARTIAL:主路径接近,但真实装备动作模式、CanFishing 和服务端拒绝回滚未端到端证明 |
|
||||
| 钓鱼角度 | 角色前方 600cm,左右 0..180、10 度,右侧优先,返回服务端角度 | native world 用 6m、同样步长并做 MapCoord 转换 | PARTIAL:算法接近;轴向、地图基准和 heading 转换需实机/固定样本对照,provider 缺失时当前端允许协议 fallback |
|
||||
| 取消钓鱼 | 只在 `IsFishing` 时发 0,500ms 内不重复发送 | `cancel_fishing` 使用 `_fishing_active` 和 500ms 节流 | PARTIAL:状态来源是本地事件 flag,不是 actor `IsFishing` 的完整动作状态;断线/旧包/取消失败清理未覆盖 |
|
||||
| `GC_FISHING START/STOP` | 按 fisher VID 查角色,调用 Start/Stop;找不到角色直接忽略 | NetWorld 写 metadata、设置 `fishing`/`wait`,NetPlay 对本地主角维护 `_fishing_active` | PARTIAL:状态/动作骨架存在;metadata/placeholder 与真实角色 motion 资源、重复/乱序包语义仍需验证 |
|
||||
| `REACT` | 仅在角色仍钓鱼时加 fish emoticon 并 ReactFishing | 当前端显示 emoticon 并切 `fishing_react`,未严格以 actor 状态确认 | PARTIAL:缺少同等 `IsFishing` 条件和清理时序证据 |
|
||||
| `SUCCESS/FAIL` | 切 catch/fail 动作;FAIL 对主角色触发失败 UI | 当前端切动作并在 ChatUI 展示失败文本 | PARTIAL:动作/文本入口有,资源、音效、重复结果和主角色判断未完整覆盖 |
|
||||
| `FISH` 结果 | info=0 显示 unknown;有 item 且能查 item data 才通知;按角色仍钓鱼决定 notify/success | ChatUI 用 proto 查询并展示;NetWorld/NetPlay 将 FISH 作为 UI-only | PARTIAL:未知物品忽略接近参考,但 item type、主角色状态和 live inventory 回包未闭合 |
|
||||
| 采矿动作 | `GC_DIG_MOTION` 查 VID/目标、朝目标转身、按 count 重复 dig motion | native parser→NetWorld 转身并只设置一次 `dig` 状态、保存 count | PARTIAL:count 仅 metadata,未证明按 count 排队多个 NAME_DIG 动作;目标缺失/动作结束清理未覆盖 |
|
||||
| 矿镐/攻击门控 | `IsHoldingPickAxe` 影响 CanAttack/CanUseSkill/动作模式 | 当前 `FishingMiningSystem` 能识别矿镐,但未作为 live NetPlay 的统一攻击/技能门控证据 | PARTIAL/GAP:识别 helper 存在,网络动作门控未闭合 |
|
||||
| 水属性/水高/水面 | ATTRIBUTE_WATER、水高和 water.wtr 由地图资源读取与渲染 | native world 读取 attr、水高并构造 water mesh/动画 | PARTIAL:结构已存在,尚未证明所有 map chunk 边界、water height 单位和纹理帧与 40250 像素级一致 |
|
||||
| 钓鱼/采矿收益 | 客户端只展示服务端 item/状态,不计算概率和库存 authority | 本地脚本直接 roll、扣 bait、写 inventory、发 fish/ore、烹饪 buff 和熔炼宝石 | GAP(custom authority):不能与 40250 Windows 客户端实现宣称统一 |
|
||||
|
||||
## Implementation-equivalence matrix
|
||||
|
||||
| 项目 | 状态 | 证据/限制 |
|
||||
|---|---|---|
|
||||
| Preconditions | PARTIAL | 游戏阶段、网络状态、水面扫描和部分本地动作门控存在;真实 `CanFishing`/装备形态、持镐对攻击/技能的统一门控、服务端拒绝未完整接入 |
|
||||
| Branch structure | PARTIAL | START/STOP/REACT/SUCCESS/FAIL/FISH、DIG_MOTION、water attribute 主分支存在;REACT 的 actor 状态条件、DIG count 队列和未知/乱序清理不等价 |
|
||||
| Algorithms/formulas | PARTIAL | 600cm/10度扫描、右优先和 5 度网络量化接近参考;收益概率、开鱼、采矿、熔炼和 buff 是自定义本地算法,不是 40250 client 算法 |
|
||||
| State transition order | PARTIAL | 回包→parser→NetWorld/NetPlay/UI 大体存在;真实角色动作完成、FISH 与 IsFishing 判断、拒绝/重连和 inventory 回包顺序未闭合 |
|
||||
| Constants/units | PARTIAL | 600cm、10度、180度、500ms、角度/5、ATTRIBUTE_WATER 和 packet layout 有证据;MapCoord 轴向/水高单位和本地 VNUM/概率表缺 reference client 证据 |
|
||||
| Timing/event sources | PARTIAL | 取消节流和网络事件队列存在;服务端 fishing reaction 时序、动作结束、重复/延迟 GC 包、矿动作 count 时序未覆盖 |
|
||||
| Resource/data sources | PARTIAL | A1 attr/water mesh、fish emoticon 和角色状态入口存在;全部地图 water.wtr、实际 fishing motion/effect/sound 和 item proto 对应关系未完整验证 |
|
||||
| Protocol side effects | PARTIAL | CG/GC fishing、GC dig parser 和 sequence/frame 存在;缺 live wrong-place、CanFishing、server result、item inventory/音效端到端证据 |
|
||||
| Interruption/failure/cleanup | GAP | 断线、换图、角色消失、错误 item、重复 FISH/FAIL、采矿 count 中断及本地 helper 的 optimistic inventory/buff 回滚没有完整证明 |
|
||||
|
||||
## Regression evidence
|
||||
|
||||
- `project/test_fishing_mining_parity.gd`:26/26 通过;验证本地装备识别、概率 helper、开鱼/蚌壳和矿脉表,不是 40250 live client 回包证据。
|
||||
- `project/test_fishing_rod_reel_parity.gd`:33/33 通过;验证本端抛竿/拉杆/鱼竿精炼 helper,40250 Windows client 不在本地执行这些收益和持久化规则。
|
||||
- `project/test_fishing_cooking_camp_parity.gd`:23/23 通过;验证本地营火、烤鱼和 buff 写入,属于 custom authority。
|
||||
- `project/test_mining_refinery_smelt_parity.gd`:21/21 通过;验证本地熔炼、开孔和镶嵌,不能证明 Windows client parity。
|
||||
- `project/fishing_water_test.gd`、`project/water_reference_test.gd`:通过;分别验证 A1 水属性/600cm heading 查询和当前 water geometry/material,不是 40250 像素级渲染证明。
|
||||
- `project/netplay_test.gd`、`project/netbridge_test.gd`、`project/combat_fx_test.gd`、`project/chat_test.gd`、`build/extension/net_entity_test`、`build/extension/net_classic_session_test`、`build/extension/net_classic_wire_test`:覆盖 fishing/dig event 的部分状态、动作和 wire;未覆盖 actor motion 资源、REACT `IsFishing`、DIG count 多动作、服务端拒绝、乱序、断线/切图和 live inventory。
|
||||
|
||||
## Status
|
||||
|
||||
## Deep audit round 2026-09-21
|
||||
|
||||
### Fresh regression results
|
||||
|
||||
- `test_fishing_mining_parity.gd` 26/26、`test_fishing_rod_reel_parity.gd` 33/33、`test_fishing_cooking_camp_parity.gd` 23/23、`test_mining_refinery_smelt_parity.gd` 21/21 均通过。
|
||||
- `fishing_water_test.gd`、`water_reference_test.gd`、`netplay_test.gd`、`combat_fx_test.gd`、`chat_test.gd` 通过;native entity/session/wire 三项通过。
|
||||
- `netbridge_test.gd` 仍失败 3 项(实体节点、mount 子节点和主实体可见性),并有 5 个 ObjectDB 泄漏;这说明 fishing/dig 事件与通用实体桥接尚未闭合。
|
||||
|
||||
### Static parity conclusion
|
||||
|
||||
- 600cm 水面扫描、10 度步长、右侧优先、500ms 取消节流和 fishing/dig 包骨架存在,但 `CanFishing`/持竿动作状态、REACT 的 `IsFishing` 条件和服务端拒绝回滚不等价。
|
||||
- `GC_DIG_MOTION` 的 `count` 目前只保存为 metadata/一次状态,未证明按参考实现重复排队 `NAME_DIG` 动作。
|
||||
- 本端钓鱼收益、烹饪、营火、采矿、熔炼、开孔和库存写入仍由本地脚本直接结算;40250 Windows 客户端只消费服务端结果,属于职责差异。
|
||||
- 水属性和水面几何有局部样本通过,但地图边界、单位、资源帧、动作/音效和断线/换图清理没有完整 live 证据。
|
||||
|
||||
### Status update
|
||||
|
||||
本轮仍为 `PARTIAL`;完成钓鱼/采矿方向的合同审计并登记,未修改实现。
|
||||
|
||||
`PARTIAL`。当前端已经具备 40250 钓鱼/采矿协议骨架、角色动作入口、水属性扫描和水面构造;但持竿/持镐 live 门控、角色状态条件、采矿 count 动作队列和真实资源副作用证据不足,且钓鱼收益、烹饪、营火、熔炼、宝石与库存脚本承担了 40250 客户端没有承担的本地 authority。本轮完成审计并记录,未修改实现。
|
||||
@@ -0,0 +1,118 @@
|
||||
# world.map_load_transition
|
||||
|
||||
## Scope
|
||||
|
||||
审计 40250 地图 `Leave/Clear/Load/Enter`、玩家坐标驱动的区域加载、terrain/property/object/environment 初始化、区域卸载,以及当前 `Metin2World` 和 `GameScene` 的换图、失败和恢复行为。目标是比较可观察的生命周期与资源/碰撞边界,不要求 Godot 复制 DirectX 线程实现。
|
||||
|
||||
## Reference implementation and evidence
|
||||
|
||||
- `../40250/Server Client TMP4/ClientVS22/source/GameLib/MapManager.cpp`
|
||||
- `LoadMap` 先调用当前 `CMapOutdoor::Leave()`,设置 map name、`LoadProperty()`,按地图类型加载;户外地图 `Load(x,y,z)` 成功后注册/设置 environment,最后 `Enter()`。
|
||||
- `UnloadMap` 校验当前 map name 后 `Clear()`;不匹配时失败,不清理另一张地图。
|
||||
- `UpdateMap` 把玩家位置交给 `CMapOutdoor::Update`,触发区域/terrain 窗口更新。
|
||||
- `../40250/Server Client TMP4/ClientVS22/source/GameLib/MapOutdoorLoad.cpp`
|
||||
- `CMapOutdoor::Load` 先 `Destroy`,读取 `Setting.txt`,建立 terrain patch/quadtree/water/shadow,调用 `Update(x,y,z)`,再确定本地或 `d:/ymir work/environment/` 的 `.msenv` 路径。
|
||||
- `LoadTerrain` 要求 `AreaProperty.txt` 的脚本类型和字段,按区域加载 water/height/attr/tile/shadow/minimap,并生成 ready terrain。
|
||||
- `LoadArea` 按区域目录加载 object/area 数据;请求重复区域时不重复创建。
|
||||
- `../40250/Server Client TMP4/ClientVS22/source/GameLib/MapOutdoorUpdate.cpp`
|
||||
- 首次或跨 `LOAD_SIZE_WIDTH` 窗口时加载周边 terrain/area;`AssignTerrainPtr` 绑定 3×3 指针窗口;移动时更新 terrain patch、area 和延迟垃圾回收。
|
||||
- 区域离开窗口后先放入 delete vector,按回收间隔销毁,不是瞬间删除所有资源。
|
||||
- `../40250/Server Client TMP4/ClientVS22/source/GameLib/AreaLoaderThread.cpp`
|
||||
- terrain/area 请求进入线程安全队列,由 loader thread 读取资源,主线程 `Fetch` 后再接入地图;请求队列和 shutdown 有独立生命周期。
|
||||
|
||||
## Current implementation and evidence
|
||||
|
||||
- `extension/src/metin2_world.cpp`
|
||||
- `load_map` 一开始 `unload_map()`,解析 `setting.txt`、建立 AssetResolver/Property registry、按 `load_radius` 构建 chunk、解析 `.msenv`;失败时保留 `setting_ok=false` 并返回 false。
|
||||
- `load_radius < 0` 时同步构建整图;否则先 `stream_update()`,再在 `load_map` 内把整个队列同步建完,之后 `_process` 才按 `stream_budget` 增量构建新 focus 范围。
|
||||
- `stream_update` 以方形 tile 半径加载/卸载 chunk,卸载立即从 vector 移除并 `queue_free` root;没有 40250 的 terrain/area delete vector 与延迟垃圾回收语义。
|
||||
- `unload_map` 清空 chunk/队列/ambience、queue_free objects/environment,重置统计和错误状态;没有 load generation/token 防止旧异步结果回灌。
|
||||
- `project/game_scene.gd`
|
||||
- 初次 setup 先解析 map path、创建 fallback lighting,再显式 `_build_world`,地图失败只 warning 并继续平地运行。
|
||||
- `_on_world_reset` 清理 NetPlay、NetWorld、掉落物、临时 UI 和 BGM;之后等待/接收 warp,再由 `_reload_map_for_warp` 创建新 world。
|
||||
- `_reload_map_for_warp` 先保存 `old_world`,调用 `_build_world` 完整加载新 world,之后才对旧 world `queue_free`;旧 world 在新 world 失败时也可能仍作为有效场景引用存在。
|
||||
- 同图 warp 仅放置本地主角;跨图 warp 才重建 world。地图切换后重新绑定 camera/player/net_world/HUD/atlas,但网络实体刷新与 world 加载之间没有原版 `Enter` 阶段契约。
|
||||
- `project/net_play.gd` / `project/net_world.gd`
|
||||
- world reset 清除当前 Actor、移动/战斗/投射物/临时 UI 生命周期;场景层通过 `queue_free` 延迟删除节点。
|
||||
- reset 前后没有与 `Metin2World` 共用的 generation;旧网络包或旧模型回调如果晚于新 world,依赖各模块自己的 guard。
|
||||
|
||||
## Reference-to-current call chain
|
||||
|
||||
| 40250 | 当前端 |
|
||||
|---|---|
|
||||
| `CMapManager::LoadMap` -> `Leave` -> `LoadProperty` -> `CMapOutdoor::Load` -> `Register/SetEnvironment` -> `Enter` | `GameScene::_reload_map_for_warp` -> `_build_world` -> `Metin2World::load_map` -> `_place_player_at_net_pos` |
|
||||
| `CMapOutdoor::Load(x,y,z)` -> `Update` -> terrain/area 3×3 window | `load_map` -> `load_radius` 方形 chunk queue;首轮队列在 load_map 内同步建完 |
|
||||
| `Update` -> `AssignTerrainPtr` -> `UpdateTerrain/UpdateAreaList` -> 延迟垃圾回收 | `set_focus_position` -> `stream_update` -> `_process` build/unload;unload 立即 queue_free |
|
||||
| `UnloadMap(name)` 校验 name -> `Clear` -> map resources reset | `unload_map` 无 map-name 校验,直接清当前 world |
|
||||
| loader thread 请求/Fetch/Shutdown | `stream_queue` 在主线程逐帧处理,没有独立异步加载线程或旧请求取消 token |
|
||||
|
||||
## Branch matrix
|
||||
|
||||
| 场景 | 40250 行为 | 当前端行为 | 结论 |
|
||||
|---|---|---|---|
|
||||
| 首次加载 | 读取 Setting/Property,按玩家坐标加载周边 terrain/area,设置 environment 后 Enter | 读取 setting/registry,按 load radius 建 chunk,再建 `.msenv`/fallback | PARTIAL:资源阶段接近,入口/ready 语义不同 |
|
||||
| 同图 warp | 保持当前 map,更新背景位置并更新区域窗口;Loading 阶段同步 Actor/临时对象清理 | 只放置主角;网络 reset 由前置 world_reset 处理 | PARTIAL:地图与网络阶段未形成同一状态机 |
|
||||
| 跨图 warp | 旧 map Leave/Clear 后再 Load 新 map,旧资源不与新 map 长时间共存 | 新 world Load 成功后才 queue_free 旧 world | GAP:存在双 world/双环境/资源峰值和旧引用窗口 |
|
||||
| Setting 缺失/格式错误 | `LoadSetting`/`Load` 失败;MapManager 返回 false,调用方决定是否进入下一阶段 | `load_map` 返回 false,GameScene warning 后继续平地 | PARTIAL:失败可见性和后续网络行为不等价 |
|
||||
| TextureSet/区域文件缺失 | 记录失败并影响 terrain/area ready;周边窗口仍有明确失败状态 | chunk 可单独失败并继续;缺失对象以统计项/空节点容忍 | ADAPTATION/PARTIAL:需要证明角色、碰撞和可见性边界一致 |
|
||||
| environment 缺失 | 仍通过 environment data name/注册流程;设置失败由原版环境 API报告 | `.msenv` 失败后 fallback lighting;跨图可能保留旧 `_sun/_env` | PARTIAL:旧环境释放和失败回滚未闭合 |
|
||||
| streaming 进入新 tile | 3×3 指针窗口、请求新 terrain/area、旧区域进 delete vector | 以方形 radius 立即卸载并重新入队,`stream_budget` 逐帧构建 | PARTIAL:窗口公式相近,时序/回收不同 |
|
||||
| 重复 warp / 中断加载 | map manager 当前状态和 loader queue 有明确销毁/重建边界 | 无显式 warp generation;新旧 `_build_world` 和 delayed queue_free 可交错 | PARTIAL |
|
||||
| map name 不匹配卸载 | 拒绝卸载并保留当前 map | world 实例无 map-name 校验,直接清理 | GAP |
|
||||
|
||||
## Equivalence matrix
|
||||
|
||||
| 项目 | 状态 | 证据/缺口 |
|
||||
|---|---|---|
|
||||
| Preconditions | PARTIAL | path/assets/setting 检查已存在;Unload 的 map-name 校验和 ready gate 不同 |
|
||||
| Branch structure | PARTIAL | 同图/跨图、失败、重复 warp、缺 environment 和中断加载尚未用同一状态 fixture 覆盖 |
|
||||
| Algorithms/formulas | PARTIAL | cm/m 坐标、base position、chunk tile 和 height/attr 查询已有转换;区域窗口与 `LOAD_SIZE_WIDTH` 的精确边界尚未逐格证明 |
|
||||
| State transition order | GAP/PARTIAL | 40250 `Leave -> Load -> Environment -> Enter`;当前新旧 world 共存且旧 world 后释放 |
|
||||
| Constants/units | PARTIAL | `CHUNK_CM`、`CM_TO_M`、map base、focus tile、load radius 与原版 terrain/cell/patch 常量需要 fixture |
|
||||
| Timing/event sources | PARTIAL | 参考端 loader thread + manager update;当前 Godot `_process` budget,异步回调/旧请求取消未等价 |
|
||||
| Resource/data sources | MAPPED | setting、property、textureset、height/attr/water/tile/shadow、object、environment 的源文件均已定位 |
|
||||
| Protocol side effects | PARTIAL | GC_WARP/world_reset/主角放置已接线,但 map ready、Actor burst 和 Enter 的相对时序未闭环 |
|
||||
| Interruption/failure/cleanup | PARTIAL | `queue_free`、fallback、重复 warp、旧 world 引用、stream queue 和失败重试缺少完整证据 |
|
||||
|
||||
## Required tests
|
||||
|
||||
- 同图 warp:不创建第二个 World,清理 Actor/掉落物/飞行物/临时 UI 后主角和 focus tile 位置正确。
|
||||
- 跨图 warp:记录 Leave/reset/build/Enter 的事件顺序,断言旧 world 不与新 world 长时间共存,且失败时旧 world 保留策略明确。
|
||||
- 缺失/格式错误的 setting、textureset、terrain/area、object、`.msenv`;区分 hard failure、fallback 和可继续运行的缺失资源。
|
||||
- 重复 warp、快速连续 warp、加载中再次 reset、旧 world `queue_free` 尚未执行时收到旧实体/模型回调。
|
||||
- focus tile 边界逐格验证:load/unload 集合、height/attr/water/collision、对象/树/ambience 和实体可见性保持一致。
|
||||
- 加载成功后的 map-ready/主角放置/GC_MAIN_CHARACTER 顺序,以及失败后重试同图和跨图的状态复位。
|
||||
|
||||
## Test evidence
|
||||
|
||||
- `project/gamescene_test.gd`:通过,验证缺图 fallback、GameScene 装配、主角定位、world reset 和实体镜像;它不证明 40250 的 Leave/Enter 顺序。
|
||||
- `project/p9_test.gd`:通过,验证小地图、昼夜、天气和 warp signal 接收;它不创建真实 world transition。
|
||||
- `project/forest_map_render_test.gd`:这是由 `script/forest_map_render_test.sh`/客户端主循环启动的 Node 脚本,不能用 `godot --headless --script` 直接当 SceneTree 测试;本轮直接启动会被 Godot 拒绝,故不得计为通过。
|
||||
|
||||
## Deep audit round 2026-09-20
|
||||
|
||||
本轮对 `MapManager::LoadMap/UnloadMap/UpdateMap` 与 `Metin2World::load_map/stream_update/_process/unload_map` 做了实现级复核:
|
||||
|
||||
1. 40250 的 `LoadMap` 顺序是当前 map `Leave` → 设置 map name/加载 Property → `CMapOutdoor::Load(x,y,z)` → 注册并设置环境 → `Enter`;当前 `load_map()` 先无条件 `unload_map()`,然后同步解析 setting/AssetResolver/Property,再按半径把整个 `stream_queue` 建完,最后才解析 `.msenv`。因此当前没有 reference 的 Leave/Enter/ready 边界,且 streaming 初始加载并非真正逐帧。
|
||||
2. 当前 `stream_update()` 立即 `queue_free` 半径外 chunk,并清空再重建待加载队列;40250 `Update` 通过 3×3 terrain/area 指针窗口和 delete vector 延迟回收。当前在 focus tile 边界快速往返时,旧 chunk 的节点仍可能处于 Godot queue_free 期,同时新 chunk 已入队,资源/碰撞峰值和事件时序不同。
|
||||
3. 40250 `UnloadMap(name)` 会校验传入 map name,不匹配时拒绝清理;当前 `Metin2World::unload_map()` 无 map-name 参数,任何调用都直接清空当前 world。GameScene 的 `_reload_map_for_warp()` 还会先创建新 world,成功后才对 old world `queue_free`,形成短暂双 world/双碰撞/双环境窗口。
|
||||
4. setting 缺失时当前 `load_map()` 返回 false,但 `GameScene::_build_world()` 将其降级为 warning 并继续平地运行;这与 40250 `LoadMap` 失败后由 phase/调用方决定是否继续不同。`.msenv` 解析失败也使用 fallback lighting,旧 `_sun/_env` 的释放与新地图失败回滚没有统一 generation/owner。
|
||||
5. 当前 `Metin2World` 没有 loader thread/Fetch/Shutdown 对应的请求 token;`stream_queue` 是主线程 vector。换图或快速 reset 后没有 generation 防止旧队列/旧模型回调回灌新 world,网络 `world_reset` 的 generation 与 Metin2World 也未共享。
|
||||
|
||||
本轮测试结果:`gamescene_test.gd`、`p9_test.gd`、`environment_test.gd` 通过;测试只证明当前场景装配、warp signal 和 `.msenv` parser 可以运行,不能证明 Leave/Enter、map-name 校验、延迟回收、双 world 窗口或中断加载与 40250 一致。
|
||||
|
||||
### Active GameLib map-support review
|
||||
|
||||
- `GameLib/MapBase.cpp` 的 `Clear/Enter/Leave/SetEnvironmentDataPtr/ResetEnvironmentDataPtr` 是地图基类边界;`MapType.cpp`/`MapUtil.cpp` 负责 MapProperty、`.msenv` 的 fog/filter/skybox/lensflare/ambience/dungeon block 解析和插值 helper;`AreaLoaderThread.h` 描述区域加载线程的 Create/Setup/Destroy 生命周期。
|
||||
- `GameLib/Property.cpp`/`PropertyLoader.cpp`/`PropertyManager.cpp` 以 pack-backed property、CRC、reserve、注册/替换/删除/清理为地图对象、树、建筑、效果、环境和 dungeon block 提供权威配置;当前 `PropertyRegistry`/AssetResolver 只做部分文本/路径适配。
|
||||
- `GameLib/MapOutdoor.h`/`MapManager.h`/`MapBase.h`、`GameType`/`Interface` 和 terrain patch/quad 文件的静态核对确认当前 `Metin2World` 的方形 chunk 队列不是完整 `CMapOutdoor::Update`/loader-thread/延迟回收链;地图合同继续为 `PARTIAL`。
|
||||
|
||||
### Active UserInterface background bridge review
|
||||
|
||||
- `PythonBackground.h`/`PythonBackgroundModule.cpp` 暴露地图 Load/Destroy/Warp、environment register/get、terrain/object picking、height、shadow level/texture、visible part、splat/view distance、guild area、target/special effect 和 dungeon map name。
|
||||
- 当前 `Metin2World`/`GameScene` 已有 map load、height/attr/water、environment 和目标效果入口,但没有同一套 background bridge 的 environment index、shadow/splat/view-distance/guild-area/特殊效果清理契约;`CameraProcedure.cpp` 还会通过 terrain-only ray 做多方向相机碰撞调整。
|
||||
- 本轮完成 Background/CameraProcedure 静态核对,地图合同继续为 `PARTIAL`,相机碰撞差异同时保留在点击/输入合同中。
|
||||
|
||||
## Status
|
||||
|
||||
`PARTIAL`。本轮完成了 40250 与当前端地图生命周期、区域窗口、资源释放和失败分支的逐文件审计;发现跨图双 World、失败 fallback、Unload 校验、区域回收和异步中断边界尚未等价。现有回归通过只说明当前路径可运行,不能标记为已修复或 EQUIVALENT。
|
||||
@@ -0,0 +1,83 @@
|
||||
# world.mount_pet_polymorph
|
||||
|
||||
## Scope
|
||||
|
||||
审计 40250 Windows 客户端的角色 `mount_vnum` 接收、`GC_MOUNT` 上下马状态、骑乘/步行 motion mode、旋转速度、骑乘攻击/技能限制、变身 affect/装备遮挡,以及当前端的马匹、宠物、变身和马上技能脚本。判断标准是客户端是否与 40250 的实体字段、动作模式、门控和回包顺序统一;服务端拥有马匹等级、宠物属性、变身持续和技能伤害 authority,当前端本地 helper 能独立推进规则不能作为 parity 证明。
|
||||
|
||||
## Reference call chain
|
||||
|
||||
- `UserInterface/PythonNetworkStreamPhaseGameActor.cpp`
|
||||
- Character add/additional/update packet 携带 `dwMountVnum`,交给 `CInstanceBase`;角色创建时非零 mount vnum 会进入 `MountHorse`。
|
||||
- `UserInterface/PythonNetworkStreamPhaseGame.cpp` / `Packet.h`
|
||||
- `RecvMountPacket` 读取 `vid/mount_vid/pos/x/y`,按角色存在性处理上下马;该版本的 `Ride/Unride/SetRidingVehicleIndex` 调用被注释,不能凭空扩大为客户端结算。
|
||||
- `UserInterface/InstanceBase.cpp` / `InstanceBaseMotion.cpp`
|
||||
- `MountHorse/DismountHorse` 更新 horse actor、动作模式和 rotation speed;持镐、钓竿、变身、婚纱、坐骑会影响 motion mode、`CanAttack`、`CanUseSkill` 和武器/发型遮挡。
|
||||
- `CanAttackHorseLevel` 通过 horse actor 等级限制普通攻击;新的坐骑/马术技能可由 `PythonPlayer` 的 can-use 门控决定。
|
||||
- `UserInterface/PythonPlayer.cpp` / `PythonPlayerSkill.cpp` / `InstanceBase.h`
|
||||
- 客户端消费 `NEW_AFFECT_POLYMORPH`/`NEW_AFFECT_MOUNT` 等状态和 skill level,阻止不允许的动作;不在客户端本地计算宠物战力、掉落、马匹成长或服务端技能伤害。
|
||||
- 40250 ClientVS22 全树搜索未找到独立宠物 follow/combat authority 实现;`ITEM_TYPE_POLYMORPH` 和 polymorph skill/affect 常量存在,但其客户端职责是展示/门控/动作。
|
||||
|
||||
## Current call chain
|
||||
|
||||
- native classic parser 已将角色 add/update、additional info 和 `GC_MOUNT` 的 `mount_vnum` 写入 `EntityStore`;`M2Client::poll` 发出 `mount_changed`,实体 getter 能提供该字段。
|
||||
- `project/net_world.gd` 保存 `mount_vnum` metadata、根据 mount 选择旋转速度并更新实体字段;`project/net_play.gd` 根据 mount/poly/weapon 刷新 local motion mode、骑乘攻击等级门和技能上下文;`EquipModel` 只用 mount 状态处理武器双手规则。
|
||||
- `project/horse_system.gd`、`horse_combat_system.gd`、`horse_level_growth_system.gd`、`mounted_combat_skill_system.gd`、`pet_companion_system.gd`、`classic_pet_system.gd`、`polymorph_system.gd`、`polymorph_marble_system.gd` 在本地维护召唤、骑乘、耐力、宠物跟随/增益、变身时限和技能伤害。
|
||||
- 当前代码搜索未发现 `mount_vnum` 被用于创建/挂接实际坐骑模型或 saddle actor 的实现;大多数 live 网络路径只保留 metadata/motion mode,宠物和变身 helper 也没有对应的 native server affect/entity 回包闭环。
|
||||
|
||||
## Branch matrix
|
||||
|
||||
| 场景 | 40250 | 当前端 | 结论 |
|
||||
|---|---|---|---|
|
||||
| 角色创建带 mount | add packet 的 `dwMountVnum`→`MountHorse`→动作/挂点;服务器字段权威 | parser/store 保存 mount,NetWorld 只写 metadata,未发现 mount actor/model attach | GAP:状态字段有,视觉实体链缺失 |
|
||||
| `GC_MOUNT` 上马 | 读取包,角色存在时进入 Ride 分支(本版本 Ride 调用被注释),主角色 vehicle index 也被注释 | parser 更新实体 `mount_vnum`,发 `mount_changed`,NetPlay 刷 motion mode | PARTIAL:字段/动作门有,需保持与该版本注释语义一致,不能自行增加本地 ride authority |
|
||||
| `GC_MOUNT` 下马 | `mount_vid==0`,服务端位置字段交给 Unride(本版本调用被注释) | mount 字段置零并刷新 motion;没有验证 x/y 下马位置和模型清理 | PARTIAL |
|
||||
| 骑乘旋转/移动 | MountHorse 使用约 300°/s rotation,步行约 1200°/s,horse motion mode 参与根运动 | NetPlay/NetWorld 有 horse rotation speed 和 mounted mode;NetWorld 对有 mount 的真模型不推送 motion speeds | PARTIAL:常量入口接近,但实际 horse motion/模型不存在,远端骑乘 root motion 不完整 |
|
||||
| 普通攻击 | `CanAttack` 受 horse level、婚纱、持镐和 actor lock 限制;等级 1 坐骑不能攻击 | NetPlay 有 `_can_attack_horse_level` 和部分新坐骑 skill gate | PARTIAL:部分映射存在,真实 horse actor 等级、服务器拒绝、动作/包副作用未完整证明 |
|
||||
| 技能/武器模式 | `CanUseSkill`、`SetMotionMode` 受变身、婚纱、持镐、坐骑、武器类型影响 | local motion mode/skill context 有 mount/poly 字段,但 poly 字段未由 native entity 回包提供 | PARTIAL/GAP |
|
||||
| 变身 | `NEW_AFFECT_POLYMORPH`/shape/parts 影响模型、装备遮挡、技能/动作门;状态来自服务器 affect/character data | `polymorph_system.gd` 直接启动、加攻防、计时、死亡取消;EquipModel 明确 `_is_poly()` 恒 false | GAP:本地 helper 有规则,live 视觉和 native affect 闭环缺失 |
|
||||
| 宠物 | 40250 ClientVS22 未发现独立宠物 follow/combat authority | `pet_companion_system.gd`/`classic_pet_system.gd` 本地召唤、跟随和属性加成 | GAP(custom feature/authority):不能标记为 40250 parity |
|
||||
| 马匹成长/耐力/喂养/马上技能 | 服务端/脚本权威;客户端只消费字段和动作 | 本地 helper 直接计算成长、耐力、技能 CD、伤害、库存消耗 | GAP(custom authority) |
|
||||
|
||||
## Implementation-equivalence matrix
|
||||
|
||||
| 项目 | 状态 | 证据/限制 |
|
||||
|---|---|---|
|
||||
| Preconditions | PARTIAL | native stage/VID/mount 字段、部分死亡/技能/新坐骑门存在;角色存在、horse actor、变身 affect 和 server rejection 的完整门控未闭合 |
|
||||
| Branch structure | PARTIAL | add/update/mount parser、动作模式、攻击范围/等级入口存在;ride/unride 位置、实际 mount attach、poly entity field 和 pet 分支不等价 |
|
||||
| Algorithms/formulas | PARTIAL | motion mode/旋转速度和部分 mount level 表有映射;马匹成长、耐力、宠物加成、变身加攻防、马上技能伤害是本地 custom 算法 |
|
||||
| State transition order | PARTIAL | server actor data→EntityStore→mount_changed→motion mode 顺序大体存在;模型 attach/detach、poly affect→装备遮挡、断线/重连和死亡清理未闭合 |
|
||||
| Constants/units | PARTIAL | `mount_vnum`、马等级表、300/1200°/s、技能/affect 常量有证据;本地宠物/成长/耐力/伤害常量没有 40250 client authority 依据 |
|
||||
| Timing/event sources | PARTIAL | mount signal、motion refresh、死亡/技能上下文存在;实际 ride animation、poly affect duration、server updates、重复/乱序包未覆盖 |
|
||||
| Resource/data sources | GAP | 当前搜索未找到 `mount_vnum`→horse model/saddle actor 挂接;真实 horse/poly motion 资源存在与否、路径和绑定关系没有端到端证明 |
|
||||
| Protocol side effects | PARTIAL | actor/mount packet parser 和 getter 存在;没有客户端等价的宠物/马战本地结算,也缺 live 上下马/变身/拒绝/重连 packet sequence |
|
||||
| Interruption/failure/cleanup | GAP | 下马、角色消失、地图切换、死亡、变身过期、宠物过期、断线重连和 mount/poly model 清理未完整回归;本地 helper 可绕开服务器 authority |
|
||||
|
||||
## Regression evidence
|
||||
|
||||
- `project/test_mount_horse_parity.gd`:24/24 通过,覆盖本地马匹命令/范围 helper;不证明 `GC_MOUNT` live actor/model attach。
|
||||
- `project/test_horse_combat_parity.gd`:32/32、`test_horse_level_growth_parity.gd`:42/42、`test_mounted_combat_skill_parity.gd`:25/25 通过;这些测试验证本地骑乘战斗、成长、耐力和技能 helper,不能作为 40250 Windows client parity。
|
||||
- `project/test_pet_companion_parity.gd`:40/40、`test_classic_pet_parity.gd`:31/31 通过;验证本地宠物状态/序列化,40250 ClientVS22 未发现同等宠物 authority。
|
||||
- `project/test_polymorph_parity.gd`:25/25、`test_polymorph_marble_parity.gd`:32/32 通过;验证本地变身 helper,但未覆盖 native affect/entity、模型 shape、装备遮挡和 server lifetime。
|
||||
- `project/netplay_test.gd`、`project/netbridge_test.gd`、`build/extension/net_entity_test`、`build/extension/net_classic_session_test`:覆盖部分 mount 字段、motion mode 和 parser 信号;未覆盖模型挂接、GC_MOUNT 位置、poly/pet live chain、骑乘根运动、拒绝、乱序和重连清理。
|
||||
|
||||
## Status
|
||||
|
||||
## Deep audit round 2026-09-21
|
||||
|
||||
### Fresh regression results
|
||||
|
||||
- `test_mount_horse_parity.gd` 24/24、`test_horse_combat_parity.gd` 32/32、`test_horse_level_growth_parity.gd` 42/42、`test_mounted_combat_skill_parity.gd` 25/25、`test_pet_companion_parity.gd` 40/40、`test_classic_pet_parity.gd` 31/31、`test_polymorph_parity.gd` 25/25、`test_polymorph_marble_parity.gd` 32/32 均通过。
|
||||
- `equip_rules_test.gd`、`test_battle_arena_duel_parity.gd`、`player_move_test.gd`、`netplay_test.gd` 和 native entity/session 测试退出码 0;通用 `netbridge_test.gd` 仍有 3 项实体桥接失败和 5 个 ObjectDB 泄漏。
|
||||
|
||||
### Static parity conclusion
|
||||
|
||||
- `mount_vnum` 的 parser/store/signal、部分动作模式、旋转速度和攻击等级门存在;但没有找到 `mount_vnum` 到实际 horse/saddle model attach/detach 的生产链。
|
||||
- `GC_MOUNT` 的位置、下马清理、重复/乱序、断线重连和角色死亡后的模型生命周期没有闭合;本版本参考代码中 Ride/Unride 调用被注释,不能用本地 helper 自行扩大 authority。
|
||||
- 变身字段没有形成 native affect→shape→装备遮挡/动作资源的完整链,`EquipModel` 的 `_is_poly()` 仍是恒 false;宠物、马匹成长/耐力/马上技能/变身加攻防由本地脚本直接结算,而 40250 Windows 客户端没有对应本地 authority。
|
||||
- 所有 helper 测试通过只证明当前端自定义规则内部一致,不证明与参考客户端实现统一。
|
||||
|
||||
### Status update
|
||||
|
||||
本轮仍为 `PARTIAL`;完成坐骑、宠物和变身方向的合同审计并登记,未修改实现。
|
||||
|
||||
`PARTIAL`。当前端已具备 `mount_vnum` 的网络存储、mount_changed 信号、部分动作模式/旋转速度/攻击门控,并有本地马匹、宠物和变身 helper;但没有发现实际坐骑模型挂接链,变身状态尚未进入 native entity,宠物与马战/成长/耐力/伤害脚本承担了 40250 客户端没有承担的本地 authority。基础测试全部通过仍不足以证明与 40250 Windows 客户端实现统一。本轮完成审计并记录,未修改实现。
|
||||
@@ -0,0 +1,87 @@
|
||||
# world.npc_drop_actor_spawn
|
||||
|
||||
## Scope
|
||||
|
||||
审计 40250 Windows 客户端的 NPC、怪物、石头、建筑/墙体、玩家实体、地面掉落、所有权、私人商店招牌和实体清理链。判断标准是实体是否按相同的包字段、VID 生命周期、分类、可见性、模型/动作资源、名字尾标、掉落所有权和删除时序进入客户端;服务端掉落概率、散落规则或本地活动规则不能因为在当前端有脚本就当作 40250 客户端等价实现。
|
||||
|
||||
## Reference call chain
|
||||
|
||||
- `UserInterface/PythonNetworkStreamPhaseGameActor.cpp`
|
||||
- `RecvCharacterAppendPacket` / `RecvCharacterAdditionalInfo` / `RecvCharacterUpdatePacket` 解包 race、character type、parts、affect、位置、速度、名字、公会、等级、mount 等字段;不可见 race 不创建实例;角色新增最终进入 `CNetworkActorManager` / `CInstanceBase`,非零 mount vnum 由角色实例处理。
|
||||
- `RecvCharacterDeletePacket` 通过 actor manager 删除 VID,并同步通知私人商店消失。
|
||||
- `RecvOwnerShipPacket` 把 owner VID 与 victim VID 写入网络 actor manager。
|
||||
- `UserInterface/NetworkActorManager.cpp` / `UserInterface/PythonCharacterManager.cpp` / `UserInterface/InstanceBase.cpp`
|
||||
- actor manager 负责 add/update/move/delete、VID 查找和实例状态;character manager 负责碰撞、拾取、排序、距离可见性和渲染列表;`CInstanceBase` 按 race/type 选择模型、动作、名字和状态。
|
||||
- 40250 的可见性由 `CHAR_STAGE_VIEW_BOUND`、`AFFECT_SHOW_ALWAYS` 和 wall 判定共同决定;离开范围时走 delete/blend-out,数据与渲染实例不是同一层。
|
||||
- `UserInterface/PythonNetworkStreamPhaseGameItem.cpp` / `UserInterface/PythonItem.cpp` / `UserInterface/PythonTextTail.cpp` / `Packet.h`
|
||||
- `RecvItemGroundAddPacket` 将全局坐标转本地坐标后调用 `CPythonItem::CreateItem(vid, vnum, x, y, z)`;未知 item data 不创建地面物体。
|
||||
- `RecvItemOwnership` 调用 `SetOwnership(vid, name)`,更新独立 owner 尾标;`RecvItemGroundDelPacket` 删除地面实例并删除对应 item text tail。
|
||||
- `CreateItem` 从 `CItemData::GetDropModelThing()` 建立掉落模型,注册 item text tail;`GetCloseMoney` / `GetCloseItem`、模型拾取和网络 `CG_ITEM_PICKUP` 负责交互,服务器回包才完成真正拾取。
|
||||
- `UserInterface/PythonNetworkStreamPhaseGame.cpp`
|
||||
- `RecvShopSignPacket` 对空 sign 触发 `BINARY_PrivateShop_Disappear`,非空 sign 触发 `BINARY_PrivateShop_Appear(vid, sign)`;该包不是普通实体 spawn,必须依赖已存在角色 VID。
|
||||
|
||||
## Current call chain
|
||||
|
||||
- `extension/src/net/classic/classic_parser.cpp` 已将 character add/additional/update/delete、`GC_OWNERSHIP`、`GC_ITEM_GROUND_ADD/DEL/OWNERSHIP` 和 `GC_SHOP_SIGN` 分别写入 `EntityStore`/ground store;`M2Client::poll` 将实体和地面变化转为 Godot signal。
|
||||
- `extension/src/net/entity_store.cpp/.h` 以 VID 保存实体,spawn 是整行替换;地面掉落独立保存 `vid/vnum/pos/owner`,shop sign 是已存在实体的可变字段;未知 VID 的 shop sign/owner 不凭空创建实体。
|
||||
- `project/net_world.gd` 在数据层保存全部实体,在场景层按主角距离、`AFFECT_SHOW_ALWAYS` 和 wall race 创建/淡出节点;`_on_spawn` 写入分类、名字、parts、affect、速度、mount 和 shop sign,`_on_info` 刷新可变字段;`project/ui/mob_view.gd` 负责 NPC/怪物模型、动作、受击和名字补点。
|
||||
- `project/ui/ground_items.gd` 监听 ground add/del,按 item proto 解析名字/颜色,加载 item bag/money 模型或 fallback BoxMesh,创建名字与 owner 尾标、稀有掉落光柱,按距离和 500ms 节流发送拾取;服务端 ground del 负责最终消失。
|
||||
- `project/entity_rules.gd`、`project/metin_spawn_event_system.gd`、`project/mob_drop_scatter.gd` 等本地 helper 还承担木门/建筑分类、石头活动、掉落散落和所有权保护等自定义规则;这些不等于 40250 Windows 客户端本身的 authority。
|
||||
|
||||
## Branch matrix
|
||||
|
||||
| 场景 | 40250 | 当前端 | 结论 |
|
||||
|---|---|---|---|
|
||||
| 角色/NPC/怪物新增 | add packet→actor manager→`CInstanceBase`→模型/尾标;不可见 race 跳过 | parser/store→`entity_spawned`→`NetWorld`,可延迟 placeholder 后升级真模型 | PARTIAL:字段链存在,但不可见 race、未知 race、真资源缺失时的失败分支未完整证明 |
|
||||
| character additional/update | 按包刷新 parts、名字、公会、affect、速度、状态和 shop 相关数据 | `_on_info`/`_apply_field_updates` 按顺序刷新部分字段 | PARTIAL:顺序入口有,角色类型、异常字段、重复/乱序包和实际 model attachment 缺 live 证据 |
|
||||
| VID 重用 | 删除旧实例后重新 add;actor manager 查找不应残留旧状态 | EntityStore whole-row spawn,NetWorld 销毁并重建节点 | PARTIAL:设计上接近,但旧节点延迟释放与同 VID 新 spawn 的边界没有完整回归 |
|
||||
| 远端删除/淡出 | actor manager 删除;尾标、商店和交互引用同步清理 | `entity_despawned`→`_fade_and_free`,数据层/场景层分离 | PARTIAL:删除链存在,blend-out 期间重复 spawn、目标/尾标/交互引用的完整时序仍需验证 |
|
||||
| 距离可见性 | `CHAR_STAGE_VIEW_BOUND`、always-show、wall 分支控制实例和渲染列表 | `_update_visibility` 每帧以 20000cm/20010cm、affect bit、wall race 建/淡出 | PARTIAL:基础分支已映射;距离常量、真实 actor manager 淡出和复杂 wall/always-show 组合仍未完全证明 |
|
||||
| 建筑/墙体/门 | 由 race 与 InstanceBase/碰撞数据决定,墙体不因距离错误消失 | `entity_rules`/cursor shape 识别 door/building,wall race 强制可见 | PARTIAL:分类与 cursor 入口有;模型、碰撞、阻挡和真实 wall 生命周期未统一证明 |
|
||||
| 地面掉落新增 | 坐标转换→CItemData drop model→item text tail;未知 vnum 不创建 | ground store→`ground_items`,item bag/money 或 BoxMesh fallback→Label3D | PARTIAL:协议、坐标、名字尾标和 fallback 有;真实 drop model/effect/sound/未知 vnum 失败语义不等价 |
|
||||
| 地面掉落所有权 | `RecvItemOwnership` 更新独立 owner text tail;服务器仍是拾取 authority | owner 写入 ground row/owner_tag,并在本地 pickup 前做名字判断 | PARTIAL:回包链存在;本地 owner precheck 是额外 optimistic gate,party/anti-flag/native action gate 未完整接入 |
|
||||
| 地面掉落删除/拾取 | `GC_ITEM_GROUND_DEL` 同步删除模型和尾标;拾取成功依赖服务器包序 | ground del signal 删除节点,pickup 只发送请求 | PARTIAL:删除基础链有;`CG_ITEM_PICKUP→GC_DEL→ITEM_GET/ITEM_SET/UPDATE` 的 live 成功、失败、乱序和回滚未证明 |
|
||||
| 私人商店招牌 | `GC_SHOP_SIGN` 空/非空走 disappear/appear,依赖已有角色 VID | `shop_sign` 进入实体 info,`_refresh_shop_sign` 创建/释放 Label3D | PARTIAL:空/非空状态可映射;BINARY 私人商店 UI、重复/未知 VID、重连清理和视觉资源不等价 |
|
||||
| 怪物死亡与掉落 | 死亡/删除/地面掉落由服务端包和 actor/item manager 驱动 | `mob_view` 死亡表现,活动 helper 可生成本地掉落 | GAP(custom authority):本地散落/活动掉落不能作为 40250 client parity,live server death→drop 时序仍缺证据 |
|
||||
|
||||
## Implementation-equivalence matrix
|
||||
|
||||
| 项目 | 状态 | 证据/限制 |
|
||||
|---|---|---|
|
||||
| Preconditions | PARTIAL | stage、VID、实体/ground store 和部分分类存在;未知 race/vnum、主 VID、模型工厂未就绪和 owner/anti-flag 门控未完整证明 |
|
||||
| Branch structure | PARTIAL | actor add/info/update/delete、ground add/del/owner、shop sign 主要分支存在;visibility、不可见 race、未知数据和 server rejection 分支不等价 |
|
||||
| Algorithms/formulas | PARTIAL | 坐标转换、20000cm 可见范围、owner 标签和拾取距离有实现;CItemData 模型选择、碰撞/拾取几何、drop effect 和参考资源算法未统一 |
|
||||
| State transition order | PARTIAL | parser→store→signal→view 顺序明确;延迟模型升级、淡出、同 VID 重用、ground del 与 owner/sign 乱序的可观察顺序未闭合 |
|
||||
| Constants/units | PARTIAL | `CHAR_STAGE_VIEW_BOUND`、+10 cull、ground height/pickup 距离、packet fields 有证据;Godot world unit 与 40250 pixel/cm、文本像素度量、模型锚点仍是近似 |
|
||||
| Timing/event sources | PARTIAL | poll、ground signal、500ms pickup throttle、visibility interval 有;真实 frame cull、blend-out、drop animation/effect/sound、server packet delay 未覆盖 |
|
||||
| Resource/data sources | PARTIAL | mob/NPC 资源扫描和部分真模型测试通过,但有 4 个 fallback;ground 使用通用 bag/BoxMesh,未证明所有 CItemData drop model、特效、音效和牌匾资源 |
|
||||
| Protocol side effects | PARTIAL | classic parser/session 与 ground/ownership/shop sign 基础路径存在;完整拾取回包、商店 UI、不可见实体过滤和未知 packet 语义缺 live 证据 |
|
||||
| Interruption/failure/cleanup | PARTIAL | `net_world_vis_test.gd` 的基本可见性/重显/删除回归已通过;地图切换基础清理已有,但实体淡出、地面掉落、尾标、商店、同 VID 重用、断线/重连和模型 fallback 的组合清理未完整证明 |
|
||||
|
||||
## Regression evidence
|
||||
|
||||
- `project/net_world_vis_test.gd`:通过;覆盖主角进入范围后的远端节点创建、单次创建、离开范围裁剪和删除信号去重。仍不证明真实 actor manager 的 wall/always-show/模型失败组合。
|
||||
- `project/entity_rules_test.gd`:通过;验证实体分类/可攻击规则,但不证明 40250 的模型、碰撞和 visibility chain。
|
||||
- `project/forest_mob_render_test.gd`:24 个结果、0 failures、4 fallback;有 Godot ObjectDB leak warning,且仍需人工资源 sign-off,因此不能标记 PASS。
|
||||
- `project/mob_view_test.gd`、`project/mob_winding_test.gd`、`project/net_world_push_test.gd`:通过;覆盖单体模型/绕序/受击推开,不能证明完整 actor manager 生命周期。
|
||||
- `project/test_ground_pickup_parity.gd`、`project/mob_death_drop_test.gd`、`project/test_mob_drop_scatter_parity.gd`、`project/test_metin_spawn_event_parity.gd`:通过;主要覆盖当前端 helper、掉落散落和活动石头规则,不等于 40250 live drop/model/pickup parity。
|
||||
- `build/extension/net_entity_test`、`build/extension/net_classic_session_test`、`build/extension/net_classic_wire_test`:覆盖部分实体、地面包和协议边界;未覆盖上述 visibility 失败、真实资源失败、完整拾取回包、商店牌匾和重连组合清理。
|
||||
|
||||
### Active GameLib spawn/property review
|
||||
|
||||
- `GameLib/MonsterAreaInfo.cpp` 保存刷怪区域 origin/size、左右上下边界、数量、随机初始位置、方向、group/leader/VID/name 和清理默认值;`Property.cpp`/`PropertyLoader.cpp`/`PropertyManager.cpp` 为地图 object/property 注册提供 CRC 和 token 数据。
|
||||
- 当前实体生成主要来自服务端 spawn packet 和 `NetWorld`,地图静态刷怪区域/property 并没有一条等价的客户端本地 `MonsterAreaInfo` 消费链;因此本轮只确认参考数据模型和清理边界,不能把网络实体测试当作静态刷怪 parity。
|
||||
|
||||
### Active UserInterface NPC data review
|
||||
|
||||
- `PythonNonPlayer.h`/`PythonNonPlayerModule.cpp` 以 MMPT mob table 为权威,提供 `LoadNonPlayerData`、vnum/name/type/event/color、VID event lookup 和按等级区间匹配的 mob list;`PythonCharacterModule.cpp` 再用 event type 决定角色 render/behavior。
|
||||
- 当前 `Metin2Proto`/`EntityStore` 能解析 mob proto 并生成网络实体,但 event type、mob color/name、等级筛选和 unknown vnum/缺记录失败分支没有完整接入 `NetWorld`/NPC view;本轮静态证据确认“能生成一个怪物”不等于 NPC data chain 一致。
|
||||
- 本轮完成 UserInterface NPC 数据入口静态核对,合同保持 `PARTIAL`。
|
||||
|
||||
## Deep audit round 2026-09-20
|
||||
|
||||
本轮复跑 `project/net_world_vis_test.gd` 时发现固定 100ms 可见性节流会让快速连续帧的结果不稳定;40250 `CPythonCharacterManager::Update` 每帧执行距离检查,因此已改为每帧调用 `_update_visibility()`。连续两次测试均 PASS。该结果只证明当前 Godot 可见性状态机的基础边沿,不足以把 40250 的 actor manager 淡出、wall 判定和真实资源失败分支标为等价。
|
||||
|
||||
## Status
|
||||
|
||||
`PARTIAL`。当前端已经具备 40250 实体/掉落协议的主要 native parser、VID 数据层、NPC/怪物场景层、地面物体和 shop sign 状态入口;可见性基础回归和每帧更新边界已修复,但掉落模型/特效/音效、建筑碰撞、完整 actor/ground 生命周期和 live 拾取/商店回包证据仍不足。
|
||||
@@ -0,0 +1,136 @@
|
||||
# world.reset_cleanup
|
||||
|
||||
## Scope
|
||||
|
||||
审计 40250 在进入 Loading、换图、死亡/断线和退出时,对网络状态、Actor、地面物品、飞行物、特效、地图和临时 UI 的清理边界与顺序。
|
||||
|
||||
## Reference call chain
|
||||
|
||||
- `CPythonNetworkStream::SetLoadingPhase`
|
||||
- 执行旧阶段 leave callback。
|
||||
- 切到 Loading 并设置 Loading 回调。
|
||||
- 调用 `CPythonPlayer::Clear()`。
|
||||
- 调用 `CFlyingManager::Instance().DeleteAllInstances()`。
|
||||
- 调用 `CEffectManager::Instance().DeleteAllInstances()`。
|
||||
- 调用 `__DirectEnterMode_Initialize()`。
|
||||
- `CNetworkActorManager::Destroy` / `__RemoveAllActors` / `__RemoveAllGroundItems`
|
||||
- 清空 Actor 字典、主 VID、角色实例、地面物品。
|
||||
- `CMapManager::LoadMap` / `UnloadMap` / `Clear`
|
||||
- 先离开/清理当前地图,再加载 property、terrain、object 和 environment,最后 Enter 新地图。
|
||||
- `CPythonApplication::Destroy`
|
||||
- 按 manager 所有权释放窗口、角色、地图、特效、网络、声音和设备资源。
|
||||
|
||||
## Current call chain
|
||||
|
||||
- `M2Client` / `EntityStore::reset_for_map_change`
|
||||
- 清空实体、目标、战斗/交互状态以及 Loading 前产生的临时网络队列。
|
||||
- `NetPlay.clear_for_map_change`
|
||||
- 对齐 `CPythonPlayer::Clear()`,清空当前 Actor 的目标、预约、连击、命中窗、击退同步、技能/钓鱼/攻击门和经验基线。
|
||||
- `GameScene::_on_world_reset`
|
||||
- 重置主角同步尝试、调用 `NetWorld.clear_for_map_change()` 与地面物品清理,关闭临时窗口并停止 BGM。
|
||||
- `NetWorld.clear_for_map_change`
|
||||
- 清理 Actor 节点、HP 条、伤害队列、主 VID、PvP 和决斗状态,并调用 `FlyObject.clear_for_map_change()`。
|
||||
- `FlyObject.clear_for_map_change`
|
||||
- 清理活动弹道、弹道视觉节点和爆点视觉节点。
|
||||
- `GameScene::_reload_map_for_warp`
|
||||
- 先对旧 `Metin2World` 调用 `unload_map()` 并 `queue_free()`,再创建并加载新 world;静态顺序已对齐参考端先 Leave/Clear 旧地图再 Load。
|
||||
- `Metin2World::unload_map`
|
||||
- 清理 chunks、stream queue、environment、objects root、光照和缓存;当前析构依赖 Godot 节点生命周期,没有独立的 Destroy 等价层。
|
||||
- `AppFlow::_exit_tree` / `GameScene::_exit_tree` / GDExtension terminator
|
||||
- 在整个客户端退出时清空模型纹理、动作片段、NPC 模型规格、UI 纹理和音频流/播放缓存,补齐 `CPythonApplication::Destroy` 的进程级资源释放边界。
|
||||
|
||||
## Field-level findings
|
||||
|
||||
- 40250 的 `SetLoadingPhase` 先运行旧 phase leave callback,再把网络阶段切到 Loading,随后依次执行 `CPythonPlayer::Clear`、`CFlyingManager::DeleteAllInstances`、`CEffectManager::DeleteAllInstances` 和 `__DirectEnterMode_Initialize`。当前 classic warp 在 `M2Client::pump_classic` 中先直接 `EntityStore::reset_for_map_change()`,再同步发 `world_reset`/`warp`,最后才替换 socket/stage;网络阶段没有独立的 Loading owner,顺序只能算近似。
|
||||
- 当前 `EntityStore::reset_for_map_change` 会清实体、变化队列、目标、飞行 cue、effect cue、掉落、PVP、观察者、技能、快捷栏、冷却和 points 基线;这是数据层完整清理,但它把 pending change 直接丢弃,不产生参考端 manager 的显式删除事件,场景层必须依赖 `world_reset` 同步清理。
|
||||
- `GameScene::_on_world_reset` 再清 NetPlay、NetWorld、GroundItems、临时窗口、UI top stack 和 BGM;信号连接是同步调用,但 `queue_free` 本身延迟到 frame 末,因此旧节点可能在清理回调后短时间仍存在。
|
||||
- `NetWorld.clear_for_map_change` 现在除遍历 `_by_vid` 外,还会清 `_upgrade_queue`、`_chat_tails`、target/hover VID,并解绑 target effect;但模型升级、HP 条和 `queue_free` 延迟回调仍没有统一 generation,未在 `_by_vid` 的其他附属节点仍依赖各组件自己的 guard。
|
||||
- `M2Client::disconnect_from_server` 现在先对活动 `ClassicSession`/`NetClient` 的 EntityStore 做 `reset_for_map_change()`,再发出 `world_reset`,最后释放 transport;这对齐 40250 离线阶段先清 phase owner、后清网络对象的边界。真实断线由 session 失败事件驱动的重连顺序仍需独立包级证明。
|
||||
- `GameScene._exit_tree` 在卸载 native world 前调用 `Audio.shutdown()`,同步停止 BGM/3D 播放器并清空 MP3/MSS stream cache;这补齐了 `CSoundManager::Destroy` 的所有权边界,避免场景节点销毁后音频资源继续存活。
|
||||
- 已修正 `screen_hp_bar_test` 对死亡后旧 HP 控件的错误访问:40250 的 `Die/DetachTextTail` 语义是移除旧尾标,回归现在检查控件从 world registry 移除,并通过 `entity_info` 验证重新进入可见性路径;`rendering_scenario_test` failures=0 但退出码 139,仍存在退出阶段资源/对象释放风险。
|
||||
|
||||
## Deep audit round 2026-09-20
|
||||
|
||||
本轮继续沿 40250 的 Loading、断线和 Destroy 路径做了静态复核,并重新执行清理相关回归:
|
||||
|
||||
- 40250 的 `SetLoadingPhase()` 通过 phase leave callback 先退出旧阶段,再由同一个网络阶段 owner 依次清 `CPythonPlayer`、飞行实例、特效实例和 direct-enter 状态;`SetOffLinePhase()` 也会重置主 Actor、选人和 direct-enter 状态。当前 `M2Client::pump_classic()` 仅在 `GC_WARP` 分支发 `world_reset`,而 `M2Client::disconnect_from_server()` 只销毁 session/stream 和回到 Idle,不发 `world_reset`。因此保留 `GameScene` 的断线重连分支不能依赖网络对象析构完成旧场景清理。
|
||||
- 当前 `EntityStore::reset_for_map_change()`、`NetPlay.clear_for_map_change()`、`GameScene._on_world_reset()`、`NetWorld.clear_for_map_change()` 和 `FlyObject.clear_for_map_change()` 的幂等性主要来自各自清空容器;它们没有统一的 reset generation。`_upgrade_queue`、目标特效、模型升级回调、HP 条和 `queue_free` 延迟回调仍由各节点分别判断,旧回调回灌到新地图的安全性没有证明。
|
||||
- `GameScene._exit_tree()` 现在显式调用当前 world 的 `unload_map()`;`AppFlow._exit_tree` 和渲染回归退出前还会清空模型纹理、动作片段、NPC 规格和 UI 纹理缓存。此前的 `rendering_scenario_test.gd` 退出码 139/54 个 ObjectDB 泄漏在该清理链补齐后已变为干净退出码 0。`fly_reset_test.gd` 中由 `FlyInstance.handler` 自捕获形成的 5 个 `RefCounted` 环也已解除;当前仍有 `test_exp_fly_parity.gd` 旧式 fixture 的 3 个退出警告,属于测试 harness 清理问题。`dungeon_state_test.gd`、`bgm_test.gd` 和 `net_entity_test` 通过,只能证明局部清理。
|
||||
|
||||
本轮结论:核心实体、飞行物和临时状态有清理实现,但断线重连、统一 generation、同步/延迟释放边界、旧新地图共存和进程退出安全仍未达到 40250 的统一生命周期语义,合同保持 `PARTIAL`。
|
||||
|
||||
## Implementation fix round 2026-09-22T06:45Z
|
||||
|
||||
本轮按 `CPythonNetworkStream::SetLoadingPhase` 的 manager 清理边界,先为 `NetWorld` 增加失败回归 `project/net_world_reset_test.gd`。修复前该回归稳定复现三项残留:换图后 `_upgrade_queue` 仍保留旧 VID,`_chat_tails` 仍保留旧文本,target/hover VID 仍指向已删除 Actor。随后在 `NetWorld.clear_for_map_change` 中按 Loading 清理顺序解绑 target effect、清空模型升级队列和聊天尾标,并归零 target/hover 引用。
|
||||
|
||||
同时修正 `screen_hp_bar_test.gd`:死亡会销毁/移除旧 HP 条,不再访问已 `queue_free` 的旧 Control;复活/位置变化通过 `entity_info` 重新建立 presentation state 后检查镜头后的隐藏行为。修复后两个回归均干净退出且无 `previously freed` 输出。
|
||||
|
||||
本轮仍未补齐统一 reset generation、真实断线重连的完整包序、通用 EffectManager 和跨中断矩阵,因此合同保持 `PARTIAL`。
|
||||
|
||||
## Implementation fix round 2026-09-22T07:10Z
|
||||
|
||||
本轮继续追踪 `rendering_scenario_test` 的退出阶段差异。按 40250 `CMapManager::UnloadMap` / `CPythonApplication::Destroy` 的显式所有权顺序,修改 `GameScene._reload_map_for_warp`:旧 world 先调用 `unload_map()`、再排队释放,之后才创建新 world;新增 `GameScene._exit_tree`,在场景被直接释放时显式卸载当前 world。回归测试清理也改为先发 `world_reset`、卸载 native map,再在 frame boundary 由 SceneTree 释放节点。
|
||||
|
||||
该修复闭合了“新地图先构造、旧地图延迟清理”的静态顺序差异,但 `rendering_scenario_test` 修复后仍为 `failures=0`、退出码 139,并报告 54 个 ObjectDB 泄漏。因此 native model/resource 的最终释放、通用 EffectManager 和退出崩溃仍保持 `PARTIAL`,不能把渲染场景记为 PASS。
|
||||
|
||||
## Implementation fix round 2026-09-22T07:40Z
|
||||
|
||||
本轮继续追踪 139/泄漏的真实 owner。`Metin2Model`、`Metin2AnimPlayer`、`MobView` 和 `UiAssets` 都持有跨场景缓存;此前只卸载 world/节点不会释放这些进程级资源。按 40250 `CPythonApplication::Destroy` 的 manager 释放边界,新增 `AppFlow._exit_tree` 的模型纹理、动作片段、NPC 规格和 UI 纹理清理,并在真实资源渲染回归中按 `world_reset -> unload_map -> queue_free -> frame boundary -> cache clear` 执行退出。
|
||||
|
||||
修复前回归为 `failures=0`、退出码 139、54 个 ObjectDB 泄漏;修复后 `rendering_scenario_test` 为退出码 0,且不再打印 ObjectDB 泄漏。GDExtension terminator 也补充了 `Metin2Model::clear_texture_cache`,防止未经过 AppFlow 的宿主退出遗漏该 cache。随后按 `CFlyingManager::DeleteAllInstances` 的同步删除边界,`FlyObject` 在地图清理和自然过期时解除 `handler`、target、world、data 引用,`fly_reset_test` 的 5 个 ObjectDB 泄漏已清零;统一 reset generation、断线显式 world_reset 和通用 EffectManager 仍未闭合,合同保持 `PARTIAL`。
|
||||
|
||||
## Implementation fix round 2026-09-22T07:55Z
|
||||
|
||||
复核 `CFlyingManager::DeleteAllInstances` 时发现,当前 `FlyInstance.handler` 闭包捕获实例自身,而实例又持有该 Callable;只清理实例数组不能打破这个 `RefCounted` 环。新增 `_release_instance()`,在地图清理和自然过期两个删除路径中同步解除 handler、target、world、data 引用,并同步释放弹道/爆点视觉节点,保持 Loading 边界的确定性。
|
||||
|
||||
修复前 `fly_reset_test.gd` 行为断言通过但退出报告 5 个 ObjectDB 泄漏;修复后退出码 0 且无 ObjectDB 泄漏。`test_exp_fly_parity.gd` 的 6/6 行为断言仍通过,但旧式单函数 fixture 仍报告 3 个 ObjectDB/不在 SceneTree 的路径警告,单独记为 harness 待清理,不提升整个飞行合同状态。
|
||||
|
||||
## Implementation fix round 2026-09-22T08:35Z
|
||||
|
||||
在 Debug 原生库重新跑真实资源渲染回归时,断言虽为 0 且退出码为 0,但退出仍报告 2 个 `ObjectDB`:`AudioStreamMP3` 和 `AudioStreamPlaybackMP3`。按 40250 `CSoundManager::Destroy`,新增 `Audio.shutdown()`,停止 BGM/3D 播放器、解除 player stream,并清空 `_cache`/`_mss_cache`;`GameScene._exit_tree()` 在 world unload 前调用该 shutdown。
|
||||
|
||||
修复后 Debug 渲染回归不再报告 ObjectDB 泄漏;这次也确认了 Release/Debug 原生库必须分别重建和验证。通用特效 manager、统一 reset generation、真实断线/GC_WARP 包序仍保持 `PARTIAL`。
|
||||
|
||||
## Branch matrix
|
||||
|
||||
| 场景 | 40250 | 当前端 | 结论 |
|
||||
|---|---|---|---|
|
||||
| GC_WARP / 进入 Loading | 先切 Loading,再按固定顺序清理 Player、飞行物、特效和 Actor | `world_reset` 清理网络/场景状态,之后处理 warp;飞行物与效果队列已补齐 | PARTIAL:顺序仍缺包级证明 |
|
||||
| 同服换坐标 | 不重建地图,但 Loading/Actor 清理仍由阶段处理 | `_place_player_at_net_pos` 复位主角位置,`world_reset` 仍清理实体 | PARTIAL |
|
||||
| 跨服/跨图 | 旧地图 Leave/Clear 后加载新地图并 Enter | 旧 world 先 `unload_map`/`queue_free`,再构造新 world | STATIC FIXED;渲染退出缓存已补齐 |
|
||||
| 旧弹道/爆点 | `DeleteAllInstances`,不能跨 Loading 回调 | 已在网络层、`NetWorld` 和 `FlyObject` 清理弹道/爆点,并解除实例自捕获 handler | MAPPED + regression |
|
||||
| 旧模型升级/聊天/目标呈现 | Loading 时随 Actor/文本尾标/目标效果一起清理 | `_upgrade_queue`、`_chat_tails`、target/hover VID 和 target effect 在 `clear_for_map_change` 中清理 | FIXED + regression |
|
||||
| 旧特效/临时窗口 | EffectManager 和 transient UI 在 Loading 清理 | 场景清理临时窗口并关闭 top stack;通用特效 manager 仍未统一 | PARTIAL |
|
||||
| 退出/窗口关闭 | `Destroy` 显式释放所有 manager | AppFlow/GDExtension/Audio/GameScene 退出清缓存;显式 disconnect 先 reset world 再释放 transport,完整 close consumer 尚未闭环 | PARTIAL |
|
||||
|
||||
## Equivalence matrix
|
||||
|
||||
| 项目 | 状态 | 证据 |
|
||||
|---|---|---|
|
||||
| Preconditions | MAPPED | world reset 仅在已建立 GameScene 的 warp/loading 路径调用 |
|
||||
| Branch structure | PARTIAL | 同服、跨图、断线、窗口关闭和死亡分支尚未全部覆盖 |
|
||||
| Algorithms/formulas | MAPPED | 清理本身无复杂公式;地图坐标转换仍使用当前 MapCoord 适配 |
|
||||
| State transition order | PARTIAL | 参考端 Loading 是单一阶段 owner,当前由 session、M2Client、GameScene 和信号协作 |
|
||||
| Constants/units | MAPPED | 飞行/地图数据单位已在各自组件中转换;没有证明所有资源释放计时一致 |
|
||||
| Timing/event sources | PARTIAL | Godot `queue_free` 是延迟释放,参考端 manager 删除是同步调用;本轮仅证明旧 presentation state 不回灌 |
|
||||
| Resource/data sources | PARTIAL | 当前 world 使用 Godot child/RAII,参考端使用显式 manager ownership |
|
||||
| Protocol side effects | PARTIAL | GC_WARP、重连、close 与 world reset 的组合顺序仍缺完整 fixture |
|
||||
| Interruption/failure/cleanup | PARTIAL | 异步 map load 失败、重复 warp、旧 session 竞态和 close cleanup 尚未证明 |
|
||||
|
||||
## Regression evidence
|
||||
|
||||
- `extension/tests/net_entity_test.cpp`:map reset 丢弃实体、地面、目标、飞行、飞行目标和特效队列。
|
||||
- `project/fly_reset_test.gd`:活动弹道、弹道视觉和爆点视觉在 map reset 后全部消失。
|
||||
- `project/screen_hp_bar_test.gd`:实体/HP 条的 map reset 清理。
|
||||
- `project/net_world_reset_test.gd`:模型升级队列、聊天尾标、target/hover 引用和 live actor 的 Loading 清理;修复前失败 3 项,修复后通过。
|
||||
- `project/dungeon_state_test.gd`:world reset 清理地下城目标/到达状态,通过。
|
||||
- `project/bgm_test.gd`:world reset 后 BGM 选曲/音量状态,通过。
|
||||
- `project/gamescene_test.gd`:场景和动作/实体清理,通过。
|
||||
- `project/screen_hp_bar_test.gd`:修复死亡后访问已释放旧控件的问题,当前干净通过。
|
||||
- `project/rendering_scenario_test.gd`:修复前报告 `failures=0`、进程退出码 139、54 个 ObjectDB 泄漏;修复后退出码 0 且无 ObjectDB 泄漏,可作为退出资源清理回归证据。
|
||||
- `project/fly_reset_test.gd`:修复前退出报告 5 个 ObjectDB 泄漏;解除 `FlyInstance` handler 自捕获环后退出干净。
|
||||
- `project/rendering_scenario_test.gd`:Debug 原生库下音频清理前报告 2 个 `AudioStreamMP3/AudioStreamPlaybackMP3` 泄漏;接入 `Audio.shutdown()` 后退出干净。
|
||||
|
||||
## Status
|
||||
|
||||
`PARTIAL`。本轮已修复明确的跨图 presentation 残留、旧地图卸载顺序和渲染场景进程级缓存泄漏,但统一 reset generation、通用特效 manager、断线重连和完整中断矩阵仍需继续审计。
|
||||
@@ -0,0 +1,117 @@
|
||||
# world.terrain-water-collision
|
||||
|
||||
## Scope
|
||||
|
||||
比较 40250 的户外地图高度、对象高度、属性阻挡、水面层、地形/对象拾取射线、桥梁和地面贴合调用链;同时核对加载失败、区块边界、地图切换、资源清理以及水面动画时序。
|
||||
|
||||
## Reference call chain
|
||||
|
||||
- `GameLib/MapOutdoor.cpp::GetTerrainHeight` / `GetHeight`
|
||||
- 以 200cm `CELLSCALE` 在 `height.raw` 的 131×131 样本上按 TL-BR 对角线做三角插值。
|
||||
- `GetHeight` 在非 terrain-only 模式下通过 culling 的 `CAttributeInstance::GetHeight` 合并桥梁、楼梯和平台等 `.mdatr` 对象高度,取对象/地形最大值。
|
||||
- `GameLib/AreaTerrain.cpp::CTerrain::GetHeight`
|
||||
- 使用 `HeightScale`、跨区块边界高度样本和固定的左/右三角分支;地图外返回 0。
|
||||
- `GameLib/MapOutdoor.cpp::isAttrOn` / `GetAttr` / `CMapManager::isPhysicalCollision`
|
||||
- 世界厘米坐标转换为 100cm 属性格;bit flag 使用按位判断,16 以上的属性值使用高半字节相等规则。
|
||||
- `isPhysicalCollision` 只判断 `ATTRIBUTE_BLOCK`,并把 Metin2 的 Y 取反后交给地图属性查询。
|
||||
- `PRTerrainLib/Terrain.cpp::LoadWaterMapFile` / `GameLib/AreaTerrain.cpp::GetWaterHeight`
|
||||
- 读取 magic=5426、128×128 水区块、layer count 和 u16/u32 水高;水 id 映射到具体层高,错误加载会清空水图并返回失败。
|
||||
- `MapOutdoorWater.cpp` 按水深计算顶点 alpha,并以 70ms/帧循环 30 张水纹理;水面共享 0 到 -15cm 的 1000~3000ms 高度扰动。
|
||||
- `GameLib/MapOutdoor.cpp::GetPickingPointWithRay`
|
||||
- 先在 culling 对象上做 `CAttributeInstance::Picking`,再以 5/10/100cm 自适应步长和 5000/10000/100000cm 上限探测地形,返回离射线起点更近的对象或地形命中。
|
||||
- `EterLib/AttributeInstance.cpp::GetHeight` / `Picking`
|
||||
- 对 `.mdatr` 高度三角形做 2D 包围盒、三角形和法向求交;对象命中包含可拾取的静态桥梁/平台,而不是只有网络角色。
|
||||
|
||||
## Current call chain
|
||||
|
||||
- `formats/terrain_files.cpp::load_height_map` / `load_attr_map` / `load_water_map`
|
||||
- 读取 `height.raw`、`attr.atr` 和 `water.wtr`,验证尺寸、magic、数据长度并支持 u16/u32 水高。
|
||||
- `formats/terrain_mesh.cpp::terrain_height_at` / `build_terrain_mesh`
|
||||
- 已按 200cm 格、TL-BR 三角形和 `HeightScale` 生成 Godot 高度网格;`Metin2World::sample_height` 对已加载区块插值。
|
||||
- `extension/src/metin2_world.cpp::build_chunk`
|
||||
- 为每区块建立地形 Mesh/HeightMapShape3D、属性对象 `.mdatr` 碰撞体、对象高度三角形和水面 Mesh;区块根节点统一卸载。
|
||||
- `extension/src/metin2_world.cpp::sample_height`
|
||||
- 合并裸地形和已加载对象 `.mdatr` 高度,桥梁/平台命中时取更高表面;边界支持邻接区块的已加载对象查询。
|
||||
- `extension/src/metin2_world.cpp::sample_attribute` / `is_blocked`
|
||||
- 读取 100cm 属性格;当前调用方主要使用 `ATTRIBUTE_BLOCK` 和 `ATTRIBUTE_WATER` 的 bit0/bit1。
|
||||
- `extension/src/water_builder.cpp::build_chunk_water` / `water_depth.h` / `water_motion.h`
|
||||
- 每个水格保留四角深度 alpha,使用 400×HeightScale 的透明度公式、30 帧 shader 和共享高度扰动。
|
||||
- `project/player_controller.gd::_ray_ground`
|
||||
- 通过每 1m 采样加二分寻找 `sample_height` 地表;实体点击单独使用网络实体 AABB/胶囊近似。
|
||||
- `project/net_world.gd::_ground_world` / `_blocked` / `_pushed_position`
|
||||
- 网络实体和主角贴 `sample_height`;移动/击退通过 `is_blocked` 截断。
|
||||
|
||||
## Audit findings (2026-09-20)
|
||||
|
||||
### 已对齐的部分
|
||||
|
||||
- `terrain_height_at` 的厘米单位、200cm 格、TL-BR 对角线、`xdist <= ydist` 三角分支和 `HeightScale` 已与 `CTerrain::GetHeight` 对齐;真实 A1/C1 桥面测试证明 `.mdatr` 高度会优先于河床。
|
||||
- `attr.atr` 的 2634 magic、256×256 数据、100cm 属性格和 bit0 阻挡/bit1 水面查询已有实现;移动、击退和钓鱼入口都使用同一属性读取路径。
|
||||
- `water.wtr` 的 5426 magic、128×128 id、layer count、u16/u32 高度兼容和失败尺寸检查已有实现;水面四角深度、浅滩透明度、动画纹理与共享高度扰动已有回归。
|
||||
- 桥梁、平台等 `.mdatr` 高度三角形已进入 `sample_height`,区块卸载由根节点统一回收;A1/C1 全地图高度探针未发现桥面掉到河床的回归。
|
||||
|
||||
### 材料差异
|
||||
|
||||
- 40250 的 `GetPickingPointWithRay` 同时比较静态对象 `CAttributeInstance::Picking` 和地形,并采用 5/10/100cm 自适应步长;当前 `PlayerController::_ray_ground` 只比较 `sample_height`,静态建筑/桥梁的射线命中没有进入点击结果。网络实体点击也只是节点 AABB/胶囊近似,尚未复用统一的世界拾取合同。
|
||||
- 40250 暴露 `GetWaterHeight(int,int)`,钓鱼抛竿会把 600cm 目标点的 Z 设置为具体水层高;当前端 `get_fishing_rotation` 只验证 `ATTRIBUTE_WATER`,`_new_fishing` 没有读取水层高来生成等价的鱼钩目标位置。水面可见且钓鱼方向可判定,不代表抛竿位置已等价。
|
||||
- `CTerrain::isAttrOn` 对 `byAttr >= 16` 使用高半字节相等规则;当前 `sample_attribute` 只返回原始字节、`is_blocked` 只支持 bit0,尚没有通用的 `is_attr_on` 兼容接口,因此 `BANPK` 以外的高半字节调用方缺少等价证据。
|
||||
- 40250 的 `GetHeight` 会根据 `m_bEnableTerrainOnlyForHeight` 选择是否合并 culling 对象,并且对象高度查询、地形加载状态、地图坐标边界有明确返回分支;当前 `sample_height` 依赖区块已被实际加载以及 `objects_enabled/registry_ok`,资源未加载或 streaming 队列中时会暂时回退为裸地形,缺少“查询触发加载/延迟重试”的同等状态契约。
|
||||
- Godot `HeightMapShape3D`/`.mdatr` 碰撞体和逻辑 `is_blocked` 是两套并行碰撞来源;当前角色移动主要走属性逻辑,静态对象物理体主要服务射线/弹道,尚没有证明两者在斜坡、对象边缘、跨区块和卸载帧的结果与 40250 `isPhysicalCollision`/`CAttributeInstance` 顺序一致。
|
||||
- 当前水面视觉测试覆盖 geometry/material 和真实 A1 水深,但没有验证 `water id -> layer height -> fishing target Z` 的运行时链,也没有覆盖缺失水图后水状态清零、边界水格和地图切换期间 shader/motion 状态的完整清理矩阵。
|
||||
- `TerrainDecal` 的高度贴花三角选择、`MapOutdoor` 的 normal/shadow/visibility 依赖和当前地形 patch 视锥剔除尚未形成可执行的功能合同;它们不阻塞当前移动主链,但不能因地形网格测试通过而宣称渲染/拾取完全一致。
|
||||
|
||||
## Equivalence matrix
|
||||
|
||||
| 项目 | 状态 | 备注 |
|
||||
|---|---|---|
|
||||
| Preconditions | PARTIAL | 资源尺寸/magic/区块加载已有检查;对象拾取、水层目标、高半字节属性和 streaming 未闭合 |
|
||||
| Branch structure | PARTIAL | 高度、属性、水面主分支已映射;对象/地形射线选择、地图外、延迟加载和水失败分支未全等价 |
|
||||
| Algorithms/formulas | PARTIAL | 高度插值、桥面取高、水深 alpha 已对齐;拾取步长、对象 picking 和水高接口不同 |
|
||||
| State transition order | PARTIAL | 区块构建→地形/碰撞/水面→对象高度基本一致;加载队列、点击命中优先级、钓鱼目标 Z 顺序未闭合 |
|
||||
| Constants/units | PARTIAL | 200cm terrain cell、100cm attr cell、600cm fishing probe、水深/动画常量已记录;射线 5/10/100cm 与当前 1m 采样不同 |
|
||||
| Timing/event sources | PARTIAL | 水纹 70ms 和扰动时序已有实现;streaming、地图卸载、拾取/钓鱼事件的端到端时序证据不足 |
|
||||
| Resource/data sources | PARTIAL | height/attr/water/mdatr 可读取;对象 culling、water layer 查询、terrain normal/decal/shadow 统一资源入口未完成 |
|
||||
| Protocol side effects | PARTIAL | 属性阻挡/钓鱼方向会影响移动和发包;水层高度未进入抛竿副作用,静态对象点击也未统一 |
|
||||
| Interruption/failure/cleanup | PARTIAL | 区块根节点卸载和基本加载失败已有路径;水状态、待处理拾取、streaming 中断和地图切换边界仍缺完整矩阵 |
|
||||
|
||||
## Regression evidence
|
||||
|
||||
- `build/formats/formats_map_test`:高度插值、地形网格、地图格式和资源边界;当前无 `M2_ASSETS` 时跳过 live A1 断言。
|
||||
- `project/test_bridge_height_parity.gd`:真实 A1 两座桥的 `.mdatr` 桥面高度、河床回退和跨度采样。
|
||||
- `project/test_all_map_height_probe.gd`:A1/C1 真实地图桥面高度、地形回退、属性阻挡和跨跨度采样。
|
||||
- `project/water_reference_test.gd`:真实 A1 水格、四角深度 alpha、浅滩和水材质/动画结构。
|
||||
- `project/fishing_water_test.gd`:真实 A1 `ATTRIBUTE_WATER`、600cm 方向扫描和地图外拒绝。
|
||||
- `project/test_click_target_effect_parity.gd`:地面命中信号、网络实体点选和静态弹道障碍;该测试的地面段使用 mock world,不能替代 40250 静态对象 ray picking。
|
||||
- `project/player_move_test.gd`、`project/test_skill_ground_fix.gd`:移动阻挡和角色/技能贴地回归。
|
||||
|
||||
## Deep audit round 2026-09-21
|
||||
|
||||
### Fresh regression results
|
||||
|
||||
- `test_bridge_height_parity.gd`、`test_all_map_height_probe.gd`、`water_reference_test.gd`、`fishing_water_test.gd`、`player_move_test.gd`、`test_skill_ground_fix.gd` 的断言均通过;A1 水面为 `failures=0`,15 pieces、212587 vertices。
|
||||
- `test_click_target_effect_parity.gd` 断言通过,但进程报告 3 个 ObjectDB 和 1 个资源未释放;`fly_target_bounds_test.gd` 断言通过但 exit 139,并泄漏 48 个 ObjectDB。
|
||||
- `formats_map_test`、`net_bounds_test`、`net_entity_test`、`net_classic_session_test`、`net_classic_wire_test` 均退出码 0;formats live A1 断言因未设置 `M2_ASSETS` 跳过。
|
||||
|
||||
### Static parity conclusion
|
||||
|
||||
- 高度插值、桥面取高、属性 bit0/bit1、水图读取和局部水面渲染结构继续与参考接近;这部分测试不能证明完整拾取/碰撞等价。
|
||||
- 当前 `_ray_ground` 主要走 1m 采样的 `sample_height`,没有实现 40250 对静态 `.mdatr` 对象先做 `Picking`、再以 5/10/100cm 自适应步长比较对象与地形命中的统一射线链。
|
||||
- 水面方向检测尚未把 `water id -> layer height -> fishing target Z` 接入抛竿副作用;高半字节属性规则、streaming 中延迟重试和对象/地形双碰撞源也未闭合。
|
||||
- 点击/飞行和真实地图测试的泄漏/exit 139 表明跨图、区块卸载和特效/目标清理仍不能视为稳定等价。
|
||||
|
||||
### Active GameLib/PRTerrainLib terrain-core review
|
||||
|
||||
- `GameLib/MapOutdoor.cpp` 除高度/水面外还负责 terrain/area 初始化、environment 绑定、视锥、对象和地形 ray picking;`MapOutdoorQuadtree.cpp` 建立/销毁四叉树,`MapOutdoorIndexBuffer.cpp` 为不同 patch/warp 方向生成索引,`TerrainPatch.cpp` 同时维护硬件/软件地形顶点、水顶点、光照和 patch proxy。
|
||||
- `MapOutdoorCharacterShadow.cpp` 创建角色 shadow render target/depth surface、切换 viewport/render target 后恢复旧状态;当前 Godot terrain/splat/WorldEnvironment 有视觉替代,但没有同一设备资源、patch LOD、四叉树和 shadow pass 生命周期。
|
||||
- `PRTerrainLib/Terrain.h`/`TerrainType.h`/`TextureSet.cpp`/`TextureSet.h` 补充 height/normal/water/texture-set 的底层数据和纹理层状态。当前格式测试与 A1 水面测试覆盖读取结果,但未证明 TextureSet、patch、quadtree、shadow 与 streaming 卸载保持同一资源边界。
|
||||
- 本轮完成上述 GameLib/PRTerrainLib 活跃地图源码静态核对;静态网格与水面测试不能替代对象 picking、patch/culling、shadow 和资源清理等价性。
|
||||
|
||||
## Status
|
||||
|
||||
`PARTIAL`。地形插值、桥面高度、属性阻挡、水面渲染和钓鱼方向已经有较强实现与真实地图证据;静态对象射线优先级、具体水层高度、高半字节属性、streaming 边界和统一碰撞/清理时序仍未达到 40250 的一致实现。
|
||||
|
||||
### Active EterLib collision-core review
|
||||
|
||||
- `EterLib/AttributeData.cpp`/`AttributeInstance.h`/`CollisionData.cpp`/`CollisionData.h` 提供静态属性数据、碰撞形状、法线/命中信息和实例级查询;`CullingManager.cpp` 负责范围、射线、可见性注册和对象集合维护;`Decal.cpp` 还参与地表贴花的对象/高度依赖。
|
||||
- 这些源码确认参考端的碰撞/拾取不是单纯高度采样:对象注册、射线、属性实例和清理顺序共同决定桥梁/平台/遮挡结果。当前端的 `sample_height`、Godot `HeightMapShape3D`、逻辑 `is_blocked` 和对象 AABB/胶囊是并行近似,未统一成同一 culling/attribute query 链。
|
||||
- 因此本轮完成 `AttributeData`、`CollisionData`、`CullingManager`、`Decal` 的活跃源码静态核对,但没有把静态符号存在误记为 parity 通过;静态对象优先级、跨区块卸载和碰撞/贴花清理仍是 `PARTIAL`。
|
||||
Reference in New Issue
Block a user