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

281 lines
25 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.
# 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`,真实服务端
攻击→死亡→经验→掉落包序、重复/乱序/重连/拾取竞争和跨图清理仍未闭合。