From e3b766d952080e9b4baf0d4c82294bd6f49063b7 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 3 Aug 2026 19:24:08 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20file=20#297-#299=20=E2=80=94=20PK=20Lit?= =?UTF-8?q?e=20gaps=20exposed=20by=20@pklite?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The user found three symptoms live within minutes of 69ba9486 making PK Lite reachable for the first time. Two independent root causes, neither a C4 route 2 regression (verified by diff: 9966b531 touched none of the gates, and all three predate it). #297 (HIGH) — PublicWeenieBitfield is written once at CreateObject and never refreshed. ACE's only PK-change message is PropertyInt 134 over 0x02CE; we store it but never translate it into the bitfield, and ACE never re-sends a PWD (EnqueueBroadcastUpdateObject has zero live callers), so a client cannot learn PK status from the bitfield after login. Both sides of the collision test read the frozen value, so CollisionExemption's "both PKLite -> collide" rule never fires. Retail's missing port is PublicWeenieDesc::SetPlayerKillerStatus @0x005AC7C0, driven from ACCWeenieObject::OnStatUpdated @0x0058DF20 case 0x86. #298 (MEDIUM-HIGH, blocked on #297) — CombatTargetPolicy.IsHostileMonster rejects BfPlayer before reaching ObjectIsAttackable, so the PKLite pool match we already ported correctly is unreachable for players. Retail uses ONE predicate for monsters and players with no exclusion. Critically: the naive fix is wrong — the same predicate backs auto-target acquisition and the combat camera, and relaxing it would violate register row IA-19's explicit product direction. The fix must SPLIT explicit-target admission from auto-acquisition, which is what IA-19's own unimplemented promise already describes. #299 (LOW) — CollisionExemption checks only the target's IsImpenetrable while retail short-circuits on mover OR target, and the class doc asserts the opposite. Found during the investigation; not symptom-causing. Co-Authored-By: Claude Opus 5 --- docs/ISSUES.md | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) 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