feat(physics): C4 route 2 — ForcePosition through the canonical placement
A local-player ForcePosition had TWO independent writers for one accepted
packet: LocalForcePositionTransaction snapped the physics body
(PlayerMovementController.BlipPosition, a raw SnapToCell with no collision
resolve), while LiveEntityNetworkUpdateController's generic tail separately
wrote position/cell/rotation to the render WorldEntity from the raw wire and
rebucketed it. Two stores, one packet — the divergence class 670f307c fixed on
the remote path. The outbound AutonomousPosition ack also fired BEFORE any
canonical commit existed: we told ACE "got it, I'm here" before deciding where
"here" was, and the trailing isCurrent() could only suppress the continuation,
never recall the packet.
RuntimeAcceptedPositionDriveController is now the one Runtime-owned seam. Both
hosts call the identical TryExecuteAcceptedLocalPosition; App and headless
project the committed result through the existing placement projection sink
(LiveEntityRuntime.TryApplyRuntimePlacementPlace already performed the same
four writes, from committed state rather than a wire guess).
Retail: SmartBox::HandleReceivedPosition @0x00453FD0's FORCE_POSITION branch is
get_heading -> Frame::set_heading -> SmartBox::BlipPlayer @0x00453940 -> stamp
POSITION_TS -> SendPositionEvent @0x00454091 -> return @0x0045409D. BlipPlayer
is CPhysicsObj::SetPositionSimple @0x005162B0 with flags 0x1012
(Teleport|Slide|SendPositionEvent) — a real collision-resolving SetPosition,
not a snap. The pinned classifier already encoded this exactly.
Named behaviour changes:
* The ack is now an OUTPUT of the committed route, fired strictly after the
canonical commit and exactly once per accepted force packet.
* The ForcePosition route no longer re-arms the constraint leash. The force
branch returns at 0x0045409D, ahead of all three ConstrainTo sites
(0x00454272, 0x0045418A, 0x004541EC); the old re-arm cited retail's "Player,
normal" branch, which BlipPlayer is not on. The teleport, CommitPreparedPosition
and first-entry callers legitimately still constrain and are untouched.
* A force correction that terminates WITHOUT committing still sends its
position event and is not retried — retail's BlipPlayer discards
SetPositionSimple's SetPositionError return and acks unconditionally.
A single _pending funnel owns the in-flight placement, deciding on the token's
PositionAuthorityVersion against the record's: equal -> clear; advanced with the
newest accepted event still a force -> re-issue, re-classified; advanced to an
ordinary Apply -> clear, since newer server truth owns that pose. This closes a
double-apply/double-ack and a silently-dropped correction that two earlier
iterations of this slice each introduced.
AD-62 records the residual: a ForcePosition our async collision publication
cannot carry to a committed placement is not re-applied. Retail has no park —
its world is fully resident and its placement synchronous — so the state is
unreachable there. AP-131 is NOT retired; its legacy Position caller is route 4.
Deleted: LocalForcePositionTransaction, PlayerMovementController.BlipPosition,
HeadlessSessionWorldProjection.BlipLocalPlayer.
Gates: complete Release solution 10,858 passed / 4 skipped / 0 failed (baseline
10,844/4/0). Two independent Opus reviews (retail-conformance and
architecture/adversarial) PASS on the final diff after three FAIL rounds; every
intermediate state was fully green, so the suite caught none of the four real
defects. Connected acceptance is NOT run: nothing a user can do makes ACE emit
a ForcePosition without retail's @pklite, which acdream does not implement — see
docs/research/2026-08-03-c4-route-2-visual-gate.md.
Known gap, recorded not claimed: the plan's acceptance item 2 is unmet. The App
double-write check is a source pin, and "the committed projection moves the
render entity" is uncovered at any layer (#292). Filed alongside: #286-#291,
#293-#296.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
22a5c95400
commit
9966b53174
25 changed files with 4292 additions and 195 deletions
|
|
@ -246,6 +246,73 @@ public sealed class RuntimeLiveEntitySessionControllerTests
|
|||
Assert.True(runtime.Portal.Snapshot.Completed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R2/R3 review fix (2026-08-03). No accepted-position drive is supplied
|
||||
/// (mirrors <see cref="ContentLessDirectSink_KeepsPreFlipLegacyRegistration"/>'s
|
||||
/// "content-less" shape, applied to route 2 specifically), so
|
||||
/// <c>TryExecuteAcceptedLocalPosition</c> can never run and every
|
||||
/// ForcePosition resolves <c>NotApplicable</c> at the call site. Proves
|
||||
/// two things the first implementation pass got wrong: (R2)
|
||||
/// <see cref="IRuntimeDirectWorldProjection.CenterOnAcceptedForcePosition"/>
|
||||
/// still fires — a host is not left holding a stale collision/streaming
|
||||
/// window just because the drive is momentarily absent or NotApplicable
|
||||
/// — and (R3) the pre-existing <c>ProjectPosition</c> fallback still runs
|
||||
/// for a NotApplicable result, exactly like it did before route 2 existed
|
||||
/// (the "no legacy fallback to run instead" comment the review found at
|
||||
/// this exact call site was false).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ForcePositionWithoutAnAcceptedPositionDrive_StillCentersAndFallsBackToProjectPosition()
|
||||
{
|
||||
using StartedRuntime started = StartRuntime();
|
||||
GameRuntime runtime = started.Runtime;
|
||||
const uint playerGuid = 0x50000005u;
|
||||
runtime.PlayerIdentity.ServerGuid = playerGuid;
|
||||
using var session = new WorldSession(
|
||||
new IPEndPoint(IPAddress.Loopback, 9000),
|
||||
new FixtureTransport());
|
||||
session.GameActionCapture = _ => { };
|
||||
var projection = new FixtureWorldProjection();
|
||||
// Deliberately no acceptedPositionDrive argument — mirrors a
|
||||
// content-less headless host, where the accepted-position drive is
|
||||
// null and route 2 can never apply.
|
||||
var controller = new RuntimeLiveEntitySessionController(
|
||||
runtime,
|
||||
session,
|
||||
worldProjection: projection);
|
||||
LiveEntitySessionSink sink = controller.CreateSink();
|
||||
WorldSession.EntitySpawn spawn =
|
||||
Spawn(playerGuid, incarnation: 1);
|
||||
|
||||
sink.Spawned(spawn);
|
||||
Assert.Equal(1, projection.SpawnCount);
|
||||
Assert.Equal(0, projection.CenterOnForceCount);
|
||||
|
||||
sink.PositionUpdated(new WorldSession.EntityPositionUpdate(
|
||||
playerGuid,
|
||||
spawn.Position!.Value with
|
||||
{
|
||||
PositionX = 30f,
|
||||
},
|
||||
Velocity: null,
|
||||
PlacementId: null,
|
||||
IsGrounded: true,
|
||||
InstanceSequence: 1,
|
||||
PositionSequence: 2,
|
||||
TeleportSequence: 0,
|
||||
ForcePositionSequence: 1));
|
||||
|
||||
Assert.Equal(1, projection.CenterOnForceCount);
|
||||
Assert.Equal(playerGuid, projection.LastCenteredRecord?.ServerGuid);
|
||||
// The fallback ran: ProjectPosition observed this exact
|
||||
// ForcePosition disposition, not just the earlier Apply-shaped spawn
|
||||
// follow-up.
|
||||
Assert.Equal(1, projection.PositionCount);
|
||||
Assert.Equal(
|
||||
PositionTimestampDisposition.ForcePosition,
|
||||
projection.LastPositionDisposition);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// C3c-R1 review F6: the drive controller outlives its session routes,
|
||||
/// so "session reset precedes a new route" is an asserted latch, not a
|
||||
|
|
@ -674,6 +741,8 @@ public sealed class RuntimeLiveEntitySessionControllerTests
|
|||
public int PositionCount { get; private set; }
|
||||
public int TeleportStartCount { get; private set; }
|
||||
public int PrepareCount { get; private set; }
|
||||
public int CenterOnForceCount { get; private set; }
|
||||
public RuntimeEntityRecord? LastCenteredRecord { get; private set; }
|
||||
public bool LastSpawnWasLocal { get; private set; }
|
||||
public bool LastPositionWasLocal { get; private set; }
|
||||
public PositionTimestampDisposition LastPositionDisposition
|
||||
|
|
@ -704,6 +773,12 @@ public sealed class RuntimeLiveEntitySessionControllerTests
|
|||
LastPositionDisposition = disposition;
|
||||
}
|
||||
|
||||
public void CenterOnAcceptedForcePosition(RuntimeEntityRecord record)
|
||||
{
|
||||
CenterOnForceCount++;
|
||||
LastCenteredRecord = record;
|
||||
}
|
||||
|
||||
public void BeginTeleport() => TeleportStartCount++;
|
||||
|
||||
public RuntimeDestinationReadiness PrepareDestination(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue