fix(physics): classify before merge on every steady-state Position (C5b, #275, AP-131/AD-60)

The steady-state accepted-Position merge did two things retail never does,
on every single Position packet: it installed the wire placement frame and
unparented unconditionally, and it derived the record's FullCellId from
bare wire acceptance. Both are now correct, and they land together - a
half-flipped intermediate (classified flags with the wire stamp, or vice
versa) is exactly the mixed-residency state this campaign keeps paying for.

WHY the flags need no route. SmartBox::HandleReceivedPosition @0x00453FD0
decides both pre-placement writes BEFORE MoveOrTeleport is consulted: Gate A
@0x0045400C returns @0x0045409D ahead of unset_parent @0x00454129 and ahead
of the HasAnims SetPlacementFrame gate @0x00454137. Neither gate reads the
near/far/teleport classification. So the two flags are a pure function of
(disposition, hasAnimations) and are computable inside the merge, pre-merge,
with no signature change, no route construction and no playerDistance - the
scoping's ~150-400-line route-plumbing estimate over-counted because it did
not see this. That truth table IS
RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition's own
ApplyPlacementFrameBeforeRouting/UnparentBeforeRouting rows; the classifier
stays the oracle and the equality is pinned by test, not by a shared path,
so each computation remains separately sabotage-verifiable.

WHY the cell is withheld. HandleReceivedPosition reads the wire objcell_id
into a LOCAL @0x00453FE3 and hands it only to BlipPlayer / TeleportPlayer /
MoveOrTeleport / ConstrainTo; it never assigns the object's cell. The
object's cell moves inside the placement family (SetPositionInternal
@0x00515BD0 to set_cell, enter_world) or per-frame transit, and nowhere
else. The continuation executor has encoded that rule since the executor
slice; this caller now matches it verbatim.

WHAT DELIBERATELY SURVIVES. Two steady-state wire-cell writers stay,
downstream of the merge and outside the classification window: the
OnPosition prologue rebucket (W2, into CommitRebucket), which is also the
local player's own cell-freshness path, and the post-routing wire-cell adopt
for non-placing arms (W3, AP-135). Gating W2 "for symmetry" would freeze the
player's canonical cell between teleports and #319's child-cell equality
would inherit the freeze. AD-60's rewrite names both so the retirement
cannot be misread as "wire acceptance never changes residency anywhere".

REGISTER. AP-131 RETIRED - the unconditional literals no longer exist; the
caller was corrected, not deleted, so the row's own "deleted at the
production cutover" framing is overtaken. AD-60's legacy half RETIRED and
the row REWRITTEN rather than deleted, naming W2/W3 (route 4b-3's D8
precedent: a silent whole-row deletion would hide surviving channels).
AP-130 amended - the merge consumes the same static HasAnimations proxy,
deliberately not escalated to a live animation-queue read. AP-146 and #320
amended - their "accepted inbound Position (RefreshSnapshot into
RuntimeEntityRecord.cs:234)" local-player cell writer is now the generic
tail's CommitRebucket, and a ForcePosition (which returns before that tail)
is placement-receipt-authoritative. #275 closed.

HEADLINE BEHAVIOURAL DELTA, stated once: a refused or contended local
ForcePosition now leaves FullCellId at the last committed cell where the
merge used to stamp the refused packet's wire cell. Retail cannot refuse
(AD-62) and its body keeps its last placed cell, so the new shape is the
retail-reachable one.

THREE CONSUMER SITES THE CONTRACT'S BLAST-RADIUS SURVEY MISSED, all
D2-caused, all found by the suite rather than by reading, all intended
semantics rather than regressions (recorded in the contract's new section
14):
(1) DatLiveEntityProjectionMaterializer's self-projection branch reads
    FullCellId inside OnPosition's prologue recovery, ahead of W2. It now
    correctly declines to project from an unplaced wire claim; production
    installs the bucket at W2 in the same call (verified: no return between
    the recovery call and W2 is conditioned on IsSpatiallyProjected or
    FullCellId). Two hydration tests asserted the bucket at the recovery
    boundary and now drive the production W2 step - the same shape as trap
    T2, one layer up.
(2) ProjectileController.SyncPresentationFromResolvedBody writes
    ParentCellId = record.FullCellId. On a refused missile placement that is
    now the committed source cell. The MAJOR-1 invariant is unchanged and is
    now asserted as the identity it always meant rather than as a wire-cell
    constant.
