fix(runtime): close the C5b re-review findings — Gate A narrowing filed, no-window payload gate, bisect hazard recorded

Both C5b re-reviews returned PASS on 02578441..ff100cf3. This lands the
bookkeeping corrections they left, the one gate asymmetry both found
independently, and one wrong retail fact neither of them caught.

1. AP-148 / #325 — Gate A's teleport test, wrong on primary source twice.

The C5b contract stated retail's Gate A teleport term as "TELEPORT_TS
equal" (and, in the trace block, as "must NOT be newer") and blessed
acdream's `teleport == _timestamps[Teleport]` as retail-exact. Disassembly
of the PDB-paired binary at SmartBox::HandleReceivedPosition
0x0045402B-0x00454054 says otherwise: the shortcut is taken iff the wire
stamp is equal OR newer (wrap-safe) — `sbb eax,eax / neg eax` materialises
the carry of the compare and the branch skips Gate A on CF, i.e. only when
the wire stamp is strictly OLDER. It is CPhysicsObj::newer_event
@0x00451B10's identical idiom with the operands swapped. Binary Ninja drops
the flag test and renders it `if (-((eax_7 - eax_7)) == 0)`, always true —
which is why two rounds of reading pseudo-C recorded it backwards.

So acdream's ForcePosition disposition is a strict SUBSET of retail's Gate
A set, and a local ForcePosition carrying a newer teleport stamp is
misrouted into a full Apply: wire heading instead of preserved heading,
unparent, possible placement frame, zeroed velocity, TELEPORT_TS advanced,
and OfferTeleportDestination called for a packet retail never starts
presentation for.

