Files
mtgodot-poc/audit/contracts/combat.target-selection-rules.md
T
shenleiandClaude Opus 5 a989b8f19b chore: delete 106 unreachable *_system.gd and their parity tests
These files were loaded only by their own test_*_parity.gd and never by
the runtime (transitively checked, incl. client_phase_integration_system).
Removes 76 manifest implementation/evidence references across 18
contracts; contract docs get a note that conclusions based on them are
void. Live systems kept: chest, consumable, item_attr, metin_socket,
whisper_chat.

Full headless suite: no new failures (7 failing + 5 headless-hanging
render tests are identical before the deletion).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 17:51:52 +09:00

126 lines
9.9 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.target_selection_rules —— 目标选择、可攻击判定、目标框和安全区
状态:`PARTIAL`
本合同对照 40250 Windows 客户端的三套不能合并的判据:
1. `CInstanceBase::IsAttackableInstance`:是否允许战斗逻辑攻击目标。
2. `CInstanceBase::CanPickInstance` / `IsTargetableInstance`:是否允许鼠标/脚本设置目标。
3. `CPythonNetworkStream::RecvTargetPacket` + `CanViewTargetHP`:服务端目标包到达后,目标框是否关闭、显示血量或保持。
## 40250 参考调用链
### 战斗判定
- `UserInterface/InstanceBase.cpp:2147` `CInstanceBase::IsAttackableInstance`
- 主角观战模式先拒绝;自身 VID 先拒绝。
- 石头只能被 PC 攻击。
- PC 目标按决斗、同公会、killer、PK mode、队伍、同/异帝国、PVP/GVG、善恶冲突逐分支判断。
- PC 可攻击敌怪、木门;敌怪可攻击 PC/建筑;变身实例按 `IsPoly` 分支判断。
- `UserInterface/PythonPlayer.cpp:207` `CPythonPlayer::__Update_AutoAttack`
- 自动攻击目标必须存在、未死亡、骑乘等级允许,并通过 `IsAttackableInstance`
- `UserInterface/PythonPlayerInput.cpp:324-370` `__OnClickActor`
- 点击实体后先清预约;攻击目标再检查 `__CanAttack`、距离和 NPC 交互分支。
- `UserInterface/PythonPlayerInput.cpp:336-380` `__OnPressActor`
- 目标切换、自动攻击、弓箭射程、预约靠近和最终 `IsAttackableInstance` 的顺序固定。
### 目标可选中判定
- `UserInterface/PythonPlayerInput.cpp:112` `CPythonPlayer::SetTarget`
- 先检查主角 `CanChangeTarget()`
- 已选中目标有强制/非强制切换时间窗。
- 新目标必须通过主角 `IsTargetableInstance()`
- 目标成功时设置 actor targeted 状态、飞行目标和网络 `SendTargetPacket`;失败时清空本地目标。
- `UserInterface/InstanceBase.cpp:2251` `IsTargetableInstance`
- 直接委托目标的 `CanPickInstance()`
- `UserInterface/InstanceBase.cpp:2263` `CanPickInstance`
- 视锥外拒绝。
- 死亡门;死亡门对门对象单独处理。
- PC 的隐身、复活隐身、普通隐身按 affect 和主角可见性拒绝。
### 目标框和安全区
- `UserInterface/PythonNetworkStreamPhaseGame.cpp:2424` `RecvTargetPacket`
- 目标或主角实例缺失:关闭目标框。
- 目标已死亡:保持当前目标框状态,不写入新的实例目标。
- PC/建筑:`CloseTargetBoardIfDifferent`
- 石头/木门/敌怪:`SetHPTargetBoard`
- 其它实体:关闭目标框。
- `UserInterface/InstanceBase.cpp:2293` `CanViewTargetHP`
- 只有石头、木门和敌怪能显示 HP 目标框。
- `UserInterface/InstanceBase.cpp:560` `IsInSafe`
- 通过地图 `ATTRIBUTE_BANPK` 判定安全区;它与“能否选目标”和“目标框能否显示”是独立判据。
- `UserInterface/PythonPlayer.cpp:496-520` `NotifyCharacterDead/Update`
- 当前目标死亡时清目标;目标更新后不再 `CanPickInstance` 时清目标并关闭目标框。
## 当前端实现映射
- `project/entity_rules.gd`
- `is_attackable_instance` 已覆盖观战、自身、PC/怪/石头/木门、PK mode、party、killer、duel、PVP/GVG 和 alignment 分支;`can_pick_instance` 统一镜像视锥、死亡、门死亡和三类隐身 affect 门。
- `project/net_play.gd`
- `_is_attackable` 拼装会话上下文并调用 `EntityRules`
- `_on_pick` 负责 NPC/warp 交互、战斗目标选择、预约靠近和自动攻击。
- `_reserve_process_click_actor` 处理距离、攻击资格、死亡、安全区、弓箭和攻击顺序。
- `_on_target_info` 镜像 `RecvTargetPacket` 的目标框分派。
- `_change_target_to_picked_instance` 是当前统一脚本目标入口,先走 `_can_change_target`,再走重复目标时间窗和 `_is_targetable_instance`
- `_motion_blocks_target_change` 读取当前 `.msa` 的 WARP/FLY/EFFECT_TO_TARGET 事件,缺少动作数据时按 40250 的保守门处理。
- `project/target_board.gd`
- `classify``can_view_target_hp`、异阵营过滤、Ctrl 悄悄话和 HP 百分比边界已独立镜像。
- `project/player_controller.gd`
- 使用射线和可点选节点进行实体命中;射线与悬停两条路径均消费 NetWorld 写入的 `can_pick` 门。
- `project/net_world.gd`
- 负责目标光圈、目标/悬停名字强显和实体节点生命周期,并在实体创建/更新时同步 `can_pick`
- `project/alignment_pk_system.gd`
- 提供善恶值、PK 头衔和死亡掉落辅助逻辑;这些不是 40250 客户端 `SetTarget` 的替代实现。
## 分支等价矩阵
| 参考判据/副作用 | 当前实现 | 结论 |
|---|---|---|
| `IsAttackableInstance` 的 PC/怪/石头/木门/PK/决斗/PVP/GVG 分支 | `EntityRules.is_attackable_instance` | 基本等价;已有 `entity_rules_test.gd` 覆盖边界 |
| 自动攻击先检查存在、死亡、坐骑等级,再检查攻击资格 | `_on_pick``_reserve_process_click_actor`、自动攻击链 | 部分等价;多个调用点重复拼接门控,未由统一目标状态对象约束 |
| `SetTarget` 先执行 `CanChangeTarget` | `_can_change_target``_motion_blocks_target_change`,按当前 `.msa` 的 WARP/FLY/EFFECT_TO_TARGET 事件门控 | 已补齐静态/可读取动作事件路径;真实完整动作资源仍待 live 验证 |
| `SetTarget` 必须通过 `IsTargetableInstance`/`CanPickInstance` | `EntityRules.can_pick_instance` + NetWorld `can_pick` 元数据,脚本目标、射线点选和悬停共用 | 已补齐视锥字段、死亡、门死亡、EUNHYEONG/复活隐身/普通隐身门;真实视锥仍待 live 验证 |
| NPC/warp 不可攻击但可在距离内交互 | `_on_pick` 分流到 `_send_click_actor_packet` | 基本等价;真实包拒绝/冷却仍缺 live 证据 |
| `RecvTargetPacket` 的 PC/building/HP/CLOSE/NONE 分支 | `TargetBoard.classify` + `_on_target_info` | 静态分支基本等价;缺少目标死亡/更新/删除乱序时序测试 |
| `NotifyCharacterDead/Update` 清目标并关闭目标框 | `_on_entity_dead` / `_on_entity_gone` 统一 `CG_TARGET(0)` 清理;NetWorld 更新同步 `can_pick` | 死亡/删除已补齐;隐身、出视野和 stale 包时序仍待完整生命周期测试 |
| `IsInSafe` 地图属性判定 | 使用实体快照 `in_safe` | 数据来源不等价;当前端未证明由地图 `ATTRIBUTE_BANPK` 实时计算并在移动/换图时更新 |
| PC 异阵营目标框开关和 Ctrl 悄悄话 | `TargetBoard` + `net_play` | helper 分支通过,真实 UI/输入时序仍需 live 验证 |
## 已执行测试
- `project/entity_rules_test.gd`PASS,覆盖攻击资格判定矩阵。
- `project/target_board_test.gd`:PASS,覆盖目标框分派、HP 可见性、异阵营过滤和 Ctrl 悄悄话。
- `project/test_alignment_pk_parity.gd`35/35 PASS,覆盖善恶值和 PK 辅助逻辑。
- `project/test_click_target_effect_parity.gd`:0 failures,覆盖点击、目标光圈和飞行物地形碰撞;存在 ObjectDB/RID 清理警告。
- `project/test_auto_combat_bot_parity.gd`:22/22 PASS,覆盖自动选目标、移动、技能/普攻和拾取;主要为 fake client/helper 路径。
- `project/playable_combat_test.gd`:0 failures,覆盖可玩战斗主链。
## 本轮实现修复(2026-09-20T21:05Z
-`CPythonPlayer::SetTarget` 的顺序增加 `_can_change_target`、重复目标 1/2 秒时间窗和失败时的 `CG_TARGET(0)`;自动命中改走 `SetTarget(victim, FALSE)` 语义。
-`CInstanceBase::CanPickInstance` 增加统一 `EntityRules.can_pick_instance`:视锥字段、死亡/木门死亡、EUNHYEONG、复活隐身和普通隐身均在脚本目标入口检查。
- NetWorld 在实体创建/更新时把同一结果写入 `can_pick`PlayerController 的点击和悬停射线均消费该门,避免 UI/脚本两条路径出现不同目标。
- `netplay_test.gd` 新增技能动作锁、四类不可选目标、非强制命中换目标窗口测试;`netplay_test``entity_rules_test``playable_adapter_test`、点击目标/NetWorld/自动战斗/可玩战斗回归均通过。
本合同仍为 `PARTIAL`:完整动作资源事件、隐身/裁剪/迟到更新生命周期、地图 `ATTRIBUTE_BANPK` 权威和真实服务端目标包顺序尚未闭合。
## 已确认差异和未验证项
1. 真实完整 `.msa``get_events()`、动作结束和缺失动作资源下的 `CanChangeTarget` live 时序仍需验证。
2. 网络实体更新没有完全对应 40250 `NotifyCharacterUpdate` 的统一目标失效回调;隐身、出视野/淡出和迟到更新仍可能在不同入口留下目标框或光圈状态。
3. 当前安全区来自实体字段 `in_safe`,尚未证明与地图 `ATTRIBUTE_BANPK` 在移动、跨图和服务端状态更新时保持同一数据权威。
4. 当前端的 `_is_attackable` 在 helper 层提前检查 `dead`,而 40250 的 `IsAttackableInstance` 本身不检查死亡;虽然主要调用点另有死亡门,但该职责边界尚未统一。
5. `OnTargeted/OnUntargeted`、飞行目标和 `SendTargetPacket` 的完整副作用顺序,以及真实服务端目标包拒绝/乱序,仍无 live fixture。
## 下一轮验证入口
- 增加目标生命周期测试:存活 → 隐身 → 普通死亡 → 删除/裁剪 → 迟到更新,逐步检查目标 VID、目标框、光圈、名字强显和 `CG_TARGET(0)`
- 增加完整 `.msa` 事件源的 `CanChangeTarget`/动作锁/重复点击时间窗测试,并对照 `_change_target_to_picked_instance` 的实际调用链。
- 增加地图属性 `ATTRIBUTE_BANPK``in_safe` 的移动、换图、目标切换矩阵。
- 只有以上差异修复并通过 native/classic 与场景回归后,才能将本合同从 `PARTIAL` 提升。
### 2026-09-22 死代码删除
以下文件只被自身测试引用、运行时从不加载,已删除;本文中以它们为实现或证据的结论作废,相关行为需按 40250 源码单元重新移植:`alignment_pk_system.gd`, `test_alignment_pk_parity.gd`, `test_auto_combat_bot_parity.gd`