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 by735f0a72and fixed only atff100cf3. 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:
parent
ff100cf33f
commit
9ee9c1a1a6
7 changed files with 455 additions and 21 deletions
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue