- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧 - inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域 - 包含其他累积的功能开发和测试文件
137 lines
15 KiB
Markdown
137 lines
15 KiB
Markdown
# 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、断线重连和完整中断矩阵仍需继续审计。
|