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
|
|
@ -17,6 +17,7 @@ internal sealed class HeadlessSessionEventRoute : ILiveSessionEventRouting
|
|||
private readonly GameRuntime _runtime;
|
||||
private readonly IRuntimePlacementProjectionSink _placements;
|
||||
private readonly RuntimeFirstEntryDriveController? _firstEntry;
|
||||
private readonly RuntimeAcceptedPositionDriveController? _acceptedPositionDrive;
|
||||
private readonly Action<RuntimeEntityRecord>? _localPlayerCompleted;
|
||||
private RuntimePlacementProjectionSubscription? _subscription;
|
||||
private bool _attachStarted;
|
||||
|
|
@ -28,7 +29,8 @@ internal sealed class HeadlessSessionEventRoute : ILiveSessionEventRouting
|
|||
GameRuntime runtime,
|
||||
IRuntimePlacementProjectionSink placements,
|
||||
RuntimeFirstEntryDriveController? firstEntry = null,
|
||||
Action<RuntimeEntityRecord>? localPlayerCompleted = null)
|
||||
Action<RuntimeEntityRecord>? localPlayerCompleted = null,
|
||||
RuntimeAcceptedPositionDriveController? acceptedPositionDrive = null)
|
||||
{
|
||||
_events = events ?? throw new ArgumentNullException(nameof(events));
|
||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||
|
|
@ -36,6 +38,7 @@ internal sealed class HeadlessSessionEventRoute : ILiveSessionEventRouting
|
|||
?? throw new ArgumentNullException(nameof(placements));
|
||||
_firstEntry = firstEntry;
|
||||
_localPlayerCompleted = localPlayerCompleted;
|
||||
_acceptedPositionDrive = acceptedPositionDrive;
|
||||
}
|
||||
|
||||
public void Attach()
|
||||
|
|
@ -52,6 +55,9 @@ internal sealed class HeadlessSessionEventRoute : ILiveSessionEventRouting
|
|||
// detached — session reset precedes a new route — before this route
|
||||
// takes ownership of the shared drive controller's tracked entries.
|
||||
_firstEntry?.AttachRoute(this, _localPlayerCompleted);
|
||||
// C4 route 2: same one-route-at-a-time latch for the accepted-
|
||||
// Position drive controller.
|
||||
_acceptedPositionDrive?.AttachRoute(this);
|
||||
_events.Attach();
|
||||
_subscription = new RuntimePlacementProjectionSubscription(
|
||||
_runtime,
|
||||
|
|
@ -73,6 +79,7 @@ internal sealed class HeadlessSessionEventRoute : ILiveSessionEventRouting
|
|||
// route-scoped — a route that never attached cannot clear a live
|
||||
// route's entries.
|
||||
_firstEntry?.DetachRoute(this);
|
||||
_acceptedPositionDrive?.DetachRoute(this);
|
||||
if (!_eventsDisposed)
|
||||
{
|
||||
_events.Dispose();
|
||||
|
|
|
|||
|
|
@ -126,6 +126,11 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
/// Runtime lifetime) plus the active world projection it pumps
|
||||
/// through.</summary>
|
||||
private RuntimeFirstEntryDriveController? _firstEntryDrive;
|
||||
/// <summary>C4 route 2 (2026-08-03): see the ctor comment in
|
||||
/// <see cref="CreateEventRoute"/> — cached across reconnects exactly
|
||||
/// like <see cref="_firstEntryDrive"/>.</summary>
|
||||
private RuntimeAcceptedPositionDriveController? _acceptedPositionDrive;
|
||||
private AcDream.Core.Net.WorldSession? _currentSession;
|
||||
private HeadlessSessionWorldProjection? _worldProjection;
|
||||
private int _disposeStage;
|
||||
private long _reconnectDeadline;
|
||||
|
|
@ -311,8 +316,20 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
_policy.Tick(Runtime, Commands);
|
||||
}
|
||||
|
||||
internal RuntimeTeardownAcknowledgement Stop() =>
|
||||
Commands.Session.Stop(Runtime.Generation);
|
||||
internal RuntimeTeardownAcknowledgement Stop()
|
||||
{
|
||||
RuntimeTeardownAcknowledgement result =
|
||||
Commands.Session.Stop(Runtime.Generation);
|
||||
// R9 review fix (2026-08-03): _currentSession is cached across
|
||||
// reconnects (see CreateEventRoute's comment) so the accepted-
|
||||
// position drive controller's outbound-ack accessor always reads the
|
||||
// CURRENT session, never one captured at first construction. Without
|
||||
// clearing it here, that accessor would keep returning a stopped
|
||||
// (possibly disposed) WorldSession in the window between this Stop
|
||||
// and the next CreateEventRoute call.
|
||||
_currentSession = null;
|
||||
return result;
|
||||
}
|
||||
|
||||
internal void Quarantine(Exception error)
|
||||
{
|
||||
|
|
@ -536,6 +553,25 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
private ILiveSessionEventRouting CreateEventRoute(
|
||||
AcDream.Core.Net.WorldSession session)
|
||||
{
|
||||
// C4 route 2 (2026-08-03): reconnects construct a FRESH WorldSession
|
||||
// each call, but the accepted-Position drive controller below is
|
||||
// cached across reconnects (`??=`) exactly like _firstEntryDrive —
|
||||
// its ack-firing accessor must therefore read the CURRENT session
|
||||
// through this field, never one captured at first construction.
|
||||
_currentSession = session;
|
||||
// R9 review note (2026-08-03): a content-less host (_contentLease is
|
||||
// null — a validated-legal headless configuration, see
|
||||
// RuntimeLiveEntitySessionController.OnSpawned's own R3 comment)
|
||||
// never constructs _acceptedPositionDrive OR worldProjection below,
|
||||
// so a ForcePosition on such a host resolves NotApplicable with no
|
||||
// ProjectPosition fallback either. This is NOT a behaviour change:
|
||||
// a content-less host never registers a first-entry residence, so
|
||||
// Runtime.MovementOwner.Controller is always null there too, and
|
||||
// LocalPlayerOutboundController.SendImmediatePosition's own
|
||||
// controller-null guard already made the PRE-route-2 unconditional
|
||||
// ack call a no-op in this exact configuration. There is no live
|
||||
// controller for either the old or the new path to place or
|
||||
// acknowledge.
|
||||
IRuntimeDirectWorldProjection? worldProjection = null;
|
||||
if (_contentLease is { } content)
|
||||
{
|
||||
|
|
@ -557,10 +593,24 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
Radius: 0.48f,
|
||||
Height: 1.835f,
|
||||
RuntimeLocalPlayerShadowDisposition.ProvenShapeless));
|
||||
// C4 route 2: one drive controller per host, mirroring
|
||||
// _firstEntryDrive exactly — same persistent Runtime lifetime,
|
||||
// collision source, and clock.
|
||||
_acceptedPositionDrive ??= new RuntimeAcceptedPositionDriveController(
|
||||
Runtime.EntityObjects,
|
||||
Runtime.Clock,
|
||||
content.PreparedCollision,
|
||||
new LocalPlayerOutboundController((_, _, _, _, _, _) => { }),
|
||||
() => Runtime.Generation,
|
||||
() => Runtime.PlayerIdentity.ServerGuid,
|
||||
() => Runtime.MovementOwner.Controller,
|
||||
() => Runtime.CharacterOwner.UsePositionFromServer,
|
||||
() => _currentSession);
|
||||
var projection = new HeadlessSessionWorldProjection(
|
||||
Runtime,
|
||||
content,
|
||||
_firstEntryDrive);
|
||||
_firstEntryDrive,
|
||||
_acceptedPositionDrive);
|
||||
_worldProjection = projection;
|
||||
worldProjection = projection;
|
||||
}
|
||||
|
|
@ -571,7 +621,8 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
_descriptor.Id,
|
||||
message,
|
||||
Runtime.Generation.Value),
|
||||
worldProjection);
|
||||
worldProjection,
|
||||
_acceptedPositionDrive);
|
||||
var route = new LiveSessionEventRouter(
|
||||
session,
|
||||
entities.CreateSink(),
|
||||
|
|
@ -620,7 +671,8 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
Runtime,
|
||||
new HeadlessRuntimePlacementProjectionSink(Runtime),
|
||||
_firstEntryDrive,
|
||||
_ => session.SendGameAction(GameActionLoginComplete.Build()));
|
||||
_ => session.SendGameAction(GameActionLoginComplete.Build()),
|
||||
_acceptedPositionDrive);
|
||||
}
|
||||
|
||||
private static LiveSessionCharacterSelector MapCharacterSelector(
|
||||
|
|
|
|||
|
|
@ -504,29 +504,39 @@ internal sealed class HeadlessSessionWorldProjection
|
|||
private readonly GameRuntime _runtime;
|
||||
private readonly IHeadlessCollisionNeighborhood _collision;
|
||||
private readonly RuntimeFirstEntryDriveController? _firstEntry;
|
||||
/// <summary>
|
||||
/// C4 route 2 (2026-08-03): pumped alongside <see cref="_firstEntry"/> so
|
||||
/// a ForcePosition parked awaiting its destination collision generation
|
||||
/// resolves promptly.
|
||||
/// </summary>
|
||||
private readonly RuntimeAcceptedPositionDriveController? _acceptedPositionDrive;
|
||||
private uint _requestedLocalPlayerCell;
|
||||
|
||||
internal HeadlessSessionWorldProjection(
|
||||
GameRuntime runtime,
|
||||
HeadlessProcessContentOwner.HeadlessProcessContentLease content,
|
||||
RuntimeFirstEntryDriveController? firstEntry = null)
|
||||
RuntimeFirstEntryDriveController? firstEntry = null,
|
||||
RuntimeAcceptedPositionDriveController? acceptedPositionDrive = null)
|
||||
: this(
|
||||
runtime,
|
||||
new HeadlessCollisionNeighborhood(runtime, content),
|
||||
firstEntry)
|
||||
firstEntry,
|
||||
acceptedPositionDrive)
|
||||
{
|
||||
}
|
||||
|
||||
internal HeadlessSessionWorldProjection(
|
||||
GameRuntime runtime,
|
||||
IHeadlessCollisionNeighborhood collision,
|
||||
RuntimeFirstEntryDriveController? firstEntry = null)
|
||||
RuntimeFirstEntryDriveController? firstEntry = null,
|
||||
RuntimeAcceptedPositionDriveController? acceptedPositionDrive = null)
|
||||
{
|
||||
_runtime = runtime
|
||||
?? throw new ArgumentNullException(nameof(runtime));
|
||||
_collision = collision
|
||||
?? throw new ArgumentNullException(nameof(collision));
|
||||
_firstEntry = firstEntry;
|
||||
_acceptedPositionDrive = acceptedPositionDrive;
|
||||
}
|
||||
|
||||
public void ProjectSpawn(
|
||||
|
|
@ -569,6 +579,7 @@ internal sealed class HeadlessSessionWorldProjection
|
|||
.TryConvertInitialResidenceToCellessRoute(record);
|
||||
}
|
||||
_firstEntry?.DriveAll();
|
||||
_acceptedPositionDrive?.Advance();
|
||||
}
|
||||
|
||||
public void ProjectPosition(
|
||||
|
|
@ -592,11 +603,43 @@ internal sealed class HeadlessSessionWorldProjection
|
|||
_collision.CenterOn(position.LandblockId);
|
||||
}
|
||||
_firstEntry?.DriveAll();
|
||||
_acceptedPositionDrive?.Advance();
|
||||
}
|
||||
// C4 route 2 (2026-08-03): a ForcePosition on the local player is
|
||||
// dispatched directly to RuntimeAcceptedPositionDriveController
|
||||
// instead of running the branch below this comment — the deleted
|
||||
// BlipLocalPlayer's body-commit/controller-reconciliation/ack job is
|
||||
// that controller's now (CenterOnAcceptedForcePosition covers the
|
||||
// re-centering half). R3 review fix (2026-08-03): when that call
|
||||
// returns NotApplicable — most commonly THIS controller-null branch,
|
||||
// reached while route 1 hasn't published a controller yet —
|
||||
// RuntimeLiveEntitySessionController.OnPositionUpdated still falls
|
||||
// back to calling this method, exactly like every other disposition.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R2 review fix (2026-08-03): the centering half of the deleted
|
||||
/// <c>BlipLocalPlayer</c> — <c>_collision.CenterOn(position.LandblockId)</c>
|
||||
/// plus the <see cref="_requestedLocalPlayerCell"/> update
|
||||
/// <see cref="PumpFirstEntry"/> polls. Without this, a ForcePosition to a
|
||||
/// destination outside the neighborhood's current 3x3 window
|
||||
/// (<c>HeadlessCollisionNeighborhood.BuildPublicationPlan</c>) would open
|
||||
/// a <c>DeferredCell</c> park this host's window can never publish —
|
||||
/// see <see cref="AcDream.Runtime.Session.RuntimeAcceptedPositionDriveController.Advance"/>'s
|
||||
/// R1 doc comment. Called BEFORE the drive controller submits so the
|
||||
/// destination is already inside the window by the time it decides
|
||||
/// whether to park.
|
||||
/// </summary>
|
||||
public void CenterOnAcceptedForcePosition(RuntimeEntityRecord record)
|
||||
{
|
||||
if (record.ServerGuid != _runtime.PlayerIdentity.ServerGuid
|
||||
|| record.Snapshot.Position is not { LandblockId: not 0u } position)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (disposition is PositionTimestampDisposition.ForcePosition)
|
||||
BlipLocalPlayer(record);
|
||||
_requestedLocalPlayerCell = position.LandblockId;
|
||||
_collision.CenterOn(position.LandblockId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -610,6 +653,7 @@ internal sealed class HeadlessSessionWorldProjection
|
|||
if (_requestedLocalPlayerCell != 0u)
|
||||
_ = _collision.IsReady(_requestedLocalPlayerCell);
|
||||
_firstEntry?.DriveAll();
|
||||
_acceptedPositionDrive?.Advance();
|
||||
}
|
||||
|
||||
public void BeginTeleport()
|
||||
|
|
@ -704,26 +748,4 @@ internal sealed class HeadlessSessionWorldProjection
|
|||
controller.SetBodyOrientation(orientation);
|
||||
}
|
||||
|
||||
private void BlipLocalPlayer(RuntimeEntityRecord record)
|
||||
{
|
||||
if (record.ServerGuid
|
||||
!= _runtime.PlayerIdentity.ServerGuid
|
||||
|| record.Snapshot.Position is not { } position
|
||||
|| _runtime.MovementOwner.Controller is not { } controller)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_collision.CenterOn(position.LandblockId);
|
||||
Vector3 wirePosition = new(
|
||||
position.PositionX,
|
||||
position.PositionY,
|
||||
position.PositionZ);
|
||||
controller.LocalEntityId = record.LocalEntityId ?? 0u;
|
||||
controller.BlipPosition(
|
||||
wirePosition,
|
||||
position.LandblockId,
|
||||
wirePosition);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue