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

25 KiB
Raw Blame History

combat.death-exp-drop

Scope

比较普通攻击/技能命中后的动作事件、GC_DAMAGE_INFOGC_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_INFOGC_DEADGC_POINT_CHANGEGC_CREATE_FLYGC_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_changedvitals_changed、去重后的 entity_deadground_item_added/removedfly_cuedamage 发给 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_CHANGEPOINT_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::Diem_isRealDead 一次性门、清 affect、卸马鞍、OnUnselected/OnUntargeted;当前 _on_dead 依赖 dead_seenmeta 的外围去重,死亡视觉、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_FLYPOINT_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 会恢复主角控制。EntityStoreM2Client.dead_seen 同时修复正 HP 清除死亡边沿,避免复活后下一次死亡信号丢失。
  • test_combat_death_pickup_entry_parity.gdnetplay_test.gdnet_entity_test.cpp 覆盖死亡、移动清理、复活恢复和再次死亡所需的状态边沿;本轮未宣称 POINT_* 特效、真实服务端包序和跨图清理全部等价。

本合同继续为 PARTIAL:经验/掉落权威链、完整死亡动画/复活 UI、真实服务器包序和异常清理仍需后续审计。

Regression evidence

  • extension/tests/net_state_queue_testPOINT_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 / __OnPressItemCPythonItem::GetCloseItem/SetOwnership 与当前 GroundItems::try_pickup/try_pickup_vidNetPlay::pick_ground_itemEntityStore::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 在点击路径先用 GetGroundItemPositionNEW_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.gdmob_death_drop_test.gdcombat_parity_test.gdplayable_combat_test.gdgamescene_test.gdnet_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_itemCPythonPlayer::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.gdnetplay_test.gdmob_death_drop_test.gdtest_combat_death_pickup_entry_parity.gd。本轮仍未宣称 真实服务端攻击→死亡→经验→掉落包序、空 owner/重名、拾取竞争和重连 catch-up 已完全 等价,合同继续为 PARTIAL

深审轮次(2026-09-20

本轮重新沿 GC_DEAD -> entity_dead -> NetWorld/NetPlay/PlayerControllerGC_PLAYER_POINT_CHANGE -> POINT_EXP.amount -> _trigger_exp_gain 跟踪正式连接:

  • EntityStore 已把 POINT_EXP.amount 累加为一次性 exp_gainNetPlay._on_points 优先消费该增量;因此“服务端确实下发 POINT_EXP 但跨等级回零后没有经验反馈”的旧问题已有防回归证据。
  • GC_CREATE_FLY(FLY_EXP) 仍只触发 NetWorld._on_fly 的视觉球,_trigger_exp_gain 不再生成第二批球,权威经验和表现来源没有混用。
  • 当前 client.entity_deadNetWorld 中只消费 _by_vid 的实体;本地主角由 GameScene_local_node 单独持有,NetPlayentity_dead 回调只在死亡目标等于当前目标时清目标,未统一调用 PlayerController.stop、清自动攻击/预约动作、死亡 UI/GameOver 和复活恢复。该缺口仍是正式链路差异,不是测试 fixture 问题。
  • RecvPointChange 的参考端对所有 POINT_* 先执行 ShowPointEffect,再按等级、属性、技能、能量和金币分支刷新;当前 _on_points 主要更新 HUD/经验/等级,完整 point effect 与金币 amount 的专门副作用仍未闭合。

本轮执行 test_combat_death_pickup_entry_parity.gdmob_death_drop_test.gdcombat_parity_test.gdplayable_combat_test.gdgamescene_test.gdbuild/extension/net_state_queue_test 均通过;这些测试证明已有局部链路可工作,但没有把主角 GC_DEAD 门禁、 真实服务端包序和完整 POINT_* 分支纳入断言,因此合同继续保持 PARTIAL

Deep audit round 2026-09-20

本轮重新从 40250 RecvDeadPacketCInstanceBase::DieRecvPointChangeRecvCreateFlyPacket 对到当前 parser/store/UI 消费链:

  • 40250 主角 GC_DEAD 会先触发非决斗 OnGameOverNotifyDeadMainCharacter 清自动攻击,再进入一次性 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.gdmob_death_drop_test.gdcombat_parity_test.gdplayable_combat_test.gdgamescene_test.gdnet_state_queue_test 均退出码 0;通过项覆盖局部战斗门、死亡怪物、掉落、经验增量和 map reset,不覆盖主角 GC_DEAD 门禁、完整 POINT_* 副作用及服务端攻击→死亡→经验→掉落包序。

结论:经验增量和基础掉落/死亡表现已可回归,但主角死亡状态门、完整 point effect、重复/乱序/复活清理和真实服务器包序仍与 40250 不同,合同保持 PARTIAL

Audit round 2026-09-20T19:03Z

本轮重新核对 RecvDeadPacketCInstanceBase::DieRecvPointChangeRecvCreateFlyPacket 与当前 parser/store/bridge 的完整边界,并重跑死亡、经验和掉落回归:

  • 上一轮补的主角死亡门现在已形成可验证闭环:NetPlay::_on_entity_dead 清除自动攻击、 预约动作、技能/钓鱼状态并调用 PlayerController.set_dead(true);正 HP 的 vitals_changedGC_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.gdmob_death_drop_test.gdcombat_parity_test.gdplayable_combat_test.gdgamescene_test.gdnet_state_queue_testnet_entity_test,均退出码 0;合同继续为 PARTIAL

Implementation fix round 2026-09-20T19:17Z

本轮按 40250 RecvPointChangeCPythonCharacterManager::ShowPointEffectCPythonPlayer::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_STEPPOINT_STAT_RESET_COUNT 及四维属性常量。
  • NetWorldPOINT_LEVEL / POINT_LEVEL_STEP 按 40250 注册的 level_up.mse / skillup_1.mse 资源在对应 VID 上播放,并在远端等级变更后刷新 LevelTagNetPlay 对金币正 amount 发出 money_picked / HUD on_pick_money 并发出属性、技能窗口刷新请求;SkillUI 监听点数事件。
  • net_entity_test.cppnetplay_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/NotifyDeadMainCharacterRecvPointChange -> POINT_EXP.amountRecvCreateFlyPacket 和地面物品包:

  • 主角死亡的 PlayerController 输入门、自动攻击清理、复活正 HP 边沿、远端死亡目标清理和经验增量一次性消费均有测试证据;本轮未发现上一轮已修复分支回归。
  • POINT_EXP.amount 仍是经验的权威增量,FLY_EXP 只负责视觉;死亡、掉落坐标/贴地、owner 文本/拒绝、拾取距离和删除主路径回归通过。
  • 仍不等价的分支保持明确:真实服务端攻击→伤害→死亡→POINT_EXPCREATE_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.gdfly_reset_test.gdnetbridge_test.gdtest_combat_death_pickup_entry_parity.gdmob_death_drop_test.gdgamescene_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.gdfly_reset_test.gdnetbridge_test.gdtest_combat_death_pickup_entry_parity.gdmob_death_drop_test.gdgamescene_test.gdcombat_fx_test.gd 全部通过。合同仍为 PARTIAL,真实服务端 攻击→死亡→经验→掉落包序、重复/乱序/重连/拾取竞争和跨图清理仍未闭合。