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:
Erik 2026-08-03 18:46:36 +02:00
parent 22a5c95400
commit 9966b53174
25 changed files with 4292 additions and 195 deletions

View file

@ -0,0 +1,906 @@
using System.Numerics;
using AcDream.Content;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Physics;
namespace AcDream.Runtime.Session;
/// <summary>
/// Typed yields for
/// <see cref="RuntimeAcceptedPositionDriveController.TryExecuteAcceptedLocalPosition"/>.
/// </summary>
internal enum RuntimeAcceptedPositionExecutionStatus : byte
{
/// <summary>
/// Out of this route's scope (not a ForcePosition, not the local player,
/// no canonical body, or an initial-Create residence still owns the
/// record). The caller's own pre-flip path runs unchanged.
/// </summary>
NotApplicable,
/// <summary>
/// Either (a) the classifier itself rejected this exact frame (non-finite
/// data, an invalid entity kind) before any SetPosition ran and no ack was
/// sent, or (b) a SetPosition WAS begun and submitted but was then
/// rejected/cancelled by <see cref="RuntimeSetPositionState.TryPrepareAndSubmitAuthoredPlacement"/>
/// (invalid prepared data, authority displaced mid-submit) — R5 review
/// fix (2026-08-03): the two call sites inside <c>SubmitAndResolve</c>
/// both reach this status AFTER a SetPosition ran, so "no SetPosition
/// ran" is only true for case (a).
///
/// Round 3 (2026-08-03): the two statuses differ in acknowledgement. Case
/// (a) sends nothing — a classifier-rejected frame is not an accepted
/// authority. Case (b) IS an accepted force packet whose placement was
/// begun and failed, so <c>SettlePending</c> sends its retail position
/// event carrying the body's unchanged pose (<c>SmartBox::BlipPlayer</c>
/// @0x00453940 discards <c>SetPositionSimple</c>'s error and
/// <c>SmartBox::HandleReceivedPosition</c> @0x00453FD0 acks
/// unconditionally @0x00454091).
/// </summary>
Rejected,
/// <summary>
/// The entity already holds an active SetPosition operation (or an
/// unacknowledged completion); begin failed on transient contention, not
/// staleness. This packet's classified force route was still recorded as
/// the drive's newest accepted force observation BEFORE the failed begin
/// (see <c>_newestForce</c>), so when the contending operation is this
/// controller's OWN tracked <c>_pending</c> — the B1 case: a park that
/// woke and had its <c>Place</c> ACCEPTED, whose retained completion
/// survives <c>RuntimeSetPositionState.Forget</c>'s early return and
/// blocks the next begin — the single <c>_pending</c> funnel
/// (<c>SettlePending</c>) re-issues THIS packet's correction the moment
/// that older completion is consumed. When it is a genuinely EXTERNAL
/// operation (a concurrent portal/teleport placement on the same entity)
/// and nothing of this route's own is pending, this route correctly
/// yields — a later accepted Position re-attempts once that other
/// placement completes, exactly like every other route yields to a
/// concurrent placement authority.
/// </summary>
Contention,
/// <summary>
/// The destination landblock's collision generation was not ready; the
/// operation parked. The ack fires later, once
/// <see cref="RuntimeAcceptedPositionDriveController.Advance"/> observes
/// the deferred commit.
/// </summary>
DeferredCell,
/// <summary>
/// The canonical SetPosition committed synchronously (retail
/// <c>CPhysicsObj::SetPositionSimple</c> @0x005162B0, called from
/// <c>SmartBox::BlipPlayer</c> @0x00453940). The controller-local
/// reconciliation ran and the outbound ack (if any) already went out.
/// </summary>
Committed,
}
/// <summary>
/// C4 route 2: the Runtime-owned accepted-Position execution seam for a
/// ForcePosition on an already-live local player. Retail
/// <c>SmartBox::HandleReceivedPosition</c> (@0x00453FD0,
/// acclient_2013_pseudo_c.txt:92896) FORCE_POSITION branch:
///
/// <code>
/// get_heading(player);
/// Frame::set_heading(&amp;dest, heading); // 00454068 preserve OUR heading
/// SmartBox::BlipPlayer(this, &amp;dest); // 00454074
/// player-&gt;update_times[0] = arg7; // 00454079 stamp POSITION_TS
/// cmdinterp-&gt;SendPositionEvent(); // 00454091 ack AFTER the commit
/// return; // 0045409d
/// </code>
///
/// <c>SmartBox::BlipPlayer</c> (@0x00453940, line 92528) calls
/// <c>CPhysicsObj::SetPositionSimple</c> (@0x005162B0, line 284276), which
/// with a non-null destination frame builds a <c>SetPositionStruct</c> with
/// flags <c>0x1012</c> (<c>Teleport|Slide|SendPositionEvent</c> —
/// <see cref="RuntimeAuthoritativePositionRouteClassifier"/>'s
/// <c>AuthoritativeTeleportFlags</c>) and calls
/// <c>CPhysicsObj::SetPosition</c>. This class is the Runtime consumer the
/// classifier's ForcePosition branch never had: it drives
/// <see cref="RuntimeSetPositionState.TryBeginExclusiveAuthoredPlacement"/> +
/// <see cref="RuntimeSetPositionState.TryPrepareAndSubmitAuthoredPlacement"/>
/// exactly like <see cref="RuntimeFirstEntryDriveController"/>'s continuation
/// completion, then fires the ack strictly AFTER the canonical commit.
///
/// Two named behaviour changes versus the deleted App/no-window authorities
/// this replaces:
/// (1) the outbound <c>AutonomousPosition</c> ack now fires only after the
/// canonical commit (previously it left before any Runtime commit existed —
/// see the deleted <c>LocalForcePositionTransaction</c>/
/// <c>HeadlessSessionWorldProjection.BlipLocalPlayer</c> pair);
/// (2) the constraint leash is NOT re-armed here — every
/// <c>CPhysicsObj::ConstrainTo</c> call in <c>HandleReceivedPosition</c>
/// (@0x00454272/0x0045418A/0x004541EC) is on a branch the FORCE_POSITION
/// early return (@0x0045409D) never reaches; the deleted
/// <c>PlayerMovementController.BlipPosition</c>'s re-arm was an unbacked
/// deviation this route retires (docs/research/2026-08-03-c4-route-2-implementation-plan.md §1b).
///
/// One instance per host session route (graphical/headless), constructed
/// once per host process and reused across reconnects exactly like
/// <see cref="RuntimeFirstEntryDriveController"/> — <see cref="AttachRoute"/>/
/// <see cref="DetachRoute"/> assert the same "session reset precedes a new
/// route" ordering and clear any pending operation left by a torn-down
/// session. The controller tracks at most one pending operation (the local
/// player is the only entity this route ever touches).
///
/// R9 review note (2026-08-03): kept <c>public</c> — unlike its template
/// <see cref="RuntimeFirstEntryDriveController"/> (<c>internal sealed</c>),
/// this class is a required parameter type on
/// <see cref="RuntimeLiveEntitySessionController"/>'s own <c>public</c>
/// constructor (<c>src/AcDream.Runtime/Session/RuntimeLiveEntitySessionController.cs</c>),
/// so C# accessibility rules (CS0051) require it. Every constructor
/// parameter and member below is <c>internal</c>; only the type name itself
/// is public, and only because the class it is threaded through already is.
/// Narrowing <see cref="RuntimeLiveEntitySessionController"/> itself is a
/// separate, broader change outside this fix's scope.
/// </summary>
public sealed class RuntimeAcceptedPositionDriveController
{
private sealed class Pending
{
internal required RuntimeEntityRecord Record { get; init; }
internal required RuntimeEntityPlacementToken Token { get; init; }
internal required RuntimeAuthoritativePositionRoute Route { get; init; }
internal required bool AwaitingCommitWake { get; init; }
/// <summary>
/// Round 3 (2026-08-03): true while this descriptor stands for an
/// accepted force packet whose placement WAS begun and whose retail
/// position event (<c>CommandInterpreter::SendPositionEvent</c>
/// @0x006B4770, called unconditionally at
/// <c>SmartBox::HandleReceivedPosition</c> @0x00454091) has not gone
/// out yet. It is <c>false</c> only for the re-issue retry marker
/// <see cref="SettlePending"/> parks when a re-issue cannot even
/// begin: that marker stands for a packet whose placement was never
/// begun, so its ack is owed by the eventual re-issue's own terminal
/// outcome, not by the marker.
/// </summary>
internal required bool PositionEventOwed { get; init; }
}
/// <summary>
/// Round 2 unified mechanism (2026-08-03): the newest accepted local-player
/// ForcePosition this drive has been handed, stamped with the canonical
/// record's <c>PositionAuthorityVersion</c> as observed at that packet's
/// own merge. It is the drive's ONLY knowledge of "what disposition was
/// the newest accepted position event", because
/// <see cref="TryExecuteAcceptedLocalPosition"/> is dispatched by both
/// hosts for ForcePosition and only for ForcePosition — an ordinary
/// <c>Apply</c> merges (advancing <c>PositionAuthorityVersion</c>) without
/// ever reaching this class.
///
/// Round 3 correction (2026-08-03): this is a ONE-WAY test, not a
/// biconditional. If the live record's current
/// <c>PositionAuthorityVersion</c> differs from
/// <see cref="PositionAuthorityVersion"/>, then some other authority
/// advance has happened since this force was recorded and the funnel must
/// not re-issue it. The converse does NOT hold: equality does not prove
/// the newest accepted event was this ForcePosition, because
/// <c>RuntimeEntityRecordTable.AdvancePositionAuthority</c> has four call
/// sites, not one — the ordinary accepted-Position merge
/// (<c>RuntimeEntityObjectLifetime.cs:1647</c>) plus
/// <c>TryApplyPickup</c> (<c>:1116</c>),
/// <c>CommitPositionChannelUpdate</c> (<c>:2041</c>) and
/// <c>AdvanceCreateAuthority</c> (<c>:2466</c>). The latter three are
/// effectively unreachable for a live local player, but the funnel's
/// safety does not depend on that: an unnoticed advance can only make the
/// funnel decline a re-issue it might have made (register row
/// <b>AD-62</b>), never make it re-issue a stale pose.
/// </summary>
private readonly record struct AcceptedForceObservation(
RuntimeEntityKey Entity,
ulong PositionAuthorityVersion,
RuntimeAuthoritativePositionRoute Route);
private readonly RuntimeEntityObjectLifetime _entityObjects;
private readonly IGameRuntimeClock _clock;
private readonly IPreparedCollisionSource _collisionSource;
private readonly LocalPlayerOutboundController _localPlayerOutbound;
private readonly Func<RuntimeGenerationToken> _generation;
private readonly Func<uint> _localPlayerServerGuid;
private readonly Func<PlayerMovementController?> _localController;
private readonly Func<bool> _usePositionFromServer;
private readonly Func<WorldSession?> _session;
/// <summary>
/// The drive's at-most-one in-flight placement for the local player.
/// Round 2 unified mechanism (2026-08-03): exactly THREE members write
/// this field — <see cref="RetainPending"/> (the operation is still
/// outstanding), <see cref="SettlePending"/> (the single terminal-outcome
/// funnel), and <see cref="AbandonPending"/> (route teardown, which is
/// outside the operation lifecycle entirely). No branch of
/// <see cref="TryExecuteAcceptedLocalPosition"/>,
/// <see cref="SubmitAndResolve"/> or <see cref="Advance"/> assigns or
/// clears it directly. Round 1 spread that ownership across the
/// individual branches, which is the shared root cause of B1, N1 and N2
/// in docs/research/2026-08-03-c4-route-2-review-findings.md.
/// </summary>
private Pending? _pending;
private AcceptedForceObservation? _newestForce;
private object? _routeOwner;
internal RuntimeAcceptedPositionDriveController(
RuntimeEntityObjectLifetime entityObjects,
IGameRuntimeClock clock,
IPreparedCollisionSource collisionSource,
LocalPlayerOutboundController localPlayerOutbound,
Func<RuntimeGenerationToken> generation,
Func<uint> localPlayerServerGuid,
Func<PlayerMovementController?> localController,
Func<bool> usePositionFromServer,
Func<WorldSession?> session)
{
_entityObjects = entityObjects
?? throw new ArgumentNullException(nameof(entityObjects));
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
_collisionSource = collisionSource
?? throw new ArgumentNullException(nameof(collisionSource));
_localPlayerOutbound = localPlayerOutbound
?? throw new ArgumentNullException(nameof(localPlayerOutbound));
_generation = generation
?? throw new ArgumentNullException(nameof(generation));
_localPlayerServerGuid = localPlayerServerGuid
?? throw new ArgumentNullException(nameof(localPlayerServerGuid));
_localController = localController
?? throw new ArgumentNullException(nameof(localController));
_usePositionFromServer = usePositionFromServer
?? throw new ArgumentNullException(nameof(usePositionFromServer));
_session = session ?? throw new ArgumentNullException(nameof(session));
_entityObjects.RegisterAcceptedPositionDriveOwnership(
() => _pending is null ? 0 : 1);
}
internal int PendingCount => _pending is null ? 0 : 1;
/// <summary>
/// C3c-R1-style one-route-at-a-time latch (mirrors
/// <see cref="RuntimeFirstEntryDriveController.AttachRoute"/>): this
/// controller outlives its session routes (hosts reuse it across
/// reconnects), so the "session reset precedes a new route" ordering is
/// asserted, not assumed.
/// </summary>
internal void AttachRoute(object route)
{
ArgumentNullException.ThrowIfNull(route);
if (_routeOwner is not null && !ReferenceEquals(_routeOwner, route))
{
throw new InvalidOperationException(
"An accepted-position drive controller serves one session "
+ "route at a time; the prior route must be disposed "
+ "(session reset precedes a new route) before a "
+ "replacement attaches.");
}
_routeOwner = route;
}
/// <summary>
/// Route-scoped teardown: abandons any pending operation, but ONLY when
/// <paramref name="route"/> is the attached owner.
/// </summary>
internal void DetachRoute(object route)
{
ArgumentNullException.ThrowIfNull(route);
if (!ReferenceEquals(_routeOwner, route))
return;
_routeOwner = null;
AbandonPending();
}
/// <summary>
/// Route teardown — the one <see cref="_pending"/> write that is NOT a
/// terminal operation outcome. The torn-down route's newest accepted
/// force observation dies with it: a reconnect re-merges its own
/// positions, and a stale observation must never survive to authorize a
/// re-issue against a later session's record.
/// </summary>
private void AbandonPending()
{
_newestForce = null;
if (_pending is not { } pending)
return;
_pending = null;
RuntimeSetPositionState setPosition = _entityObjects.Physics.SetPosition;
setPosition.ForgetPlacementCompletion(pending.Token);
RuntimePlacementCancellationReceipt cancellation =
setPosition.ForgetExactPlacement(pending.Token);
if (cancellation.IsValid)
setPosition.PublishCancellation(cancellation);
}
/// <summary>
/// Executes a ForcePosition Position update against the canonical
/// Runtime SetPosition owner. <paramref name="record"/>'s Snapshot,
/// timestamps, and PositionAuthorityVersion must already reflect the
/// merge <see cref="RuntimeEntityObjectLifetime.TryApplyPosition"/>
/// performed for this exact <paramref name="update"/> — this method
/// never re-merges the wire frame; it only routes the already-accepted
/// pose through the canonical placement transaction.
/// </summary>
internal RuntimeAcceptedPositionExecutionStatus TryExecuteAcceptedLocalPosition(
RuntimeEntityRecord record,
in WorldSession.EntityPositionUpdate update,
PositionTimestampDisposition disposition,
in AcceptedPhysicsTimestamps timestamps,
ushort previousTeleportSequence)
{
ArgumentNullException.ThrowIfNull(record);
if (disposition is not PositionTimestampDisposition.ForcePosition
|| record.ServerGuid != _localPlayerServerGuid()
|| record.PhysicsBody is null
|| record.Key is not { } key
// Route 1 owns an active initial-Create residence: the
// executor already retains the Position as its own tail
// action (RuntimeInitialCreateContinuationExecutor
// .ApplyPositionAction) and already carries
// SendPositionImmediately. Route 2 must not double-drive it.
|| _entityObjects.TryGetInitialCreateResidence(record, out _))
{
return RuntimeAcceptedPositionExecutionStatus.NotApplicable;
}
RuntimeAuthoritativePositionRoute route = ClassifyForcePosition(
record, key, update, disposition, timestamps, previousTeleportSequence);
if (!route.Accepted)
{
// The wire's accepted timestamp/position channels were already
// merged into record.Snapshot upstream
// (RuntimeEntityObjectLifetime.TryApplyPosition) unconditionally
// on the disposition being non-Rejected — unlike the initial-
// Create continuation flow (whose merge happens INSIDE the
// route.Accepted branch), there is no separate "stamp-only"
// write left to perform here. A classifier rejection at this
// point is a data-validity failure (non-finite frame), not a
// staleness one; the caller's own currency re-check already
// covers staleness upstream.
return RuntimeAcceptedPositionExecutionStatus.Rejected;
}
// Round 2 unified mechanism: record THIS packet as the newest accepted
// force BEFORE attempting to begin, so a failed begin (Contention)
// still leaves the funnel able to re-issue this exact correction. A
// classifier-rejected frame deliberately never gets here: it is not an
// accepted authority and must never authorize a re-issue.
ulong acceptedVersion = record.PositionAuthorityVersion;
_newestForce = new AcceptedForceObservation(key, acceptedVersion, route);
RuntimeSetPositionState setPosition = _entityObjects.Physics.SetPosition;
RuntimeEntityPlacementToken token =
setPosition.TryBeginExclusiveAuthoredPlacement(
record,
acceptedVersion,
route.OperationKind);
if (!token.IsValid)
{
// The entity already owns an active operation (or an
// unacknowledged completion). Not staleness — see the Contention
// status doc.
return RuntimeAcceptedPositionExecutionStatus.Contention;
}
return SubmitAndResolve(record, token, route);
}
/// <summary>
/// Host cadence pump: resolves a parked DeferredCell operation once its
/// destination landblock's collision generation eventually commits it
/// (<c>RuntimeSetPositionState.CommitCollisionGeneration</c> →
/// <c>RetryDeferred</c> → <c>CommitCanonical</c>, driven entirely by
/// unrelated collision/streaming machinery — this pump never re-submits
/// the operation itself). Retries a preparation-only retry status
/// (<c>RetrySetupUnavailable</c>/<c>RetryWorldFrameUnavailable</c>) by
/// re-calling the SAME prepare+submit pair, exactly like
/// <see cref="RuntimeFirstEntryDriveController"/>'s own continuation
/// completion. Safe to call from any host cadence point; a no-op when
/// nothing is pending.
///
/// R1 review fix (2026-08-03): <c>RuntimeEntityObjectLifetime.TryApplyPosition</c>
/// calls <c>RuntimeSetPositionState.Forget</c> on EVERY accepted Position
/// for this entity — any disposition, not only ForcePosition — which
/// unconditionally cancels whatever operation this controller has
/// in-flight (<c>Forget</c> → <c>CancelCoreDeferred</c> →
/// <c>ForgetPlacementCompletionCore</c>, which drops the token from both
/// <c>_placementCompletionWatches</c> and
/// <c>_acknowledgedPlacementCompletions</c> with no trace). ACE broadcasts
/// at 5-10 Hz, so a <see cref="RuntimeAcceptedPositionExecutionStatus.DeferredCell"/>
/// park surviving past one broadcast interval is cancelled before its
/// collision generation can ever commit it — the exact far-destination
/// case the park exists to serve. The SAME cancellation path is also how
/// supersession, the lost-cell deadline, <c>ParkCollisionResidents</c>,
/// and a generation change retire an operation
/// (<c>ForgetPlacementCompletionCore</c> is their common funnel too).
///
/// Retail <c>SmartBox::BlipPlayer</c> (@0x00453940) has no "give up
/// quietly" state — every accepted Position it sees gets applied. So
/// rather than silently leaking <see cref="_pending"/> forever (which
/// would also pin <c>AcceptedPositionDrivePendingCount</c> non-zero for
/// the rest of the session — <c>GameWindowLifetime.DisposeGameRuntime</c>
/// throws on non-convergence), this pump detects the cancellation via the
/// existing read-only <c>RuntimeSetPositionState.IsPlacementCompletionTracked</c>
/// query (a watched-and-not-yet-cancelled DeferredCell park) or
/// <c>IsPlacementCurrent</c> (a still-in-flight prepare retry) and, when
/// neither holds, hands the dead operation to the single
/// <see cref="SettlePending"/> funnel, which decides on ONE input whether
/// a re-issue is owed.
/// </summary>
internal void Advance()
{
if (_pending is not { } pending)
return;
RuntimeSetPositionState setPosition = _entityObjects.Physics.SetPosition;
if (pending.AwaitingCommitWake)
{
if (setPosition.TryPeekAcknowledgedPlacement(
pending.Token,
out RuntimePlacementProjectionToken projection))
{
if (!setPosition.ConsumeAcknowledgedPlacement(
pending.Token, projection))
{
// Raced against a concurrent consumer; retry next pump.
return;
}
// Retail order: the deferred commit's own reconciliation and
// ack come first, THEN the funnel decides whether a newer
// accepted force is still owed a placement (B1).
ReconcileAndAcknowledge(pending.Record, pending.Route);
SettlePending(
pending.Record,
pending.Token,
pending.Route,
positionEventOwed: false);
return;
}
if (setPosition.IsPlacementCompletionTracked(pending.Token))
{
// Still parked, watch alive. Nothing more this pump can do.
return;
}
// The watch died — most likely a subsequent accepted Position's
// merge-time Forget. The funnel owns what happens next, including
// this packet's still-unsent position event (retail acks whether
// or not the placement took — see SettlePending).
SettlePending(
pending.Record,
pending.Token,
pending.Route,
pending.PositionEventOwed);
return;
}
if (setPosition.IsPlacementCurrent(pending.Token))
{
_ = SubmitAndResolve(pending.Record, pending.Token, pending.Route);
return;
}
// The prepare-retry operation died the same way. (A re-issue retry
// marker also lands here, carrying PositionEventOwed: false — its
// packet's placement was never begun, so its ack belongs to the
// eventual re-issue's terminal outcome.)
SettlePending(
pending.Record,
pending.Token,
pending.Route,
pending.PositionEventOwed);
}
/// <summary>
/// Round 2 unified mechanism (2026-08-03) — the SINGLE terminal-outcome
/// funnel for <see cref="_pending"/>. Every branch that ends an operation
/// (committed, rejected, cancelled, watch died, key released) calls this
/// and nothing else touches the field. One decision input: the terminal
/// operation's own <c>PositionAuthorityVersion</c> (carried on its
/// <see cref="RuntimeEntityPlacementToken"/>,
/// <c>RuntimeSetPositionState.cs:50</c>) compared against the live
/// canonical record's CURRENT <c>PositionAuthorityVersion</c>:
///
/// <list type="bullet">
/// <item><description><b>Equal</b> — the canonical accepted authority has
/// not moved since this operation began, so nothing is outstanding. Clear
/// and do not re-issue. This is what makes one server correction produce
/// exactly ONE canonical placement and ONE outbound
/// <c>AutonomousPosition</c> even when a stale dead entry was still parked
/// in <see cref="_pending"/> when the fresh packet committed (N1 — the
/// double-apply/double-ack class <c>670f307c</c> deleted).</description></item>
/// <item><description><b>Advanced, newest accepted event still a
/// ForcePosition</b> — a newer force arrived while we were in flight and
/// could not begin (see the Contention status doc). Re-issue it,
/// re-classified from the CURRENT record via <see cref="_newestForce"/> —
/// never the terminal operation's own route (B1).</description></item>
/// <item><description><b>Advanced, newest accepted event is an ordinary
/// Apply</b> — clear and do not re-issue. The correction was superseded by
/// newer server truth and the ordinary route owns that pose. Re-issuing
/// here would apply the force route's <c>Teleport|Slide</c> flags to an
/// ordinary pose, send an ack retail never sends on that branch, and skip
/// the <c>ConstrainTo</c> the ordinary branch runs
/// (<c>RuntimeAuthoritativePositionRouteClassifier.cs:368-388</c>) — N2.
/// This is not a silent drop: retail applies each event as it arrives, and
/// a force overtaken by a newer position is moot.</description></item>
/// </list>
///
/// Recursion is bounded at one level: a re-issue always begins at the
/// record's CURRENT version, so its own terminal settle necessarily takes
/// the Equal branch (nothing can advance the authority between a begin and
/// its synchronous submit — only an inbound merge does, and inbound
/// dispatch is what called us).
///
/// <para><b>Round 3 (2026-08-03) — the terminal-without-commit ack.</b>
/// Retail acknowledges an accepted force packet whether or not its
/// placement took. <c>SmartBox::BlipPlayer</c> @0x00453940 calls
/// <c>CPhysicsObj::SetPositionSimple</c> @0x005162B0 — which returns an
/// <c>enum SetPositionError</c> that other retail call sites DO test
/// (<c>== OK_SPE</c> @0x0055605D, @0x00556021) — and DISCARDS it;
/// <c>BlipPlayer</c> itself returns <c>void</c>. Its caller
/// <c>SmartBox::HandleReceivedPosition</c> @0x00453FD0 then runs
/// <c>cmdinterp-&gt;SendPositionEvent()</c> @0x00454091 unconditionally and
/// returns @0x0045409D. So retail's semantics are: attempt the placement;
/// if it fails the body simply does not move; acknowledge regardless;
/// never retry. This funnel therefore sends the position event for any
/// terminal outcome whose placement was begun and did NOT commit
/// (<paramref name="positionEventOwed"/>), carrying the body's UNCHANGED
/// pose — which is exactly what retail's ack carries after a failed
/// <c>SetPositionSimple</c>, and is informative to the server: its force
/// did not take. It does NOT re-issue that correction, because retail
/// never retries. The commit paths pass <c>false</c>: their ack already
/// left through <see cref="ReconcileAndAcknowledge"/>, so exactly one
/// position event goes out per begun placement THAT REACHES ITS OWN
/// TERMINAL SETTLE — never two.</para>
///
/// <para>It is deliberately NOT "never zero per begun placement". This
/// method opens by nulling <c>_pending</c> without reading it, so a
/// descriptor still carrying <c>PositionEventOwed</c> is discarded when a
/// NEWER ForcePosition displaces it (the merge-time
/// <c>Forget</c> clears the block, the newer packet begins cleanly, and
/// its terminal settle nulls the field). That older packet's owed ack is
/// lost — AD-62 shape (v), pinned by
/// <c>OneServerCorrectionProducesExactlyOnePlacementAndOneAck</c>, which
/// feeds two force packets and asserts a single ack. Replaying it would be
/// worse: the message would carry a stale sequence against the newer
/// packet's committed pose. The displacing packet always acks, so ACE
/// always receives a report for the NEWEST force.</para>
///
/// <para>Divergence: the two non-reissuing branches mean a ForcePosition
/// retired without committing is never re-applied —
/// <c>docs/architecture/retail-divergence-register.md</c> row <b>AD-62</b>
/// (the park itself is our async collision-publication adaptation; retail
/// <c>SmartBox::BlipPlayer</c> @0x00453940 is synchronous against a fully
/// resident world and cannot reach this state). The ack is no longer part
/// of that loss for a begun placement; AD-62 names the narrower shapes
/// where the packet's placement was never begun at all and the ack is
/// still lost.</para>
/// </summary>
private void SettlePending(
RuntimeEntityRecord terminalRecord,
in RuntimeEntityPlacementToken terminalToken,
in RuntimeAuthoritativePositionRoute terminalRoute,
bool positionEventOwed)
{
_pending = null;
if (!_entityObjects.Entities.TryGetActive(
terminalRecord.ServerGuid, out RuntimeEntityRecord record)
|| record.ServerGuid != _localPlayerServerGuid()
|| record.PhysicsBody is null
|| record.Key is not { } key
|| key != terminalToken.Entity)
{
// The entity departed the world, is no longer the local player,
// has no canonical body, or released/replaced the incarnation this
// operation belonged to (the post-teardown key release). Nothing
// left in the world for this operation to place — and nothing left
// to acknowledge a position for either.
return;
}
// Retail order: the packet's own position event first (@0x00454091),
// THEN whatever the next accepted force is owed. No reconciliation
// runs here — the body did not move, so there is no committed frame to
// reconcile; only the ack is owed.
if (positionEventOwed && _localController() is { } terminalController)
SendPositionEvent(terminalController, terminalRoute);
ulong current = record.PositionAuthorityVersion;
if (terminalToken.PositionAuthorityVersion == current)
return;
if (_newestForce is not { } newest
|| newest.Entity != key
|| newest.PositionAuthorityVersion != current)
{
return;
}
RuntimeSetPositionState setPosition = _entityObjects.Physics.SetPosition;
RuntimeEntityPlacementToken token =
setPosition.TryBeginExclusiveAuthoredPlacement(
record,
current,
newest.Route.OperationKind);
if (!token.IsValid)
{
// Could not even begin the re-issue (a concurrent placement
// authority still owns the entity). The correction is still owed,
// so keep the terminal descriptor as the retry marker: the next
// Advance() pump re-enters this funnel and re-evaluates the SAME
// decision against the record as it then stands.
//
// PositionEventOwed: false — the marker stands for the NEWEST
// accepted force, whose placement was never begun. Its ack belongs
// to that re-issue's own terminal outcome, so re-entering this
// funnel through the marker must never fire a second position
// event for a packet the re-issue will ack itself.
_pending = new Pending
{
Record = terminalRecord,
Token = terminalToken,
Route = terminalRoute,
AwaitingCommitWake = false,
PositionEventOwed = false,
};
return;
}
_ = SubmitAndResolve(record, token, newest.Route);
}
private RuntimeAcceptedPositionExecutionStatus SubmitAndResolve(
RuntimeEntityRecord record,
in RuntimeEntityPlacementToken token,
in RuntimeAuthoritativePositionRoute route)
{
RuntimeSetPositionState setPosition = _entityObjects.Physics.SetPosition;
RuntimeSetPositionMoverPreparationStatus status =
setPosition.TryPrepareAndSubmitAuthoredPlacement(
record,
token,
route.OperationKind,
route.SetPositionFlags,
_collisionSource,
_clock.SimulationTimeSeconds,
out RuntimeSetPositionOutcome outcome,
resolveWorldOffsetFromRuntimeFrame: true);
if (status != RuntimeSetPositionMoverPreparationStatus.Prepared)
{
if (status.IsRetryable())
{
RetainPending(setPosition, new Pending
{
Record = record,
Token = token,
Route = route,
AwaitingCommitWake = false,
PositionEventOwed = true,
});
return RuntimeAcceptedPositionExecutionStatus.Contention;
}
CancelToken(setPosition, token);
SettlePending(record, token, route, positionEventOwed: true);
return RuntimeAcceptedPositionExecutionStatus.Rejected;
}
switch (outcome.Status)
{
case RuntimeSetPositionStatus.CommittedHostAcknowledgementPending:
// R4 review fix (2026-08-03): unlike
// RuntimeFirstEntryDriveController.TryCompleteContinuationPlacement
// (whose ack here is benign because ITS residence is what
// just drained, so the sink's residence gate can no longer
// decline), this route's sink can legitimately decline this
// exact receipt for real production reasons unrelated to
// anything this route does (!IsLoaded(landblock), stale
// transit authority — RuntimePlacementProjectionSubscription
// deliberately leaves a declined Place at the FIFO head for
// its OWN later retry). Acknowledging it here would consume
// and destroy that retry with no second writer to cover the
// render-facing projection (the generic App tail is skipped
// for this route). So this route does NOT acknowledge the
// projection itself — the production subscription already
// did, synchronously, inside the SetPosition call above, if
// it was going to; if it declined, its own retry contract
// owns the receipt from here, exactly like every other
// placement kind.
ReconcileAndAcknowledge(record, route);
SettlePending(record, token, route, positionEventOwed: false);
return RuntimeAcceptedPositionExecutionStatus.Committed;
case RuntimeSetPositionStatus.DeferredCell:
// Parked with a published Withdraw; consume it if it is
// already the head so the collision-generation wake can
// resubmit (RuntimeFirstEntryDriveController
// .TryCompleteContinuationPlacement:351-365's exact pattern).
while (setPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot parked)
&& parked.Token.Entity == token.Entity
&& parked.Kind is RuntimePlacementProjectionKind.Withdraw)
{
if (!setPosition.AcknowledgeProjection(parked.Token))
break;
}
if (!setPosition.WatchPlacementCompletion(token))
{
// Something displaced the operation between Submit and
// here — no cross-pump tracking is possible; cancel
// rather than leak a watch we can never resolve.
CancelToken(setPosition, token);
SettlePending(record, token, route, positionEventOwed: true);
return RuntimeAcceptedPositionExecutionStatus.Rejected;
}
RetainPending(setPosition, new Pending
{
Record = record,
Token = token,
Route = route,
AwaitingCommitWake = true,
PositionEventOwed = true,
});
return RuntimeAcceptedPositionExecutionStatus.DeferredCell;
default:
// Rejected/Cancelled — authority moved out from under this
// operation, so the body never moved. Retail still
// acknowledges the packet (SettlePending's positionEventOwed
// path); it just does not re-apply it.
CancelToken(setPosition, token);
SettlePending(record, token, route, positionEventOwed: true);
return RuntimeAcceptedPositionExecutionStatus.Rejected;
}
}
/// <summary>
/// R6 review fix (2026-08-03): the previous shape assigned
/// <see cref="_pending"/> unconditionally, which could silently
/// overwrite a still-tracked live pending (losing the only reference
/// able to later consume its eventual acknowledged completion — a
/// permanent <c>HasRetainedCompletion</c> orphan that would block every
/// future Begin for this entity with Contention forever). Given
/// <see cref="RuntimeSetPositionState.TryBeginExclusiveAuthoredPlacement"/>
/// already refuses to Begin while the entity holds an active operation
/// or an unconsumed acknowledged completion, <paramref name="next"/>'s
/// token could only have successfully begun if any DIFFERENT existing
/// <see cref="_pending"/> is already dead — so this is a defensive
/// invariant check, not a routine code path.
///
/// Round 2 (2026-08-03): this is the ONLY "the operation is still
/// outstanding" writer of <see cref="_pending"/>; every terminal outcome
/// goes through <see cref="SettlePending"/> instead.
/// </summary>
private void RetainPending(
RuntimeSetPositionState setPosition,
Pending next)
{
if (_pending is { } existing
&& existing.Token != next.Token
&& setPosition.IsPlacementCurrent(existing.Token))
{
throw new InvalidOperationException(
"RuntimeAcceptedPositionDriveController tracks at most one "
+ "pending accepted-position operation (the local player); "
+ "a still-live pending operation must never be silently "
+ "overwritten by a new one.");
}
_pending = next;
}
private static void CancelToken(
RuntimeSetPositionState setPosition,
in RuntimeEntityPlacementToken token)
{
RuntimePlacementCancellationReceipt cancellation =
setPosition.ForgetExactPlacement(token);
if (cancellation.IsValid)
setPosition.PublishCancellation(cancellation);
}
/// <summary>
/// §4a/§4b: the controller-local reconciliation
/// <c>CommitCanonical</c> does not perform, followed by the outbound ack
/// — an OUTPUT of the committed route, never a step performed alongside
/// it (retail <c>cmdinterp-&gt;SendPositionEvent()</c> @0x00454091 runs
/// after <c>SmartBox::BlipPlayer</c> @0x00454074 returns).
/// </summary>
private void ReconcileAndAcknowledge(
RuntimeEntityRecord record,
in RuntimeAuthoritativePositionRoute route)
{
if (record.ServerGuid != _localPlayerServerGuid())
return;
if (_localController() is not { } controller)
return;
controller.CommitCanonicalForcePositionFrame();
SendPositionEvent(controller, route);
}
/// <summary>
/// Retail <c>cmdinterp-&gt;SendPositionEvent()</c> @0x00454091 — the sole
/// outbound-ack site for this route, shared by the committed path
/// (<see cref="ReconcileAndAcknowledge"/>, which reconciles the moved
/// frame first) and the terminal-without-commit path
/// (<see cref="SettlePending"/>, which has no moved frame to reconcile and
/// so sends the body's unchanged pose).
///
/// The <c>CanSendPositionEvent</c> admission inside
/// <see cref="LocalPlayerOutboundController.SendImmediatePosition"/> is
/// retail's own (<c>CommandInterpreter::SendPositionEvent</c> @0x006B4770
/// tests the transient-state contact bits), so a legitimately airborne
/// body still suppresses the send on BOTH paths — that suppression is
/// retail behaviour, not a divergence.
/// </summary>
private void SendPositionEvent(
PlayerMovementController controller,
in RuntimeAuthoritativePositionRoute route)
{
if (!route.SendPositionImmediately)
return;
_localPlayerOutbound.SendImmediatePosition(_session(), controller);
}
private RuntimeAuthoritativePositionRoute ClassifyForcePosition(
RuntimeEntityRecord record,
RuntimeEntityKey key,
in WorldSession.EntityPositionUpdate update,
PositionTimestampDisposition disposition,
in AcceptedPhysicsTimestamps timestamps,
ushort previousTeleportSequence)
{
var authority = new RuntimeAuthoritativePositionAuthority(
_generation(),
key,
record.PositionAuthorityVersion,
update.PositionSequence,
previousTeleportSequence,
timestamps.Teleport,
disposition);
// Round 3 A3 (mirrored from RuntimeInitialCreateContinuationExecutor
// .ApplyPositionAction): contact is SOLELY the retained wire
// packet's own IsGrounded bit — never a live body query.
bool hasContact = update.IsGrounded;
bool hasAnimations = (record.Snapshot.MotionTableId
?? record.Snapshot.Physics?.MotionTableId) is { } motionTableId
&& motionTableId != 0u;
bool usePositionFromServer = _usePositionFromServer();
float playerDistance = 0f;
if (_localController() is { } controllerForDistance
&& (record.Snapshot.Physics?.Position
?? record.Snapshot.Position) is { } acceptedForDistance)
{
var target = new Vector3(
acceptedForDistance.PositionX,
acceptedForDistance.PositionY,
acceptedForDistance.PositionZ);
playerDistance = Vector3.Distance(
target, controllerForDistance.Position);
}
var request = new RuntimeAcceptedPositionRouteRequest(
authority,
RuntimePositionEntityKind.LocalPlayer,
RuntimeAcceptedPositionSource.PositionEvent,
update.Position,
update.PlacementId,
update.Velocity,
record.FullCellId,
hasContact,
playerDistance,
usePositionFromServer,
hasAnimations,
new RuntimePositionPlacementFacts(
record.FinalPhysicsState,
record.Snapshot.SetupTableId is not null));
return RuntimeAuthoritativePositionRouteClassifier
.ClassifyAcceptedPosition(request);
}
}