PhysicsTimestampGate.cs is NOT changed. The predicate exists twice (also
ValidAcceptedAuthority's PreviousTeleport == AcceptedTeleport), and the fix
has to decide TELEPORT_TS's disposition on a Gate A path that has never
seen a stale-but-equal pair. #325 records all of it and says explicitly
that it is not a one-line comparison swap. C5b made this marginally
better, not worse: clearParent was unconditionally true before C5b and is
unchanged; installPlacementFrame moved toward retail's HasAnims gate.

2. Retail F2 / architecture L-A — the no-window route had no pre-merge
payload validation. Root fix, not a documented asymmetry.

The graphical route validates before the merge (OnPosition's payloadIsValid
-> LiveEntityInboundAuthorityGate's !payloadIsValid return); despite its
name CanAcceptPositionPayload is not projectile-scoped. The no-window route
had no equivalent, and since D1 fed an unvalidated LandblockId into
CommitWireCellRebucket — where 0 is the withdrawal shape, silently
de-residencing the entity in the field every bot reads as CellId.

RuntimeLiveEntitySessionController.OnPositionUpdated now applies the same
rule at the same point, reusing
RuntimeAuthoritativePositionRouteClassifier.IsValidCreateWirePosition plus
the finite-velocity term — the exact pair TryApplyPosition already applies
on its initial-residence branch. Chosen over documenting it because the fix
is five lines and leaving it would have left two written claims falsified
by the code. It is a behaviour change: headless now drops packets it
merged. Against ACE the set is empty, and the graphical host has carried
this gate since it was written; the argument is recorded in the contract's
§15.2 rather than gated.

Two test fixtures carried cell ids retail's own inbound_valid_cellid
rejects (low words 0x41 and 0x51, above the 0x40 landcell ceiling). Their
constants were corrected; their assertions were not.

New test sabotage-verified in both directions: gate removed -> red at the
withdrawal-shape assertion; gate moved to guard only the cell commit ->
red at the pose assertion, which is what makes it a before-the-MERGE test
rather than a before-the-commit test.

3. Register and doc corrections.

- AD-64: "deliberately absent" was presented as the complete difference
  list and was not. Adds (a) the residence gate is weaker than the merge's
  own — both hosts' commits use TryGetCurrent while TryApplyPosition's FIFO
  branch uses TryGetTransaction, so the wire cell can commit ahead of the
  continuation that will replay it; (b) the two missile gates are two
  different expressions that agree today; (c) the payload gate, now
  present. Risk column records that (a) and (b) have no discriminating test
  on either side.
- AP-147: amended for D1 — pre-D1 the no-window host published [Updated]
  alone and lost the Rebucketed, so a headless event log is now a real
  instance of the "consumer that snapshots a delta" the row warns about.
- AD-60: "Matches retail exactly" scoped to the withhold, since the row's
  body documents two channels that do not.
- CommitWireCellRebucket: notes the unreachable ThrowIfNull /
  EnsureNotDisposed precedence inversion.
- TryCommitAcceptedWireCell: the discarded commit bool is explained rather
  than left bare — false means IsCurrent went stale, unreachable three
  statements after a synchronous TryGetActive.

4. Bisect hazard recorded in the C4 closeout handoff (the doc CLAUDE.md
sends readers to before any C5 work) and in the contract's §15.3: commits
735f0a72..23aa62f2 contain a live headless defect — every remote's
FullCellId frozen for the session — introduced by 735f0a72 and fixed only
at ff100cf3. Nothing throws and no test in the range fails.

Gates: Release build 0 errors/0 warnings. Complete suite 11,142 passed /
4 skipped / 0 failed against the 11,141 / 4 / 0 baseline — net +1, exactly
the one new test. No flake appeared (#302, #308, #321 all green).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-05 23:10:24 +02:00
parent ff100cf33f
commit 9ee9c1a1a6
7 changed files with 455 additions and 21 deletions

View file

@ -659,9 +659,16 @@ public sealed class RuntimeLiveEntitySessionControllerTests
Assert.NotEqual(0u, placedCell);
// (a) A stale position sequence is Rejected by the timestamp gate.
// The cell is 0x…0011 rather than the 0x…0041 this fixture used
// before the C5b follow-up: a low word above 0x40 is not a legal
// landcell (LandDefs.CellLowInRange, retail
// LandDefs::inbound_valid_cellid @0x004979A0), so once this route
// gained the graphical route's payload gate the packet would have
// been refused for its CELL and this assertion would have passed
// without the timestamp gate ever being consulted.
sink.PositionUpdated(PositionUpdate(
spawn.Guid,
0x01010041u,
0x01010011u,
positionX: 13f,
positionSequence: 1));
Assert.Equal(placedCell, remote.FullCellId);
@ -674,6 +681,110 @@ public sealed class RuntimeLiveEntitySessionControllerTests
Assert.Equal(0x0202FFFFu, remote.CanonicalLandblockId);
}
/// <summary>
/// C5b follow-up (retail F2 / architecture L-A): the no-window route
/// refuses an invalid wire payload with the same rule, at the same
/// point, as the graphical route's <c>payloadIsValid</c> gate
/// (<c>LiveEntityNetworkUpdateController.OnPosition</c> →
/// <c>ProjectileController.CanAcceptPositionPayload</c> →
/// <c>LiveEntityInboundAuthorityGate</c>'s <c>!payloadIsValid</c>
/// return).
///
/// <para>
/// The three shapes are the three terms of that rule. <c>LandblockId</c>
/// 0 is the one that mattered: <c>LandDefs.InboundValidCellId</c> rejects
/// a 0 low word, and since D1 an unvalidated 0 flowed straight into
/// <c>CommitWireCellRebucket</c>, whose own doc calls cell 0 + landblock
/// 0 "the withdrawal shape" — silently de-residencing the entity in the
/// field <c>RuntimeEntityObjectViews.Snapshot</c> hands every bot as
/// <c>CellId</c>.
/// </para>
///
/// <para>
/// The last assertion is the discriminating one, and it is what makes
/// this a BEFORE-THE-MERGE test rather than a before-the-commit test: a
/// valid packet at the SAME position sequence the three refused packets
/// carried still applies. If the refusal sat anywhere downstream of
/// <c>TryApplyPosition</c>, the timestamp gate would have consumed that
/// sequence and the follow-up would be <c>Rejected</c>.
/// </para>
/// </summary>
[Fact]
public void InvalidPositionPayload_IsRefusedBeforeTheMerge_InANoWindowHost()
{
using StartedRuntime started = StartRuntime();
GameRuntime runtime = started.Runtime;
CommitLandblockCollision(runtime, 0x01010000u);
RuntimeFirstEntryDriveController drive = CreateDrive(runtime);
using var session = new WorldSession(
new IPEndPoint(IPAddress.Loopback, 9000),
new FixtureTransport());
var controller = new RuntimeLiveEntitySessionController(
runtime,
session,
worldProjection: new FixtureWorldProjection());
LiveEntitySessionSink sink = controller.CreateSink();
WorldSession.EntitySpawn spawn =
SpawnAt(0x70000051u, incarnation: 1, 0x01010001u);
sink.Spawned(spawn);
DrainFirstEntry(runtime, drive);
Assert.True(runtime.EntityObjects.Entities.TryGetActive(
spawn.Guid,
out RuntimeEntityRecord remote));
uint placedCell = remote.FullCellId;
uint placedLandblock = remote.CanonicalLandblockId;
float placedX = remote.Snapshot.Position!.Value.PositionX;
Assert.NotEqual(0u, placedCell);
const ushort refusedSequence = 7;
// (a) LandblockId 0 — the withdrawal shape.
sink.PositionUpdated(PositionUpdate(
spawn.Guid,
0u,
positionX: 20f,
positionSequence: refusedSequence));
Assert.Equal(placedCell, remote.FullCellId);
Assert.Equal(placedLandblock, remote.CanonicalLandblockId);
Assert.Equal(placedX, remote.Snapshot.Position!.Value.PositionX);
// (b) A NaN origin component — retail Frame::IsValid @0x00534ED0.
sink.PositionUpdated(PositionUpdate(
spawn.Guid,
0x01010031u,
positionX: float.NaN,
positionSequence: refusedSequence));
Assert.Equal(placedCell, remote.FullCellId);
Assert.Equal(placedX, remote.Snapshot.Position!.Value.PositionX);
// (c) A non-finite velocity — the graphical gate's third term.
WorldSession.EntityPositionUpdate infiniteVelocity = PositionUpdate(
spawn.Guid,
0x01010031u,
positionX: 22f,
positionSequence: refusedSequence) with
{
Velocity = new System.Numerics.Vector3(
float.PositiveInfinity,
0f,
0f),
};
sink.PositionUpdated(infiniteVelocity);
Assert.Equal(placedCell, remote.FullCellId);
Assert.Equal(placedX, remote.Snapshot.Position!.Value.PositionX);
// The discriminator: POSITION_TS was never consumed.
const uint movedCell = 0x01010031u;
sink.PositionUpdated(PositionUpdate(
spawn.Guid,
movedCell,
positionX: 23f,
positionSequence: refusedSequence));
Assert.Equal(movedCell, remote.FullCellId);
Assert.Equal(23f, remote.Snapshot.Position!.Value.PositionX);
}
/// <summary>
/// D1's missile gate. The graphical route sends a BOUND projectile's
/// accepted Position to the canonical projectile placement owner and
@ -737,7 +848,12 @@ public sealed class RuntimeLiveEntitySessionControllerTests
Assert.NotNull(bound.Projectile);
Assert.Null(unbound.Projectile);
const uint movedCell = 0x01010051u;
// 0x…0012, not the 0x…0051 this fixture used before the C5b
// follow-up: a low word above 0x40 is not a legal landcell
// (LandDefs.CellLowInRange), so the payload gate this route now
// shares with the graphical host would refuse BOTH packets and the
// missile distinction under test would never be reached.
const uint movedCell = 0x01010012u;
sink.PositionUpdated(PositionUpdate(
boundGuid,
movedCell,