diff --git a/docs/ISSUES.md b/docs/ISSUES.md index b444f0ad..160df6c4 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -436,6 +436,100 @@ contract: [`2026-08-03-c4-route-2-contract.md`](research/2026-08-03-c4-route-2-c conflated with the dropped case. Filed from the C4 route-2 round-3 adversarial review. +## PK Lite gaps exposed by `@pklite` — 2026-08-03 + +All three found live by the user minutes after `69ba9486` made PK Lite +reachable for the first time. **None is a C4 route 2 regression** — verified by +diff: `9966b531` touched none of `CollisionExemption`, `CombatTargetPolicy`, +`SelectedObjectHealthPolicy`, `EntityCollisionFlags`, `ObjectTableWiring` or +`CreateObject`, and all three gates predate it (`3361a8d7`, `2644d1d5`, +`0f2d98c5`). `@pklite` made pre-existing behaviour reachable; it did not create +it. Do #297 FIRST — #298 depends on it. + +- **#297 — OPEN — PublicWeenieBitfield is frozen at CreateObject, so a PK + status change never reaches the client. HIGH.** User symptom: after `@pklite`, + the local player walks straight through other PKLite players. + ACE's only PK-change message is `GameMessagePublicUpdatePropertyInt` (0x02CE) + carrying `PropertyInt.PlayerKillerStatus`(134) = `PKLite`(0x40) + (`Player.cs:1153` -> `Player_Properties.cs:1122-1132` -> + `WorldObject_Networking.cs:1413-1442`). The PWD `ObjectDescriptionFlag` bits + are recomputed only inside serialization, and `EnqueueBroadcastUpdateObject` + (`WorldObject.cs:662-665`) has zero live callers — so no PWD re-send ever + happens and a client CANNOT learn PK status from the bitfield after login. + We parse and store the property (`ObjectTableWiring.cs:41-47`, landing in + `Properties.Ints[134]`) but never translate it: `ClientObject.PublicWeenieBitfield` + has exactly one writer (`ClientObjectTable.cs:891`) fed solely from the 0xF745 + CreateObject parse (`CreateObject.cs:827`). Both sides of the collision test + then read that frozen value — mover via `EntityCollisionFlags.cs:133-139` / + `LiveSessionEventRouter.cs:419-442`, target via + `LiveEntityCollisionBuilder.cs:151-153` — so `CollisionExemption.cs:117-122` + ("4c. both PKLite -> collide") never fires and `:125` exempts. + **Retail is the exact port we are missing:** `ACCWeenieObject::OnStatUpdated` + @0x0058DF20 `case 0x86:` calls `PublicWeenieDesc::SetPlayerKillerStatus` + @0x005AC7C0, which rewrites `pwd._bitfield` in place — PK `|0x20`, PKLite + `|0x2000000`, Free `|0x200000`, mutually exclusive, else clear all three. + `IsPKLite` @0x0058C8A0 reads `(_bitfield >> 0x19) & 1`. Entry points are + `Handle_Qualities__PrivateUpdateInt` @0x00558FD0 (0x02CD, self) and + `Handle_Qualities__UpdateInt` @0x00558D60 (0x02CE, remote). + Fix shape: port `SetPlayerKillerStatus` as a bitfield rewrite on PropertyInt + 134, applied from BOTH routes. `RecomputePvpStatus` already reacts to + `ObjectUpdated` so the mover side follows for free; the TARGET side needs a + second edge because shadow-registry `EntityCollisionFlags` are frozen at + registration. Cheap falsification test: a relog fixes it for the local player + only, since a fresh CreateObject carries the real bit. + Unaudited: whether making the bitfield mutable disturbs its item-shaped + readers (`ToolbarController.cs:633`, `ItemInteractionController.cs:1272`, + `AppraisalUiController.cs:543`, `ItemAppraisalTextFormatter.cs:949,:1110`). + Note the vivid target indicator (`WorldSelectionQuery.cs:271-298`) reads the + spawn PWD bits directly and is stale by the same mechanism. + +- **#298 — OPEN — melee/missile attack admission excludes players by + construction. MEDIUM-HIGH. Blocked on #297.** User symptom: selecting a + PKLite player and attacking retargets to the nearest monster (auto-target on) + or does nothing (auto-target off, logging + `combat: attack ignored; no creature target found`, + `LiveCombatAttackOperations.cs:187`). + `CombatTargetPolicy.IsHostileMonster:31-33` rejects any candidate carrying + `SelectedObjectHealthPolicy.BfPlayer` before reaching `ObjectIsAttackable`, + so the PKLite pool match at `SelectedObjectHealthPolicy.cs:70-71` is + unreachable for player targets. + **Retail has ONE predicate for monsters and players, with no player + exclusion:** `ClientCombatSystem::ExecuteAttack` @0x0056BB70 gates + unconditionally on `ObjectIsAttackable` @0x0056A600, which checks creature + type, the `0x200000` bits, then `IsPlayer(): (bothPK) || (bothPKLite)`, else + `BF_ATTACKABLE` with pets excluded. We already have that predicate ported + verbatim and correctly at `SelectedObjectHealthPolicy.cs:41-78` — it is + simply unreachable. + **DO NOT fix by relaxing the shared predicate.** `IsHostileMonster` also + backs auto-target ACQUISITION (`CombatAttackTargetSource.cs:80`, + `WorldSelectionQuery.cs:255`) and the combat camera + (`WorldSelectionQuery.cs:264-269`), and relaxing it would violate register row + **IA-19**, explicit product direction that auto-target must never select + NPCs, players or pets. Retail's own auto-target DOES admit players + (@0x0056C040 pc:377318-377327), so retail and IA-19 genuinely disagree here. + Fix shape: SPLIT explicit-target admission (-> `ObjectIsAttackable`, + retail-exact) from auto-acquisition (-> keep `IsHostileMonster`, IA-19 + intact). IA-19's own text already promises "manual player-selection commands + remain available"; that promise is currently unimplemented, which is the real + gap. Not affected: the health bar (`SelectedObjectHealthPolicy.cs:32` already + admits `BfPlayer`) and the vivid target indicator. + Correct model to copy: spells already work on PKLite players because + `RetailSpellTargetPolicy.cs:40-46` treats `BF_PLAYER` as an ACCEPT and never + calls `ObjectIsAttackable` — the client checks target-TYPE compatibility and + lets the server arbitrate PK legality (retail + `ClientMagicSystem::ObjectCompatibleWithSpellTargetType` @0x00567230). + +- **#299 — OPEN — CollisionExemption misses retail's mover-side + IsImpenetrable branch, and its doc comment asserts the opposite. LOW.** + `CollisionExemption.cs:103` checks only the TARGET's `IsImpenetrable`, and the + class doc at `:33-39` claims "retail's pseudo-C only checks the target's + `IsImpenetrable()`; acdream follows retail." The pseudo-C at pc:276824-276827 + has TWO short-circuit branches — mover `state & IS_IMPENETRABLE (0x80)` OR + target `IsImpenetrable()` — either alone exempting. We are missing the mover + branch, and the comment blames ACE (`PhysicsObj.cs:403-405`) for an addition + that is actually retail-faithful. Found during the #297/#298 investigation; + not symptom-causing. Fix the code and the comment together. + ## C3c placement cutover — 2026-08-02 - **#276 — OPEN — SpawnPlacementSettler discards the settle's resolved