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

137 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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、断线重连和完整中断矩阵仍需继续审计。