- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧 - inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域 - 包含其他累积的功能开发和测试文件
15 KiB
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()。
- 清理 Actor 节点、HP 条、伤害队列、主 VID、PvP 和决斗状态,并调用
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的进程级资源释放边界。
- 在整个客户端退出时清空模型纹理、动作片段、NPC 模型规格、UI 纹理和音频流/播放缓存,补齐
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_testfailures=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 泄漏;解除FlyInstancehandler 自捕获环后退出干净。project/rendering_scenario_test.gd:Debug 原生库下音频清理前报告 2 个AudioStreamMP3/AudioStreamPlaybackMP3泄漏;接入Audio.shutdown()后退出干净。
Status
PARTIAL。本轮已修复明确的跨图 presentation 残留、旧地图卸载顺序和渲染场景进程级缓存泄漏,但统一 reset generation、通用特效 manager、断线重连和完整中断矩阵仍需继续审计。