View file

@ -19,6 +19,24 @@ public interface IRuntimeDirectWorldProjection
bool isLocalPlayer,
PositionTimestampDisposition disposition);
/// <summary>
/// R2 review fix (2026-08-03): a ForcePosition on the local player is
/// dispatched directly to <see cref="RuntimeAcceptedPositionDriveController"/>
/// and never reaches <see cref="ProjectPosition"/> at all, so THIS is
/// where a host that keeps a narrow collision/streaming window (the
/// deleted <c>HeadlessSessionWorldProjection.BlipLocalPlayer</c>'s own
/// <c>_collision.CenterOn</c> call) re-centers on the destination BEFORE
/// the drive controller submits — establishing that the destination's
/// collision generation is one this host's window can ever publish is a
/// precondition for a <c>DeferredCell</c> park to be a real park rather
/// than a dead end (see <c>RuntimeAcceptedPositionDriveController.Advance</c>'s
/// R1 doc comment). A host with no narrow window (the graphical host,
/// whose landblock streaming already follows the accepted position via
/// <c>LiveEntityInboundAuthorityGate.ObserveAcceptedLocalPosition</c>) is
/// a no-op here.
/// </summary>
void CenterOnAcceptedForcePosition(RuntimeEntityRecord record);
void BeginTeleport();
RuntimeDestinationReadiness PrepareDestination(
@ -38,20 +56,28 @@ public sealed class RuntimeLiveEntitySessionController
private readonly WorldSession _session;
private readonly Action<string> _log;
private readonly IRuntimeDirectWorldProjection? _worldProjection;
private readonly LocalPlayerOutboundController _localPlayerOutbound =
new((_, _, _, _, _, _) => { });
/// <summary>
/// C4 route 2 (2026-08-03): the headless accepted-Position drive
/// controller. Owns its own outbound-ack collaborator internally; the
/// ForcePosition + manual <c>LocalPlayerOutboundController.SendImmediatePosition</c>
/// pair this class used to drive directly is retired (the deleted
/// <c>HeadlessSessionWorldProjection.BlipLocalPlayer</c>).
/// </summary>
private readonly RuntimeAcceptedPositionDriveController? _acceptedPositionDrive;
private bool _initialLoginCompleteSent;
public RuntimeLiveEntitySessionController(
GameRuntime runtime,
WorldSession session,
Action<string>? log = null,
IRuntimeDirectWorldProjection? worldProjection = null)
IRuntimeDirectWorldProjection? worldProjection = null,
RuntimeAcceptedPositionDriveController? acceptedPositionDrive = null)
{
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
_session = session ?? throw new ArgumentNullException(nameof(session));
_log = log ?? (_ => { });
_worldProjection = worldProjection;
_acceptedPositionDrive = acceptedPositionDrive;
}
public LiveEntitySessionSink CreateSink() => new(
@ -218,16 +244,55 @@ public sealed class RuntimeLiveEntitySessionController
update.Guid,
out RuntimeEntityRecord record))
{
_worldProjection?.ProjectPosition(
record,
isLocalPlayer: true,
disposition);
}
if (disposition is PositionTimestampDisposition.ForcePosition)
{
_localPlayerOutbound.SendImmediatePosition(
_session,
_runtime.MovementOwner.Controller);
if (disposition is PositionTimestampDisposition.ForcePosition)
{
// R2 review fix (2026-08-03): re-center BEFORE submitting —
// see IRuntimeDirectWorldProjection.CenterOnAcceptedForcePosition's
// doc comment. This is what the deleted BlipLocalPlayer's own
// _collision.CenterOn call used to guarantee.
_worldProjection?.CenterOnAcceptedForcePosition(record);
// C4 route 2 (2026-08-03): the Runtime-owned accepted-
// Position execution seam replaces the deleted
// HeadlessSessionWorldProjection.BlipLocalPlayer + manual
// SendImmediatePosition pair. Canonical commit, controller
// reconciliation, and the outbound ack (an OUTPUT of the
// committed route, not a step alongside it) all run inside
// the call below.
RuntimeAcceptedPositionExecutionStatus forceStatus =
_acceptedPositionDrive?.TryExecuteAcceptedLocalPosition(
record,
update,
disposition,
timestamps,
timestamps.PreviousTeleport)
?? RuntimeAcceptedPositionExecutionStatus.NotApplicable;
if (forceStatus is RuntimeAcceptedPositionExecutionStatus
.NotApplicable)
{
// R3 review fix (2026-08-03): NotApplicable (e.g. an
// initial-Create residence still owns this record —
// route 1's job, or the login-window controller-null
// branch before route 1 has even published a
// controller) is NOT the terminal case the previous
// comment here claimed. ProjectPosition's own
// controller-null branch is the pre-existing legacy
// fallback this disposition always had — it must still
// run, exactly as every other disposition's fallback
// does below.
_worldProjection?.ProjectPosition(
record,
isLocalPlayer: true,
disposition);
}
}
else
{
_worldProjection?.ProjectPosition(
record,
isLocalPlayer: true,
disposition);
}
}
TryCompletePortal();
}