fix(physics): #297 — keep the PWD bitfield live so PK status reaches the client
The user typed @pklite and then walked straight through other PKLite players. Root cause: ClientObject.PublicWeenieBitfield was written exactly once, from the 0xF745 CreateObject parse, and never refreshed. ACE's only PK-change message is PropertyInt.PlayerKillerStatus (134) over 0x02CE/0x02CD, which we parsed and stored into Properties.Ints[134] but never translated back into the bitfield — and ACE never re-sends a PublicWeenieDesc at all (EnqueueBroadcastUpdateObject has zero live callers), so that property is the ONLY signal a client can learn from. Both sides of the collision test read the frozen value, so CollisionExemption's "4c. both PKLite -> collide" rule could never fire. Retail's missing port: PublicWeenieDesc::SetPlayerKillerStatus @0x005AC7C0 rewrites _bitfield in place — PK(4) -> (b & 0xfddfffff) | 0x20; PKLite(0x40) -> (b & 0xffdfffdf) | 0x2000000; Free(0x20) -> (b & 0xfdffffdf) | 0x200000; else b &= 0xfddfffdf. Mutually exclusive, verified byte-for-byte, with input values confirmed against retail's own PKStatusEnum (acclient.h:6412-6427), not just ACE's. Driven from ACCWeenieObject::OnStatUpdated @0x0058DF20 case 0x86. The fix rewrites the value at its source rather than patching consumers. Two review rounds were needed because the first pass missed that there are TWO snapshot stores: InboundPhysicsStateController keeps its own private _snapshots dictionary, and every untimestamped-field merge (ApplyAcceptedObjDesc and friends) reads `old` from THAT store, not from RuntimeEntityRecord.Snapshot. Refreshing only the active record left the target-side shadow flags correct until the remote's next equip or unequip — ACE broadcasts an ObjDesc on every one — at which point the appearance path rebuilt the registration from the frozen spawn and dropped the bit permanently. The regression test demanded by review is what surfaced that; it is verified discriminating (reverting gives Actual: 8 instead of 33554440). Five stores now hold this value, kept coherent from one source by two ObjectUpdated subscribers plus the appearance-rebuild path. The two shadow-flag writers are the same invalidation applied at the two edges that can invalidate it, not competing authorities — review enumerated every drift path and closed each. That coherence invariant is new as of this commit and is recorded as register row AP-134, with AP-133 as the precedent for filing a row when the danger is a future writer rather than current behaviour. Also corrects TS-23's retirement narrative, which claimed every mover-flags call site read the mover's "real" PK bits from 2026-07-30. The bits existed but their source was frozen, so that only became true here; the site enumeration also missed RuntimeSetPositionMoverPreparation, a seventh site that decodes the snapshot directly. Unblocks #298 (melee/missile admission needs the local player's own PKLite bit). Follow-ups filed: #300 (Properties.Ints[134] vs bitfield mirror gap), #301 (same defect class for radar blip colour and radar behaviour), #302 (a pre-existing PortalProjection allocation-assertion flake, 1 in 6, found while verifying this gate), #303 (LiveEntityPvpBitfieldSync is App-resident but Runtime-owned-state). Gates: complete Release solution 10,895 passed / 4 skipped / 0 failed (baseline 10,887 including #299). Adversarial + retail-conformance review PASS after one FAIL round. Every new test discrimination-verified by reverting the fix. Connected acceptance NOT run — needs a live two-client PKLite session. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
88348f6791
commit
9b1e6fc637
19 changed files with 1329 additions and 5 deletions
|
|
@ -530,6 +530,56 @@ it. Do #297 FIRST — #298 depends on it.
|
|||
that is actually retail-faithful. Found during the #297/#298 investigation;
|
||||
not symptom-causing. Fix the code and the comment together.
|
||||
|
||||
## Follow-ups from the #297 fix and its review — 2026-08-03
|
||||
|
||||
- **#300 — OPEN — `Properties.Ints[134]` and `PublicWeenieBitfield` can disagree
|
||||
inside one `ClientObject`. LOW.** `ClientObjectTable.UpdateIntProperty:792-796`
|
||||
is the only entry point that mirrors PropertyInt 134 (PlayerKillerStatus) into
|
||||
the PWD bitfield. `UpsertProperties:750-767` (PlayerDescription 0x0013) and
|
||||
`UpdateProperties:728-741` (IdentifyObjectResponse) write
|
||||
`Properties.Ints[134]` **without** the mirror. An assess/appraisal bundle on a
|
||||
player carrying PlayerKillerStatus would leave the raw int saying PKLite while
|
||||
the bitfield still reads NPK — and `LiveSessionEventRouter.RecomputePvpStatus:426-429`
|
||||
reads the raw int for the jump-stamina PK timer while everything else reads the
|
||||
bitfield, so one row would drive two different answers. Benign today
|
||||
(CreateObject's PWD is authoritative at login and ACE's assess bundles for
|
||||
players are unlikely to carry 134) and it does not affect the #297 collision
|
||||
path. Fix shape: a shared mirror helper called from all three appliers. Filed
|
||||
from the #297 delta review; see register row AP-134.
|
||||
|
||||
- **#301 — OPEN — retail's OnStatUpdated also rewrites radar blip colour and
|
||||
radar behaviour; acdream ignores both. LOW.** `ACCWeenieObject::OnStatUpdated`
|
||||
@0x0058DF20 rewrites `pwd._blipColor` on `case 0x5f` (95 = RadarBlipColor) and
|
||||
`pwd._radar_enum` on `case 0x85` (133 = RadarBehavior), verified at
|
||||
`acclient_2013_pseudo_c.txt:408381-408391`. acdream handles neither, and
|
||||
`RadarSnapshotProvider.cs:85,134` reads the frozen CreateObject spawn — so a
|
||||
server-side radar-appearance change never reaches the radar. This is #297 for
|
||||
the radar, same defect class, same fix shape (mirror the property into the
|
||||
bitfield/snapshot at its source). Filed from the #297 delta review.
|
||||
|
||||
- **#302 — OPEN — `PortalProjectionTests.ClipToRegion_FrameOwnedStore_ReusesExactResultArray`
|
||||
is flaky. LOW.** Measured 1 failure in 6 consecutive isolated runs of
|
||||
`AcDream.App.Tests` at `88348f67`, and once in a full-suite run that passed on
|
||||
two immediate retries. The test asserts on
|
||||
`GC.GetAllocatedBytesForCurrentThread()`
|
||||
(`tests/AcDream.App.Tests/Rendering/PortalProjectionTests.cs:532`), which is
|
||||
sensitive to JIT tiering and background GC regardless of the code under test.
|
||||
Unrelated to the PK/collision work it surfaced during. **Do not treat a green
|
||||
suite as proof this is gone** — it passes ~5 times in 6. Fix shape: warm the
|
||||
path before measuring, or assert a bounded range rather than an exact
|
||||
allocation count, matching how the other allocation gates in the repo are
|
||||
written. Found while independently verifying the #297 gate.
|
||||
|
||||
- **#303 — OPEN — `LiveEntityPvpBitfieldSync` lives in App but touches only
|
||||
Runtime-owned state. INFO/shape.**
|
||||
`src/AcDream.App/Physics/LiveEntityPvpBitfieldSync.cs` reads
|
||||
`RuntimeEntityObjectLifetime.Objects` and writes
|
||||
`RuntimePhysicsState.Engine.ShadowObjects` — both Runtime-owned since J5.5.
|
||||
Moving it beside `RuntimeEntityPvpBitfieldSnapshotSync` would leave one
|
||||
subscriber and one owner. No coverage gap today (headless has no
|
||||
`LiveEntityCollisionBuilder` and therefore no live-entity target shadows), so
|
||||
this is shape rather than a defect. Filed from the #297 delta review.
|
||||
|
||||
## C3c placement cutover — 2026-08-02
|
||||
|
||||
- **#276 — OPEN — SpawnPlacementSettler discards the settle's resolved
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue