Member-wise deletion of the three legacy resolver members named in docs/research/2026-08-05-c5a-contract.md: PhysicsEngine.Resolve, PhysicsEngine.HasCellSurface, and PhysicsEngine.ResolvePlacement. An exhaustive receiver census over src/ found zero production callers of any of the three — every production placement writer already reaches the canonical PhysicsEngine.SetPosition transaction exclusively through RuntimeSetPositionState (three call sites total). The deletion is purely member-wise: IsSpawnCellReady and AdjustPosition, which shared the same source region as the deleted members, are preserved byte-identical — every remaining production caller of either (including PhysicsCameraCollisionProbe, AdjustPosition's sole surviving production caller) is unaffected. Companion changes: - PlayerMovementController's 3-argument SetPosition test overload is renamed to SeedPlacementForTest (internal) and CommitPreparedPosition is deleted; 83 call sites across 19 test files were mechanically renamed to match. - Seven pinned test dispositions from the contract are executed: 3.1 (PhysicsEngineTests.cs: 11 legacy-resolver tests deleted, 6 ResolveWithTransition tests kept), 3.2/3.3/3.4 (re-point to canonical SetPosition, with TransitionScratchDifferentialTests.cs additionally gaining positive IsCommitted assertions after each bitwise comparison so the differential proves a placement actually committed, not just that two possibly-uncommitted results match), 3.5 (Runtime rename), and 3.6 (PlayerMovementPlacementTransactionTests.cs rewritten — its xmldoc now states plainly that the render-root publish moved to RuntimeSetPositionState.cs, but the sticky-release relocation claim was false and is retracted; this disposition's coverage loss is the sticky release path, not silently absorbed elsewhere). - Stale `PhysicsEngine.Resolve`/`Resolve` doc citations in CellTransit.cs, PlayerMovementController.cs, and HeadlessSessionWorldProjection.cs are corrected to name the surviving canonical entry points by symbol (SetPosition, AdjustSetPosition/AdjustPosition, ResolveWithTransition) rather than fragile line numbers. Retires AP-1 and AD-1 in docs/architecture/retail-divergence-register.md: both rows described production zero-delta placement routing remaining on the legacy resolver pending the Slice 4B2/4B route cutover; that resolver no longer exists, so the condition each row tracked is now structurally false rather than merely narrowed. AP-145 (routed through the prior commit) and this commit's AP-1/AD-1 together bring the section counts to 101 AP / 47 AD active rows. Builds on the AP-145 fix (previous commit) — this commit's staged tree was independently rebuilt and its four suites independently rerun on top of that commit before this commit was created, in addition to the combined rebuild/rerun below. Full-solution build: 0 errors (21 pre-existing warnings, all unrelated). Suite results (combined tree): Core 4270/4271 passed (1 skip; the single DatSoundCacheTests concurrent-decode-dedup failure is a known load-sensitive race, confirmed passing standalone and unrelated to this change), Runtime 1176/1176, Headless 86/86, App 4132/4135 (3 skips). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1547 lines
77 KiB
C#
1547 lines
77 KiB
C#
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 operation parked instead of committing. The ack fires later, once
|
|
/// <see cref="RuntimeAcceptedPositionDriveController.Advance"/> observes
|
|
/// the deferred commit.
|
|
///
|
|
/// Round 4 (2026-08-04, N3): the previous text named only one cause —
|
|
/// "the destination landblock's collision generation was not ready" —
|
|
/// which is <c>SubmitPreparedPlacementCore</c>'s plain
|
|
/// <c>result.IsDeferred</c> park. Two other causes reach this same
|
|
/// status and are asserted by this route's own tests: the PRE-engine
|
|
/// collision-prefix quiescence park (the destination prefix is
|
|
/// mid-retirement) and the POST-sweep one (a landblock the sweep merely
|
|
/// TOUCHED is mid-retirement, which can rewrite an otherwise
|
|
/// about-to-commit placement). All three are "parked, not committed";
|
|
/// none of them means the placement failed.
|
|
/// </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(&dest, heading); // 00454068 preserve OUR heading
|
|
/// SmartBox::BlipPlayer(this, &dest); // 00454074
|
|
/// player->update_times[0] = arg7; // 00454079 stamp POSITION_TS
|
|
/// cmdinterp->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).
|
|
/// This no-re-arm rule is scoped to FORCE_POSITION only — C4 route 3's
|
|
/// portal arm (<see cref="TryExecuteAcceptedPortalArrival"/>) DOES re-arm,
|
|
/// because retail's local TELEPORT branch of the same function reaches
|
|
/// <c>ConstrainTo</c> @0x0045418A; see
|
|
/// docs/research/2026-08-04-c4-route-3-contract.md §2 Inversion A.
|
|
///
|
|
/// 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.
|
|
///
|
|
/// C4 route 3: always <c>false</c> for a portal pending — the
|
|
/// portal route's <see cref="RuntimeAuthoritativePositionRoute.SendPositionImmediately"/>
|
|
/// is always <c>false</c> (retail's teleport branch never sends
|
|
/// <c>AutonomousPosition</c>), so there is never an owed position
|
|
/// event to carry.
|
|
/// </summary>
|
|
internal required bool PositionEventOwed { get; init; }
|
|
|
|
/// <summary>
|
|
/// C4 route 3: <see cref="RuntimePortalPlacementAuthority.Present"/>
|
|
/// when this descriptor is the trap T7 (2026-08-04 contract) portal
|
|
/// arm's own DeferredCell park — a SIBLING use of this same
|
|
/// retained-operation machinery, not a repurposed force pending.
|
|
/// <see cref="Advance"/> and <see cref="SubmitAndResolvePortal"/> are
|
|
/// the only writers/readers that branch on it; the force funnel
|
|
/// (<see cref="SettlePending"/>, <see cref="_newestForce"/>) never
|
|
/// sees or produces a portal pending.
|
|
/// </summary>
|
|
internal RuntimePortalPlacementAuthority Portal { 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, all in <c>RuntimeEntityObjectLifetime</c> (C5a,
|
|
/// 2026-08-05 — cite by symbol, not line: these move) — the ordinary
|
|
/// accepted-Position merge plus <c>TryApplyPickup</c>,
|
|
/// <c>CommitPositionChannelUpdate</c>, and
|
|
/// <c>AdvanceCreateAuthority</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>
|
|
/// C4 route 3: the D-T3 <c>PlayerTeleported</c> port
|
|
/// (<c>CommandInterpreter::PlayerTeleported</c> @0x006B32B0 =
|
|
/// <c>SetAutoRun(0,1)</c> + <c>SendMovementEvent</c>) needs the J5.4
|
|
/// autorun latch owner, which lives one level above
|
|
/// <see cref="PlayerMovementController"/> and is not reachable from
|
|
/// <see cref="_localController"/>. Late-bound like every other
|
|
/// dependency here so this controller does not need to outlive a
|
|
/// specific movement-owner instance across a reconnect.
|
|
/// </summary>
|
|
private readonly Func<RuntimeLocalPlayerMovementState?> _localMovementState;
|
|
|
|
/// <summary>
|
|
/// A2 review fix (2026-08-05, D-T2.4): re-validates a retained portal
|
|
/// authority against the transit owner's CURRENT reveal before either
|
|
/// wake site in <see cref="Advance"/> acts on it. A <c>DeferredCell</c>
|
|
/// park commits asynchronously (<c>RuntimeSetPositionState.RetryDeferred</c>,
|
|
/// driven entirely by an unrelated collision-generation wake) — nothing
|
|
/// in this class can prevent that body-level commit once it starts. What
|
|
/// this predicate CAN prevent is running the reconcile/ack suffix (or a
|
|
/// stale resubmission) against a reveal that ended or was superseded
|
|
/// while the park was outstanding — exactly the D-T2.4 requirement this
|
|
/// slice's first pass never implemented (architecture review A2). Wired
|
|
/// by each host composition to
|
|
/// <c>RuntimeWorldTransitState.CanPlacePortalDestination</c> (the SAME
|
|
/// idempotent query <see cref="TryExecuteAcceptedPortalArrival"/>'s
|
|
/// caller already uses at the Place edge); left <see langword="null"/>
|
|
/// by fixtures that do not exercise the DeferredCell wake, in which case
|
|
/// every retained portal pending is treated as still current (today's
|
|
/// unconditional behaviour, preserved for callers that never park).
|
|
/// </summary>
|
|
private readonly Func<RuntimePortalPlacementAuthority, bool>?
|
|
_isPortalAuthorityCurrent;
|
|
|
|
/// <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,
|
|
Func<RuntimeLocalPlayerMovementState?>? localMovementState = null,
|
|
Func<RuntimePortalPlacementAuthority, bool>? isPortalAuthorityCurrent = null)
|
|
{
|
|
_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));
|
|
_localMovementState = localMovementState ?? (static () => null);
|
|
_isPortalAuthorityCurrent = isPortalAuthorityCurrent;
|
|
_entityObjects.RegisterAcceptedPositionDriveOwnership(
|
|
() => _pending is null ? 0 : 1);
|
|
}
|
|
|
|
internal int PendingCount => _pending is null ? 0 : 1;
|
|
|
|
/// <summary>
|
|
/// B1 review fix (2026-08-05): the drive's own record of the LAST portal
|
|
/// authority <see cref="ReconcileAndAcknowledgePortal"/> actually
|
|
/// committed — set only there, so this is never an inference. Both host
|
|
/// gates were latching "committed" from <see cref="PendingCount"/>
|
|
/// reaching zero, but that global (force-arm-shared) slot ALSO clears on
|
|
/// three non-committing paths (a merge-time <c>Forget</c> — the drive's
|
|
/// own doc names this the EXPECTED outcome of a park surviving one ACE
|
|
/// broadcast interval — and both of A2's new abandon branches), so
|
|
/// "not pending" never implied "this specific reveal placed". Consumed
|
|
/// exactly once per commit via <see cref="TryConsumePortalCommit"/>.
|
|
/// </summary>
|
|
private (long RevealGeneration, ushort TeleportSequence)? _lastCommittedPortal;
|
|
|
|
/// <summary>
|
|
/// B1 review fix: the host gate's ONLY correct way to learn "did MY
|
|
/// specific reveal commit" — never infer it from
|
|
/// <see cref="PendingCount"/>. Returns <see langword="true"/> and
|
|
/// consumes the fact exactly once when the drive's last portal commit
|
|
/// matches the caller's own (revealGeneration, teleportSequence); a
|
|
/// mismatch (nothing committed yet, a DIFFERENT reveal committed, or
|
|
/// this generation's park was abandoned/forgotten instead) returns
|
|
/// <see langword="false"/> without side effects, so the caller keeps
|
|
/// retrying or falls through to a fresh attempt.
|
|
/// </summary>
|
|
internal bool TryConsumePortalCommit(
|
|
long revealGeneration,
|
|
ushort teleportSequence)
|
|
{
|
|
if (_lastCommittedPortal is not { } committed
|
|
|| committed.RevealGeneration != revealGeneration
|
|
|| committed.TeleportSequence != teleportSequence)
|
|
{
|
|
return false;
|
|
}
|
|
_lastCommittedPortal = null;
|
|
return true;
|
|
}
|
|
|
|
/// <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.
|
|
/// <para>
|
|
/// Coordinator hygiene fix (round-3 closeout, 2026-08-05): also clears
|
|
/// <see cref="_lastCommittedPortal"/>. An unconsumed latch surviving a
|
|
/// session reset was harmless only because the transit's own
|
|
/// generation counter is monotonic across resets within one
|
|
/// <c>GameRuntime</c> lifetime, so a stale entry could never match a
|
|
/// later reveal's generation/sequence pair by construction — a
|
|
/// correctness argument resting on an invariant this method never
|
|
/// declared. Clearing it here lets the ledger converge to zero on
|
|
/// every reset instead of relying on that invariant to stay true.
|
|
/// </para>
|
|
/// </summary>
|
|
private void AbandonPending()
|
|
{
|
|
_newestForce = null;
|
|
_lastCommittedPortal = null;
|
|
if (_pending is not { } pending)
|
|
return;
|
|
_pending = null;
|
|
RuntimeSetPositionState setPosition = _entityObjects.Physics.SetPosition;
|
|
setPosition.ForgetPlacementCompletion(pending.Token);
|
|
// Cancellation, not withdrawal: the placement intent is abandoned but
|
|
// the local player stays in the world, so a DeferredCell park must be
|
|
// rolled back rather than left stranding the body.
|
|
RuntimePlacementCancellationReceipt cancellation =
|
|
setPosition.ForgetExactPlacement(
|
|
pending.Token,
|
|
restoreCancelledPark: true);
|
|
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>
|
|
/// C4 route 3: executes the local player's portal arrival against the
|
|
/// canonical Runtime SetPosition owner. Retail
|
|
/// <c>SmartBox::TeleportPlayer</c> @0x00453910 =
|
|
/// <c>CPhysicsObj::SetPositionSimple(player, dest, 1)</c> — the SAME
|
|
/// generic primitive route 2 already routes through
|
|
/// <see cref="TryExecuteAcceptedLocalPosition"/> — plus
|
|
/// <c>PlayerPositionUpdated</c>. <paramref name="destination"/> must be
|
|
/// the transit's OWN retained accepted destination
|
|
/// (<c>RuntimeWorldTransitState.TryGetAcceptedTeleportDestination</c>),
|
|
/// never re-derived from live per-tick timestamps: by the time the Place
|
|
/// edge fires, the packet merged seconds ago and nothing is "advancing"
|
|
/// anymore (docs/research/2026-08-04-c4-route-3-contract.md D-T2.2).
|
|
/// </summary>
|
|
internal RuntimeAcceptedPositionExecutionStatus TryExecuteAcceptedPortalArrival(
|
|
in RuntimeTeleportDestination destination,
|
|
in RuntimePortalPlacementAuthority portal)
|
|
{
|
|
if (!portal.IsValid
|
|
|| !_entityObjects.Entities.TryGetActive(
|
|
_localPlayerServerGuid(), out RuntimeEntityRecord record)
|
|
|| record.PhysicsBody is null
|
|
|| record.Key is not { } key
|
|
// Route 1 owns an active initial-Create residence exactly like
|
|
// route 2's equivalent guard above — the residence executor's
|
|
// own tail action already carries any position it needs.
|
|
|| _entityObjects.TryGetInitialCreateResidence(record, out _))
|
|
{
|
|
LogPortalArrivalAttempt(
|
|
RuntimeAcceptedPositionExecutionStatus.NotApplicable,
|
|
portal,
|
|
resolvedCell: 0u);
|
|
return RuntimeAcceptedPositionExecutionStatus.NotApplicable;
|
|
}
|
|
|
|
RuntimeAuthoritativePositionRoute route = ClassifyPortalArrival(
|
|
record, key, destination, _generation());
|
|
if (!route.Accepted)
|
|
{
|
|
LogPortalArrivalAttempt(
|
|
RuntimeAcceptedPositionExecutionStatus.Rejected,
|
|
portal,
|
|
record.FullCellId);
|
|
return RuntimeAcceptedPositionExecutionStatus.Rejected;
|
|
}
|
|
|
|
RuntimeSetPositionState setPosition = _entityObjects.Physics.SetPosition;
|
|
ulong acceptedVersion = record.PositionAuthorityVersion;
|
|
RuntimeEntityPlacementToken token =
|
|
setPosition.TryBeginExclusiveAuthoredPlacement(
|
|
record,
|
|
acceptedVersion,
|
|
route.OperationKind,
|
|
portal);
|
|
if (!token.IsValid)
|
|
{
|
|
// Either a concurrent placement authority already owns the
|
|
// entity, or Begin's own portal-vs-latest-cell gate refused
|
|
// (D-T5's Begin cell-mismatch edge — a second local Position
|
|
// merged between the offer and this Place edge). Neither is
|
|
// staleness; the caller's D-T5 refusal handling owns what
|
|
// happens next.
|
|
LogPortalArrivalAttempt(
|
|
RuntimeAcceptedPositionExecutionStatus.Contention,
|
|
portal,
|
|
record.FullCellId);
|
|
return RuntimeAcceptedPositionExecutionStatus.Contention;
|
|
}
|
|
|
|
return SubmitAndResolvePortal(record, token, route, portal);
|
|
}
|
|
|
|
/// <summary>
|
|
/// C4 route 3: the classifier's LocalPlayer-teleport route
|
|
/// (<see cref="RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition"/>,
|
|
/// <c>request.Authority.TeleportAdvanced</c> branch) built from the
|
|
/// retained destination rather than a live merge. Retail's
|
|
/// <c>PhysicsTimestampGate.IsNewer(PreviousTeleportSequence,
|
|
/// AcceptedTeleportSequence)</c> gate only needs to be TRUE — its exact
|
|
/// magnitude is not read anywhere past that boolean (the classifier's
|
|
/// resulting <see cref="RuntimeAuthoritativePositionRoute"/> for this
|
|
/// branch does not depend on the previous stamp's value, and the drive
|
|
/// controller's own `expectedPositionAuthorityVersion` — not this
|
|
/// authority's — gates Begin), so a synthetic strictly-older sequence
|
|
/// forces retail's exact branch without any second copy of the merge-time
|
|
/// timestamp pair having to survive from offer to Place.
|
|
/// </summary>
|
|
private static RuntimeAuthoritativePositionRoute ClassifyPortalArrival(
|
|
RuntimeEntityRecord record,
|
|
RuntimeEntityKey key,
|
|
in RuntimeTeleportDestination destination,
|
|
RuntimeGenerationToken generation)
|
|
{
|
|
ushort acceptedTeleport = destination.TeleportSequence;
|
|
ushort priorTeleport = unchecked((ushort)(acceptedTeleport - 1));
|
|
var authority = new RuntimeAuthoritativePositionAuthority(
|
|
generation,
|
|
key,
|
|
record.PositionAuthorityVersion,
|
|
destination.PositionSequence,
|
|
priorTeleport,
|
|
acceptedTeleport,
|
|
PositionTimestampDisposition.Apply);
|
|
|
|
bool hasAnimations = (record.Snapshot.MotionTableId
|
|
?? record.Snapshot.Physics?.MotionTableId) is { } motionTableId
|
|
&& motionTableId != 0u;
|
|
|
|
var wirePosition = new CreateObject.ServerPosition(
|
|
destination.CellId,
|
|
destination.Position.Frame.Origin.X,
|
|
destination.Position.Frame.Origin.Y,
|
|
destination.Position.Frame.Origin.Z,
|
|
destination.Position.Frame.Orientation.W,
|
|
destination.Position.Frame.Orientation.X,
|
|
destination.Position.Frame.Orientation.Y,
|
|
destination.Position.Frame.Orientation.Z);
|
|
|
|
var request = new RuntimeAcceptedPositionRouteRequest(
|
|
authority,
|
|
RuntimePositionEntityKind.LocalPlayer,
|
|
RuntimeAcceptedPositionSource.PositionEvent,
|
|
wirePosition,
|
|
PlacementFrame: null,
|
|
PositionPackVelocity: null,
|
|
CommittedCellId: record.FullCellId,
|
|
HasContact: false,
|
|
PlayerDistance: 0f,
|
|
UsePositionFromServer: false,
|
|
hasAnimations,
|
|
new RuntimePositionPlacementFacts(
|
|
record.FinalPhysicsState,
|
|
record.Snapshot.SetupTableId is not null));
|
|
|
|
return RuntimeAuthoritativePositionRouteClassifier
|
|
.ClassifyAcceptedPosition(request);
|
|
}
|
|
|
|
/// <summary>
|
|
/// C4 route 3 (trap T7): the portal SIBLING of <see cref="SubmitAndResolve"/>
|
|
/// — shares Begin/Submit/status handling, deliberately does NOT touch
|
|
/// <see cref="_newestForce"/> or route through <see cref="SettlePending"/>'s
|
|
/// force-shaped re-issue funnel. ACE sends one destination per teleport;
|
|
/// a portal placement that fails to commit is never re-applied.
|
|
/// </summary>
|
|
private RuntimeAcceptedPositionExecutionStatus SubmitAndResolvePortal(
|
|
RuntimeEntityRecord record,
|
|
in RuntimeEntityPlacementToken token,
|
|
in RuntimeAuthoritativePositionRoute route,
|
|
in RuntimePortalPlacementAuthority portal)
|
|
{
|
|
RuntimeSetPositionState setPosition = _entityObjects.Physics.SetPosition;
|
|
RuntimeSetPositionMoverPreparationStatus status =
|
|
setPosition.TryPrepareAndSubmitAuthoredPlacement(
|
|
record,
|
|
token,
|
|
route.OperationKind,
|
|
route.SetPositionFlags,
|
|
_collisionSource,
|
|
_clock.SimulationTimeSeconds,
|
|
out RuntimeSetPositionOutcome outcome,
|
|
portal: portal,
|
|
resolveWorldOffsetFromRuntimeFrame: true);
|
|
|
|
if (status != RuntimeSetPositionMoverPreparationStatus.Prepared)
|
|
{
|
|
if (status.IsRetryable())
|
|
{
|
|
RetainPending(setPosition, new Pending
|
|
{
|
|
Record = record,
|
|
Token = token,
|
|
Route = route,
|
|
AwaitingCommitWake = false,
|
|
PositionEventOwed = false,
|
|
Portal = portal,
|
|
});
|
|
LogPortalArrivalAttempt(
|
|
RuntimeAcceptedPositionExecutionStatus.Contention,
|
|
portal,
|
|
record.FullCellId);
|
|
return RuntimeAcceptedPositionExecutionStatus.Contention;
|
|
}
|
|
|
|
CancelToken(setPosition, token);
|
|
LogPortalArrivalAttempt(
|
|
RuntimeAcceptedPositionExecutionStatus.Rejected,
|
|
portal,
|
|
record.FullCellId);
|
|
return RuntimeAcceptedPositionExecutionStatus.Rejected;
|
|
}
|
|
|
|
switch (outcome.Status)
|
|
{
|
|
case RuntimeSetPositionStatus.CommittedHostAcknowledgementPending:
|
|
ReconcileAndAcknowledgePortal(record, route, portal);
|
|
return RuntimeAcceptedPositionExecutionStatus.Committed;
|
|
|
|
case RuntimeSetPositionStatus.DeferredCell:
|
|
// D-T2.4: a park should be rare (the destination was already
|
|
// centered by the host before submit), but must never leak —
|
|
// same drain-stale-Withdraw-then-watch shape as the force arm.
|
|
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))
|
|
{
|
|
CancelToken(setPosition, token);
|
|
LogPortalArrivalAttempt(
|
|
RuntimeAcceptedPositionExecutionStatus.Rejected,
|
|
portal,
|
|
record.FullCellId);
|
|
return RuntimeAcceptedPositionExecutionStatus.Rejected;
|
|
}
|
|
RetainPending(setPosition, new Pending
|
|
{
|
|
Record = record,
|
|
Token = token,
|
|
Route = route,
|
|
AwaitingCommitWake = true,
|
|
PositionEventOwed = false,
|
|
Portal = portal,
|
|
});
|
|
LogPortalArrivalAttempt(
|
|
RuntimeAcceptedPositionExecutionStatus.DeferredCell,
|
|
portal,
|
|
record.FullCellId);
|
|
return RuntimeAcceptedPositionExecutionStatus.DeferredCell;
|
|
|
|
default:
|
|
// Rejected/Cancelled — authority moved out from under this
|
|
// operation; the body never moved. Unlike the force arm,
|
|
// retail's teleport branch has no unconditional ack to send,
|
|
// so there is nothing left to do here.
|
|
CancelToken(setPosition, token);
|
|
LogPortalArrivalAttempt(
|
|
RuntimeAcceptedPositionExecutionStatus.Rejected,
|
|
portal,
|
|
record.FullCellId);
|
|
return RuntimeAcceptedPositionExecutionStatus.Rejected;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// R8 review fix (2026-08-05): D-T8 specified "one line per
|
|
/// portal-arrival ATTEMPT", but the first pass logged only from
|
|
/// <see cref="ReconcileAndAcknowledgePortal"/> — reached solely on
|
|
/// <c>Committed</c> — so every refusal was invisible under the gate's
|
|
/// own pinned <c>ACDREAM_PROBE_LOCAL_TELEPORT</c> env var (the graphical
|
|
/// refusal path logged under the DIFFERENT <c>ACDREAM_PROBE_TELEPORT</c>,
|
|
/// and headless logged nothing at all). Every non-terminal/refusal exit
|
|
/// from <see cref="TryExecuteAcceptedPortalArrival"/> and
|
|
/// <see cref="SubmitAndResolvePortal"/> now emits through this one
|
|
/// helper; the richer hookTail/leash/autorun facts remain
|
|
/// <see cref="ReconcileAndAcknowledgePortal"/>'s own line on the
|
|
/// <c>Committed</c> path, since those three booleans are meaningless
|
|
/// before a commit.
|
|
/// </summary>
|
|
private static void LogPortalArrivalAttempt(
|
|
RuntimeAcceptedPositionExecutionStatus status,
|
|
in RuntimePortalPlacementAuthority portal,
|
|
uint resolvedCell)
|
|
{
|
|
PhysicsDiagnostics.LogLocalTeleportArrival(
|
|
cause: "portal",
|
|
placementStatus: status.ToString(),
|
|
portalGeneration: portal.RevealGeneration,
|
|
teleportSequence: portal.TeleportSequence,
|
|
destinationCell: portal.Projection.DestinationCell,
|
|
resolvedCell: resolvedCell,
|
|
hookTailRan: false,
|
|
leashArmed: false,
|
|
autorunCancelled: false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A2/D-T2.4 review fix (2026-08-05): treats a portal authority as
|
|
/// current when no re-validation predicate was wired (today's
|
|
/// unconditional behaviour, preserved for fixtures that never park), and
|
|
/// otherwise defers to <see cref="_isPortalAuthorityCurrent"/> — which
|
|
/// production wires to the SAME
|
|
/// <c>RuntimeWorldTransitState.CanPlacePortalDestination</c> query the
|
|
/// App/headless Place edge itself uses.
|
|
/// </summary>
|
|
private bool IsPortalAuthorityCurrent(
|
|
in RuntimePortalPlacementAuthority portal) =>
|
|
_isPortalAuthorityCurrent is null || _isPortalAuthorityCurrent(portal);
|
|
|
|
/// <summary>
|
|
/// C4 route 3: the committed-portal-placement controller-local
|
|
/// reconciliation and outbound tail. Runs
|
|
/// <see cref="PlayerMovementController.CommitCanonicalTeleportFrame"/>
|
|
/// (the re-homed <c>SetPositionCore</c> duties, D-T3), then the
|
|
/// <c>PlayerTeleported</c> port (<c>CommandInterpreter::PlayerTeleported</c>
|
|
/// @0x006B32B0 = <c>SetAutoRun(0,1)</c> + <c>SendMovementEvent</c>) — two
|
|
/// named behaviour changes versus the deleted App/Headless placement
|
|
/// authorities: autorun now cancels on portal arrival, and exactly one
|
|
/// movement-event refresh goes out (never an <c>AutonomousPosition</c> —
|
|
/// the route's <c>SendPositionImmediately</c> is always false).
|
|
///
|
|
/// <para>
|
|
/// A4 review fix (2026-08-05): <paramref name="route"/>'s
|
|
/// <c>ZeroVelocity</c>/<c>ConstrainPhase</c>/<c>TeleportHookPhase</c> are
|
|
/// now READ, not assumed — <see cref="RuntimeAuthoritativePositionRoute.RunsTeleportHook"/>
|
|
/// gates whether the hook tail runs at all, and its
|
|
/// <c>ZeroVelocity</c>/<see cref="RuntimeAuthoritativePositionRoute.ConstrainAfterRouting"/>
|
|
/// drive <c>CommitCanonicalTeleportFrame</c>'s two conditional duties.
|
|
/// The LocalPlayer-teleport branch's values are unchanged today
|
|
/// (<c>AfterPositionOperation</c>/<c>AfterPositionOperation</c>/<c>true</c>),
|
|
/// so this is purely a wiring correction: a future classifier edit now
|
|
/// changes this method's behaviour instead of silently disagreeing with
|
|
/// it, and the contract's own §8 item 11 sabotage (force the classifier
|
|
/// onto <c>ConstrainPhase.None</c> — the leash must not re-arm) can
|
|
/// finally fail as designed.
|
|
/// </para>
|
|
/// </summary>
|
|
private void ReconcileAndAcknowledgePortal(
|
|
RuntimeEntityRecord record,
|
|
in RuntimeAuthoritativePositionRoute route,
|
|
in RuntimePortalPlacementAuthority portal)
|
|
{
|
|
// B1 review fix (2026-08-05): this method is called ONLY from the
|
|
// two sites that just observed Runtime's canonical
|
|
// CommittedHostAcknowledgementPending outcome for THIS portal
|
|
// authority (SubmitAndResolvePortal's first-attempt commit and
|
|
// Advance's re-validated deferred wake) — so the commit fact is
|
|
// true here regardless of whether the two guards below decline the
|
|
// REST of this method's App-level suffix work. Latching it FIRST,
|
|
// unconditionally, is what lets TryConsumePortalCommit replace the
|
|
// unsound PendingCount==0 inference both host gates used to make.
|
|
_lastCommittedPortal = (portal.RevealGeneration, portal.TeleportSequence);
|
|
if (record.ServerGuid != _localPlayerServerGuid())
|
|
return;
|
|
if (_localController() is not { } controller)
|
|
return;
|
|
// N4 review fix (2026-08-05): the frame/cell/stop/input-reset/clock
|
|
// commit runs UNCONDITIONALLY (retail's SetPositionInternal
|
|
// @0x00515330 has no hook-phase gate); only the UnStick/UnConstrain/
|
|
// re-arm tail inside it is conditioned on the hook phase, via
|
|
// runTeleportHookTail. Previously this whole call was skipped when
|
|
// RunsTeleportHook was false, which would have silently dropped the
|
|
// render-root UpdateCellId publish too (the doorway-FLAP class) the
|
|
// day a route ever sets TeleportHookPhase.None — today's portal
|
|
// route always sets a non-None phase, so this is a structural fix
|
|
// with no live behavior change yet.
|
|
bool hookTailRan = route.RunsTeleportHook;
|
|
controller.CommitCanonicalTeleportFrame(
|
|
zeroVelocity: route.ZeroVelocity,
|
|
rearmConstraintLeash: route.ConstrainAfterRouting,
|
|
runTeleportHookTail: hookTailRan);
|
|
bool autorunCancelled = _localMovementState()?.CancelAutoRun() ?? false;
|
|
// R7 review fix (2026-08-05): retail CommandInterpreter::SendMovementEvent
|
|
// @0x006B4680 (PlayerTeleported's tail-jump) gates on TWO facts — a
|
|
// non-null raw motion state (TryGetOutboundPosition/TryGetOutboundMotion
|
|
// already cover that) AND `autonomy_level != 0`. This call was
|
|
// unconditional. This is route 3's OWN call site only —
|
|
// LocalPlayerOutboundController.TrySendMovement is shared with
|
|
// route 2's DIFFERENT retail function
|
|
// (CommandInterpreter::SendPositionEvent) and is not touched.
|
|
//
|
|
// Known approximation, filed AP-144 (2026-08-05, R7 round-3 review —
|
|
// CLAUDE.md's register rule is binding, not an implementer's call):
|
|
// this class only has RuntimeCharacterState.UsePositionFromServer in
|
|
// scope (`AutonomyLevel != FullAutonomyLevel(2)`, retail's
|
|
// `autonomy_level != 2`), not the raw AutonomyLevel — so
|
|
// `!UsePositionFromServer` sends only when AutonomyLevel==2,
|
|
// whereas retail's actual gate (`autonomy_level != 0`) ALSO sends
|
|
// at AutonomyLevel==1. The two agree everywhere except that one mid
|
|
// level, currently unreachable because TrySetAutonomyLevel has zero
|
|
// production callers. Retire by threading the raw AutonomyLevel
|
|
// through this constructor (and both host compositions) and gating
|
|
// on `!= 0` directly instead of reusing UsePositionFromServer.
|
|
if (!_usePositionFromServer())
|
|
{
|
|
_localPlayerOutbound.TrySendMovement(
|
|
_session(),
|
|
controller,
|
|
controller.CapturePresentationResult());
|
|
}
|
|
|
|
// D-T8 probe (temporary): confirms the reconcile suffix actually
|
|
// ran its three named duties on THIS commit, not just that the
|
|
// commit was reached. R3 review fix: the leash observable is
|
|
// ConstraintManager.IsConstrained ("has a leash"), not
|
|
// IsFullyConstrained ("has strained past 90% of it") — the latter
|
|
// reads false immediately after ConstrainTo re-anchors at distance
|
|
// 0, so every committed arrival printed leash=unarmed as coded.
|
|
PhysicsDiagnostics.LogLocalTeleportArrival(
|
|
cause: "portal",
|
|
placementStatus: "Committed",
|
|
portalGeneration: portal.RevealGeneration,
|
|
teleportSequence: portal.TeleportSequence,
|
|
destinationCell: portal.Projection.DestinationCell,
|
|
resolvedCell: record.FullCellId,
|
|
hookTailRan: hookTailRan,
|
|
leashArmed: controller.PositionManager?.Constraint?.IsConstrained
|
|
?? false,
|
|
autorunCancelled: autorunCancelled);
|
|
}
|
|
|
|
/// <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;
|
|
}
|
|
// C4 route 3 (trap T7): a portal pending never enters the
|
|
// force funnel — SettlePending's _newestForce re-issue
|
|
// decision belongs to the force arm only. The deferred
|
|
// commit's own reconciliation is the portal wake's entire
|
|
// terminal action.
|
|
if (pending.Portal.Present)
|
|
{
|
|
_pending = null;
|
|
// A2/D-T2.4 re-validation: RetryDeferred already moved
|
|
// the body (asynchronously, outside this class's
|
|
// control — nothing here can prevent that). What this
|
|
// CAN prevent is running the reconcile/ack suffix
|
|
// against a reveal that ended or was superseded while
|
|
// the park sat outstanding, which would otherwise
|
|
// publish a Place receipt naming a dead portal
|
|
// authority (architecture review A2's FIFO-wedge
|
|
// shape).
|
|
if (!IsPortalAuthorityCurrent(pending.Portal))
|
|
{
|
|
PhysicsDiagnostics.LogLocalTeleportArrival(
|
|
cause: "portal",
|
|
placementStatus: "AbandonedAtWake",
|
|
portalGeneration: pending.Portal.RevealGeneration,
|
|
teleportSequence: pending.Portal.TeleportSequence,
|
|
destinationCell:
|
|
pending.Portal.Projection.DestinationCell,
|
|
resolvedCell: pending.Record.FullCellId,
|
|
hookTailRan: false,
|
|
leashArmed: false,
|
|
autorunCancelled: false);
|
|
return;
|
|
}
|
|
ReconcileAndAcknowledgePortal(
|
|
pending.Record, pending.Route, pending.Portal);
|
|
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. A portal pending owes no re-issue and no
|
|
// ack (SendPositionImmediately is always false for the portal
|
|
// route), so it simply clears — matching the D-T5 refusal shape
|
|
// (nothing mutates; the transit's own cancellation/supersession
|
|
// machinery is the authority on what happens next).
|
|
//
|
|
// B1/N1 review fix (2026-08-05): this is the drive's own
|
|
// documented MODAL park outcome (ACE's 5-10 Hz broadcast Forgets
|
|
// any park surviving one interval — "the exact far-destination
|
|
// case the park exists to serve"), not a corner case. It does
|
|
// NOT set _lastCommittedPortal — nothing committed — so the host
|
|
// gate's TryConsumePortalCommit correctly reports "not yet" and
|
|
// either re-attempts fresh or converges through the transit's
|
|
// own cancellation, instead of the old PendingCount==0
|
|
// inference latching a false "committed".
|
|
if (pending.Portal.Present)
|
|
{
|
|
_pending = null;
|
|
PhysicsDiagnostics.LogLocalTeleportArrival(
|
|
cause: "portal",
|
|
placementStatus: "WatchDied",
|
|
portalGeneration: pending.Portal.RevealGeneration,
|
|
teleportSequence: pending.Portal.TeleportSequence,
|
|
destinationCell: pending.Portal.Projection.DestinationCell,
|
|
resolvedCell: pending.Record.FullCellId,
|
|
hookTailRan: false,
|
|
leashArmed: false,
|
|
autorunCancelled: false);
|
|
return;
|
|
}
|
|
// 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))
|
|
{
|
|
if (pending.Portal.Present
|
|
&& !IsPortalAuthorityCurrent(pending.Portal))
|
|
{
|
|
// A2/D-T2.4: unlike the AwaitingCommitWake branch above,
|
|
// this retry has NOT submitted yet — re-validating here
|
|
// genuinely prevents a stale commit rather than only
|
|
// suppressing its suffix.
|
|
_pending = null;
|
|
CancelToken(setPosition, pending.Token);
|
|
PhysicsDiagnostics.LogLocalTeleportArrival(
|
|
cause: "portal",
|
|
placementStatus: "AbandonedAtWake",
|
|
portalGeneration: pending.Portal.RevealGeneration,
|
|
teleportSequence: pending.Portal.TeleportSequence,
|
|
destinationCell: pending.Portal.Projection.DestinationCell,
|
|
resolvedCell: pending.Record.FullCellId,
|
|
hookTailRan: false,
|
|
leashArmed: false,
|
|
autorunCancelled: false);
|
|
return;
|
|
}
|
|
_ = pending.Portal.Present
|
|
? SubmitAndResolvePortal(
|
|
pending.Record, pending.Token, pending.Route, pending.Portal)
|
|
: SubmitAndResolve(pending.Record, pending.Token, pending.Route);
|
|
return;
|
|
}
|
|
|
|
// The prepare-retry operation died the same way. B1/N1: no commit,
|
|
// no _lastCommittedPortal write — see the watch-died branch above.
|
|
if (pending.Portal.Present)
|
|
{
|
|
_pending = null;
|
|
PhysicsDiagnostics.LogLocalTeleportArrival(
|
|
cause: "portal",
|
|
placementStatus: "PrepareRetryLost",
|
|
portalGeneration: pending.Portal.RevealGeneration,
|
|
teleportSequence: pending.Portal.TeleportSequence,
|
|
destinationCell: pending.Portal.Projection.DestinationCell,
|
|
resolvedCell: pending.Record.FullCellId,
|
|
hookTailRan: false,
|
|
leashArmed: false,
|
|
autorunCancelled: false);
|
|
return;
|
|
}
|
|
// (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->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)
|
|
{
|
|
// Cancellation, not withdrawal - see AbandonPending.
|
|
RuntimePlacementCancellationReceipt cancellation =
|
|
setPosition.ForgetExactPlacement(
|
|
token,
|
|
restoreCancelledPark: true);
|
|
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->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->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);
|
|
}
|
|
}
|