(3) The merge's Rebucketed ternary does NOT become always-Updated as the
    contract predicted, and is deliberately kept: the
    Forget(restoreCancelledPark: true) above it can roll a wakeable
    lost-cell park back, and RestoreParkWithdrawal restores canonical
    residency. That is a real cell edge produced inside this method by a
    placement owner.

TEST-COUNT RECONCILIATION. Baseline measured at this HEAD by stashing the
change: Runtime.Tests 1176, App.Tests 4135 (4132 passed / 3 skipped),
solution 11,106 passed / 4 skipped - matching the recorded figure at
6921a027 exactly. Post-change: Runtime.Tests 1195, App.Tests 4135 unchanged,
solution 11,125 passed / 4 skipped / 0 failed. Net +19, entirely new Runtime
tests: 3 facts plus a 12-row matrix theory in
InboundPhysicsStateControllerTests, 1 fact plus a 2-row theory in the new
RuntimeSteadyStatePositionMergeTests, and 1 fact in
RuntimeAcceptedPositionDriveControllerTests. No test was deleted; five
existing tests were rewritten in place, never delete-only. No new skip; none
of #302/#308/#321 appeared.

SABOTAGE VERIFICATIONS (each new discriminating test, both directions;
production line broken, suite run, line restored):
  installPlacementFrame (!force && !hasAnimations) to (!force)
    5 fail: ApplyOnAnimatedEntity_NeverInstallsTheWirePlacementFrame plus
    the 4 animated non-force matrix rows.
  installPlacementFrame to false
    6 fail: ApplyOnNonAnimatedEntity_InstallsTheWirePlacementFrame,
    PositionPlacementAbsentAndPresentZeroBothApplyRetailZero plus the 4
    non-animated non-force matrix rows.
  clearParent (!force) to true
    3 fail: ForcePositionOnParentedLocalPlayer_RetainsTheParentAttachment
    plus the 2 force+parented matrix rows.
  clearParent (!force) to false
    4 fail: the 4 Apply+parented matrix rows.
  refreshPosition false to acceptedPosition
    4 fail: AcceptedPosition_WithholdsTheWireCellAtTheMergeBoundary,
    ContendedForcePosition_WritesNoResidencyAnywhere,
    ReentrantNewerPositionDuringPickupDiscardSuppressesStalePickupDelta,
    MissileFarRefused_...ParentCellIdAgreesWithCommittedCell. Confirmed a
    second time by the baseline measurement above, where the withhold test
    was the sole red.
  CommitRebucket publishes Updated instead of Rebucketed
    2 fail: both parent classes of
    CellChangingAcceptedPosition_ConservesOneRebucketAndOneChildPropagation.
  RuntimeEntityDirectory.SetFullCell drops PropagateFullCellToChildren
    2 fail: the same two rows.
T4 respected: the ForcePosition placement-frame half is inert
(appliedPlacement keeps old.PlacementId under either flag value), so the
force row's discriminating assertion is parent retention, never the frame.

NOT DONE, deliberately: the executor is still not wired into the
steady-state path (#275's alternative branch); W2/W3 are untouched; no probe
added or stripped; AP-130's proxy not escalated; no while-here unification
of the two merge callsites. No automated OnPosition-level test drives the
full pickup / drop / reproject sequence (no fixture covers pickup at that
layer); the contract's connected gate recipe item 1 is the positive evidence
for it and has NOT been run - this commit is not connected-gated.

Contract: docs/research/2026-08-05-c5b-contract.md (committed here, with its
section 14 implementation outcome appended).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-05 21:17:20 +02:00
parent 0257844106
commit 735f0a72af
11 changed files with 1715 additions and 63 deletions

View file

@ -66,12 +66,28 @@ public sealed class InboundPhysicsStateControllerTests
Assert.Equal(30f, returned.Position!.Value.PositionX);
}
/// <summary>
/// C5b (#275) rewrite of the pre-classification test: the retail-zero
/// rule (<c>PositionPack::UnPack</c> @0x00516740 initializes an absent
/// placement id to zero and <c>HandleReceivedPosition</c> forwards that
/// exact value to <c>SetPlacementFrame</c>) is only reachable when the
/// gate ABOVE it passes — <c>CPhysicsObj::HasAnims(arg2) == 0</c>
/// @0x00454137. The seed is therefore explicitly NON-animated; the
/// animated counterpart is
/// <see cref="ApplyOnAnimatedEntity_NeverInstallsTheWirePlacementFrame"/>.
/// </summary>
[Fact]
public void PositionPlacementAbsentAndPresentZeroBothApplyRetailZero()
{
var controller = new InboundPhysicsStateController();
WorldSession.EntitySpawn seed = Spawn(
0x70000009u, 7, 20, 1, Position(0x0101FFFFu, 10f), 0x408u);
0x70000009u,
7,
20,
1,
Position(0x0101FFFFu, 10f),
0x408u,
motionTableId: null);
seed = seed with
{
PlacementId = 7,
@ -122,6 +138,306 @@ public sealed class InboundPhysicsStateControllerTests
Assert.Equal((uint)0, second.Physics!.Value.AnimationFrame);
}
/// <summary>
/// C5b (#275) / AP-131 retirement, half 1. Retail gates
/// <c>CPhysicsObj::SetPlacementFrame</c> @0x00454142 on
/// <c>CPhysicsObj::HasAnims(arg2) == 0</c> @0x00454137: an ANIMATED body
/// never takes the wire placement frame, because its animation sequencer
/// owns the pose. Before C5b the steady-state merge stamped it anyway,
/// which is the animation snap/reset half of AP-131.
/// </summary>
[Fact]
public void ApplyOnAnimatedEntity_NeverInstallsTheWirePlacementFrame()
{
var controller = new InboundPhysicsStateController();
WorldSession.EntitySpawn seed = Spawn(
0x8000C001u,
7,
20,
1,
Position(0x0101FFFFu, 10f),
0x408u,
motionTableId: 0x09000001u);
seed = seed with
{
PlacementId = 7,
Physics = seed.Physics!.Value with { AnimationFrame = 7 },
};
controller.AcceptCreate(seed);
Assert.True(controller.TryApplyPosition(
new WorldSession.EntityPositionUpdate(
seed.Guid,
Position(0x0101FFFFu, 20f),
Velocity: null,
PlacementId: 5,
IsGrounded: true,
InstanceSequence: 7,
PositionSequence: 21,
TeleportSequence: 0,
ForcePositionSequence: 0),
isLocalPlayer: false,
forcePositionRotation: null,
currentLocalVelocity: null,
out PositionTimestampDisposition disposition,
out WorldSession.EntitySpawn accepted,
out _));
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
// The gate above SetPlacementFrame never opened.
Assert.Equal((uint)7, accepted.PlacementId);
Assert.Equal((uint)7, accepted.Physics!.Value.AnimationFrame);
// The rest of the merge is unaffected: pose and POSITION_TS still land.
Assert.Equal(20f, accepted.Position!.Value.PositionX);
Assert.Equal((ushort)21, accepted.PositionSequence);
// unset_parent @0x00454129 is unconditional on this path.
Assert.Null(accepted.ParentGuid);
}
/// <summary>
/// C5b (#275) / AP-131 retirement, half 1 (the other direction). A
/// NON-animated body reaches <c>SetPlacementFrame</c> @0x00454142 and
/// takes the wire frame verbatim.
/// </summary>
[Fact]
public void ApplyOnNonAnimatedEntity_InstallsTheWirePlacementFrame()
{
var controller = new InboundPhysicsStateController();
WorldSession.EntitySpawn seed = Spawn(
0x8000C002u,
7,
20,
1,
Position(0x0101FFFFu, 10f),
0x408u,
motionTableId: null);
seed = seed with
{
PlacementId = 7,
Physics = seed.Physics!.Value with { AnimationFrame = 7 },
};
controller.AcceptCreate(seed);
Assert.True(controller.TryApplyPosition(
new WorldSession.EntityPositionUpdate(
seed.Guid,
Position(0x0101FFFFu, 20f),
Velocity: null,
PlacementId: 5,
IsGrounded: true,
InstanceSequence: 7,
PositionSequence: 21,
TeleportSequence: 0,
ForcePositionSequence: 0),
isLocalPlayer: false,
forcePositionRotation: null,
currentLocalVelocity: null,
out PositionTimestampDisposition disposition,
out WorldSession.EntitySpawn accepted,
out _));
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
Assert.Equal((uint)5, accepted.PlacementId);
Assert.Equal((uint)5, accepted.Physics!.Value.AnimationFrame);
}
/// <summary>
/// C5b (#275) / AP-131 retirement, half 2. Retail's Gate A
/// (@0x0045400C, the local-player FORCE_POSITION self-echo shortcut)
/// returns @0x0045409D BEFORE <c>CPhysicsObj::unset_parent</c>
/// @0x00454129, so a ForcePosition never unparents. Parent RETENTION is
/// the discriminating observable for the force row: the placement-frame
/// half is inert there (<c>appliedPlacement</c> keeps
/// <c>old.PlacementId</c> under either flag value), so sabotaging it on a
/// ForcePosition proves nothing.
/// </summary>
[Fact]
public void ForcePositionOnParentedLocalPlayer_RetainsTheParentAttachment()
{
var controller = new InboundPhysicsStateController();
WorldSession.EntitySpawn seed = WithTimestamps(
Spawn(0x50000021u, 3, 10, 1, Position(0x0101FFFFu, 10f), 0x408u),
teleport: 10,
forcePosition: 0);
seed = seed with
{
ParentGuid = 0x70004444u,
ParentLocation = 9u,
Physics = seed.Physics!.Value with
{
Parent = new PhysicsAttachment(0x70004444u, 9u),
},
};
controller.AcceptCreate(seed);
var preserved = new Quaternion(0.1f, 0.2f, 0.3f, 0.9f);
Assert.True(controller.TryApplyPosition(
PositionUpdate(
seed.Guid,
instance: 3,
position: 9,
teleport: 10,
forcePosition: 1),
isLocalPlayer: true,
forcePositionRotation: preserved,
currentLocalVelocity: Vector3.Zero,
out PositionTimestampDisposition disposition,
out WorldSession.EntitySpawn accepted,
out _));
Assert.Equal(PositionTimestampDisposition.ForcePosition, disposition);
Assert.Equal(0x70004444u, accepted.ParentGuid);
Assert.Equal(9u, accepted.ParentLocation);
Assert.Equal(
new PhysicsAttachment(0x70004444u, 9u),
accepted.Physics!.Value.Parent);
// Heading preservation (BlipPlayer's get_heading/set_heading pair
// @0x00454056-68) is unchanged by C5b.
Assert.Equal(preserved.W, accepted.Position!.Value.RotationW);
Assert.Equal(preserved.X, accepted.Position!.Value.RotationX);
}
/// <summary>
/// C5b (#275) proof obligation 1: the merge's two pre-placement flags ARE
/// <see cref="RuntimeAuthoritativePositionRouteClassifier"/>'s
/// <c>ApplyPlacementFrameBeforeRouting</c>/<c>UnparentBeforeRouting</c>
/// rows. The production classifier is the oracle — this test does not
/// re-encode the truth table, it drives both computations over the packet
/// matrix and asserts the merged snapshot equals what the route's own
/// flags would have produced. Dual parent classes (player <c>0x5…</c> and
/// creature <c>0x8…</c>) per the #319 discipline.
/// </summary>
[Theory]
// guid, isLocalPlayer, animated, parented, force
[InlineData(0x50000031u, true, true, true, false)]
[InlineData(0x50000032u, true, true, false, false)]
[InlineData(0x50000033u, true, false, true, false)]
[InlineData(0x50000034u, true, false, false, false)]
[InlineData(0x50000035u, true, true, true, true)]
[InlineData(0x50000036u, true, true, false, true)]
[InlineData(0x50000037u, true, false, true, true)]
[InlineData(0x50000038u, true, false, false, true)]
[InlineData(0x80000031u, false, true, true, false)]
[InlineData(0x80000032u, false, true, false, false)]
[InlineData(0x80000033u, false, false, true, false)]
[InlineData(0x80000034u, false, false, false, false)]
public void MergedPrePlacementFieldsMatchTheClassifiedRouteFlags(
uint guid,
bool isLocalPlayer,
bool animated,
bool parented,
bool force)
{
WorldSession.EntitySpawn seed = WithTimestamps(
Spawn(
guid,
3,
10,
1,
Position(0x0101FFFFu, 10f),
0x408u,
motionTableId: animated ? 0x09000001u : null),
teleport: 10,
forcePosition: 0);
seed = seed with
{
PlacementId = 7,
Physics = seed.Physics!.Value with { AnimationFrame = 7 },
};
if (parented)
{
seed = seed with
{
ParentGuid = 0x70004444u,
ParentLocation = 9u,
Physics = seed.Physics!.Value with
{
Parent = new PhysicsAttachment(0x70004444u, 9u),
},
};
}
WorldSession.EntityPositionUpdate update = new(
guid,
Position(0x0101FFFFu, 20f),
Velocity: null,
PlacementId: 5,
IsGrounded: true,
InstanceSequence: 3,
PositionSequence: force ? (ushort)9 : (ushort)11,
TeleportSequence: 10,
ForcePositionSequence: force ? (ushort)1 : (ushort)0);
var merging = new InboundPhysicsStateController();
merging.AcceptCreate(seed);
Assert.True(merging.TryApplyPosition(
update,
isLocalPlayer,
forcePositionRotation: isLocalPlayer ? Quaternion.Identity : null,
currentLocalVelocity: isLocalPlayer ? Vector3.Zero : null,
out PositionTimestampDisposition disposition,
out WorldSession.EntitySpawn merged,
out AcceptedPhysicsTimestamps timestamps));
Assert.Equal(
force
? PositionTimestampDisposition.ForcePosition
: PositionTimestampDisposition.Apply,
disposition);
// The oracle: the production classifier, fed the same wire packet and
// the same static HasAnimations proxy.
RuntimeAuthoritativePositionRoute route =
RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition(
new RuntimeAcceptedPositionRouteRequest(
new RuntimeAuthoritativePositionAuthority(
new RuntimeGenerationToken(7),
new RuntimeEntityKey(guid, 1),
PositionAuthorityVersion: 4UL,
AcceptedPositionSequence: update.PositionSequence,
timestamps.PreviousTeleport,
timestamps.Teleport,
disposition),
isLocalPlayer
? RuntimePositionEntityKind.LocalPlayer
: RuntimePositionEntityKind.Remote,
RuntimeAcceptedPositionSource.PositionEvent,
update.Position,
PlacementFrame: update.PlacementId,
PositionPackVelocity: null,
CommittedCellId: 0x0101FFFFu,
HasContact: true,
PlayerDistance: 0f,
UsePositionFromServer: true,
HasAnimations: animated,
default));
// Rebuild what the classified route would have installed, through the
// SAME production merge, and require field equality.
var classified = new InboundPhysicsStateController();
classified.AcceptCreate(seed);
Assert.True(classified.ApplyAcceptedPositionSnapshot(
guid,
update,
disposition,
timestamps,
isLocalPlayer,
isLocalPlayer ? Quaternion.Identity : null,
isLocalPlayer ? Vector3.Zero : null,
installPlacementFrame: route.ApplyPlacementFrameBeforeRouting,
clearParent: route.UnparentBeforeRouting,
out WorldSession.EntitySpawn expected));
Assert.Equal(expected.PlacementId, merged.PlacementId);
Assert.Equal(
expected.Physics!.Value.AnimationFrame,
merged.Physics!.Value.AnimationFrame);
Assert.Equal(expected.ParentGuid, merged.ParentGuid);
Assert.Equal(expected.ParentLocation, merged.ParentLocation);
Assert.Equal(
expected.Physics!.Value.Parent,
merged.Physics!.Value.Parent);
}
[Fact]
public void RemotePositionWithoutVelocityAppliesUnpackedZeroVector()
{
@ -645,13 +961,22 @@ public sealed class InboundPhysicsStateControllerTests
};
}
/// <summary>
/// C5b (#275): <paramref name="motionTableId"/> drives AP-130's static
/// HasAnimations proxy, which now gates the steady-state merge's
/// placement-frame install exactly as retail's
/// <c>CPhysicsObj::HasAnims</c> check @0x00454137 gates
/// <c>SetPlacementFrame</c>. The default keeps every pre-existing fixture
/// animated, which is what the retail population overwhelmingly is.
/// </summary>
private static WorldSession.EntitySpawn Spawn(
uint guid,
ushort instance,
ushort positionSequence,
ushort stateSequence,
CreateObject.ServerPosition? position,
uint state)
uint state,
uint? motionTableId = 0x09000001u)
{
var timestamps = new PhysicsTimestamps(
positionSequence,
@ -669,7 +994,7 @@ public sealed class InboundPhysicsStateControllerTests
Movement: null,
AnimationFrame: null,
SetupTableId: 0x02000001u,
MotionTableId: 0x09000001u,
MotionTableId: motionTableId,
SoundTableId: null,
PhysicsScriptTableId: null,
Parent: null,
@ -696,7 +1021,7 @@ public sealed class InboundPhysicsStateControllerTests
"fixture",
null,
null,
0x09000001u,
motionTableId,
PhysicsState: state,
InstanceSequence: instance,
MovementSequence: 1,