fix(physics): C4 route 3 — portal placement authority (local player)

Removes a duplicate placement authority for local-player portal arrival.
Portalling worked before this change and works after it — this is not a
bug fix, EXCEPT that it found and fixed one dead-code production bug.

THE PRODUCTION BUG: TryExecuteCanonicalPortalPlacement re-read the
accepted destination at Place time, but TryBeginPortalReveal already
consumes that slot at Aim time — so the arm was 100% dead code and every
real portal Place refused with host-token-unavailable. Found only
because we refused to accept 7 skipped tests instead of chasing the
count to zero.

RETAIL IS THE GENERIC PATH FOR THE THIRD ROUTE RUNNING:
SmartBox::TeleportPlayer @0x00453910 = SetPositionSimple(dest, 1) with
flags 0x1012, followed by PlayerPositionUpdated.

BOTH INVERSIONS, WITH THEIR ANCHORS: unlike route 2, the leash IS armed
here (ConstrainTo @0x0045418A) and velocity is zeroed
(set_velocity @0x004541B4); unlike route 4b-3, the local teleport_hook
runs AFTER placement (@0x004538AE).

THE THREE-ROUND DEFECT CHAIN, HONESTLY:
- Round 1 released the player at the pre-teleport position while the
  anim stream marched on — the contract wrongly assumed Place re-fires
  (process rule 1's third occurrence this campaign).
- Round 2's fix inferred commit from a global PendingCount, which three
  non-committing paths also clear — making the SAME bug complete
  cleanly and silently. Strictly worse than round 1: round 1 at least
  tripped portal-complete-before-materialized.
- Round 3 latches the commit where it actually happens
  (ReconcileAndAcknowledgePortal), keyed on reveal generation and
  teleport sequence, via TryConsumePortalCommit. Two of the three
  required regression tests landed and are sabotage-verified on both
  hosts (ParkedPlace_ForgottenByOrdinaryMergeDoesNotLatchAsCommitted /
  HeadlessPortalPrepareDestinationForgottenByOrdinaryMergeDoesNotLatchAsCommitted).
  The third (force-arm-takes-the-slot) was judged unnecessary on review:
  with the inference gone, PendingCount is only a "don't ask yet" guard
  at both gates, so a force operation occupying or vacating the slot no
  longer changes an input the commit decision reads — the case collapses
  into what the landed test already discriminates.

THE B2/P3 RESOLUTION: both round-2 reviews were right about different
branches of the same synchronous call. RuntimePlacementProjectionSubscription
.OnPlacement acknowledges the FIFO head only when TryApply returns true;
a Place whose portal authority went stale (transit ended/superseded
while parked) used to return false, wedging every later entity's
placement receipt behind it forever. Both sinks
(RuntimePlacementPresentationSink, HeadlessRuntimePlacementProjectionSink)
now acknowledge-and-ignore a stale-authority Place instead of refusing
it. The regression test (RuntimePlacementPresentationSinkTests
.PortalPlace_StaleTransitHostOrSequenceIsAcknowledgedAndIgnored) had
been asserting the old, wrong `false` behaviour; it now asserts and
sabotage-verifies the fix.

Also lands: AP-144 (register discipline — the portal movement-event
send reuses the stricter UsePositionFromServer gate where retail's
SendMovementEvent is the looser autonomy_level != 0 test, diverging
only at level 1, currently unreachable), AP-145 + issue #318 (the
local-player collision-shadow presentation write bypasses its own
publisher's ShadowObjects write via a direct cache .Set(), self-healing
only once dedup diverges — filed, not fixed, pending a composition
test), AD-42 deleted (its last citation retired by the canonical portal
arm), AD-2 updated (the wait-cue's trigger predicate now covers a
second cause), and two documentation corrections: the enter_world
misattribution (both call sites are in SmartBox::HandleCreateObject,
only one in the player branch — portal arrival is TeleportPlayer, not
enter_world) and the stale "local player never reaches this path"
comment on the generic-remote-render-pose write.

Suite: 11,090 passed / 4 skipped / 0 failed. No new skips, nothing
weakened.

STILL OWED: the connected two-client gate, with
ACDREAM_PROBE_LOCAL_TELEPORT=1, scored only if [local-tp] lines
actually appear in the capture — and explicitly NOT scored as covering
issue #318 (no composition test yet asserts PhysicsEngine.ShadowObjects
directly).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-05 03:57:37 +02:00
parent cd3129e9d6
commit e0f96a55bf
24 changed files with 5261 additions and 243 deletions

View file

@ -1949,6 +1949,145 @@ public sealed class PlayerMovementController
UpdateCellId(_body.CellPosition.ObjCellId, "force-position");
}
/// <summary>
/// C4 route 3: the controller-local half of a portal-teleport commit
/// whose body write, cell install, and orientation already happened
/// inside Runtime's canonical <c>RuntimeSetPositionState.CommitCanonical</c>
/// (retail <c>CPhysicsObj::SetPositionSimple</c> @0x005162B0 with flags
/// <c>0x1012</c>, called from <c>SmartBox::TeleportPlayer</c> @0x00453910,
/// acclient_2013_pseudo_c.txt:284276/92528). Unlike
/// <see cref="CommitCanonicalForcePositionFrame"/> (which the FORCE_POSITION
/// branch's early return at @0x0045409D exempts from every
/// <c>ConstrainTo</c>), the local TELEPORT branch of
/// <c>SmartBox::HandleReceivedPosition</c> (@0x0045415F) DOES re-arm the
/// leash (@0x0045418A, anchored at the received destination) and DOES
/// zero velocity (@0x004541B4) — the inversion is deliberate, not a
/// missed exemption; see docs/research/2026-08-04-c4-route-3-contract.md
/// §2 Inversion A.
///
/// Performs every <see cref="SetPositionCore"/> duty NOT already covered
/// by the canonical commit (P1's duty map): render-lerp anchor reset,
/// <c>UpdateCellId</c> publication, the retail teleport_hook tail
/// (UnStick @0x00514eee / UnConstrain @0x00514f02 / re-arm @0x0045418A),
/// the retail StopCompletely full stop (0x00527e40, zeroes velocity and
/// resets fwd/sidestep/turn commands so input resumes at rest), the
/// input-edge/mouse press-edge reset, and the physics-clock reset for a
/// fresh <c>update_object</c> boundary. TransientState (Contact/OnWalkable/
/// Sliding/WaterContact) is deliberately NOT re-seeded here — the canonical
/// commit's <c>PhysicsObjUpdate.CommitSetPositionContactTransition</c>
/// already derives those bits from the SLIDE placement's OWN resolved
/// contact result, which is more retail-faithful than the old
/// <see cref="SetPositionCore"/>'s unconditional
/// <c>Contact|OnWalkable|Active</c> overwrite (that overwrite could mark a
/// portal arrival grounded even when the destination placement actually
/// resolved airborne). <c>Active</c> is untouched because a live in-world
/// local player already carries it; the canonical commit only sets it on
/// entry from a celless residence, which a portal arrival never is.
/// </summary>
/// <summary>
/// A4 review fix (2026-08-05): the two inversions this method embodies
/// are named, retail-cited facts on the classifier's
/// <c>RuntimeAuthoritativePositionRoute</c> — <c>ZeroVelocity</c> and
/// <c>ConstrainPhase.AfterPositionOperation</c> — and this method must
/// actually READ them rather than assume the LocalPlayer-teleport
/// branch's values are the only ones that will ever reach it. Both
/// parameters are the route's own facts, passed by the one caller
/// (<c>RuntimeAcceptedPositionDriveController.ReconcileAndAcknowledgePortal</c>);
/// a future classifier edit that changes either value now changes this
/// method's behaviour instead of silently disagreeing with it.
/// <para>
/// Coordinator note (round-3 closeout, 2026-08-05): the two parameters
/// are read, not hardcoded — but they are NOT equally load-bearing.
/// <paramref name="zeroVelocity"/> is read and applied, then
/// <see cref="StopCompletelyAtPhysicsObjectBoundary"/> runs
/// UNCONDITIONALLY on the very next line and zeroes velocity again — so
/// a <c>zeroVelocity: false</c> sabotage changes nothing observable
/// here; the field is proven read but not proven DISCRIMINATING.
/// <paramref name="rearmConstraintLeash"/> (<c>ConstrainAfterRouting</c>)
/// has no such unconditional fallback and IS the load-bearing one —
/// it alone decides whether the leash re-arms. Do not read this doc
/// comment as proving both fields equally; only the leash flag is.
/// </para>
/// </summary>
/// <param name="runTeleportHookTail">
/// N4 review fix (2026-08-05): before this parameter, the caller gated
/// the ENTIRE method call on <c>route.RunsTeleportHook</c> — but retail's
/// <c>SetPositionInternal</c> @0x00515330 does the frame/cell/stop/input-
/// reset/clock work UNCONDITIONALLY; only retail's <c>teleport_hook</c>
/// @0x00514ED0 (UnStick/UnConstrain/re-arm, mapped below) is itself
/// conditional on the hook phase. Gating the whole call meant a future
/// <see cref="AcDream.Runtime.Physics.RuntimeAuthoritativePositionRoute.TeleportHookPhase"/>
/// of <c>None</c> would silently skip the render-root <c>UpdateCellId</c>
/// publish too — the doorway-FLAP class. Today the portal route always
/// sets a non-None phase, so this parameter is always <c>true</c> in
/// production and there is no live behavior change; it exists so a
/// future <c>None</c> phase changes only the hook tail, not the frame
/// commit.
/// </param>
internal void CommitCanonicalTeleportFrame(
bool zeroVelocity,
bool rearmConstraintLeash,
bool runTeleportHookTail = true)
{
EnsurePublishedForRuntimeOperation();
_prevPhysicsPos = _body.Position;
_currPhysicsPos = _body.Position;
UpdateCellId(_body.CellPosition.ObjCellId, "teleport");
// Retail set_velocity(player, 0, 1) @0x004541B4 — route.ZeroVelocity.
if (zeroVelocity)
_body.Velocity = Vector3.Zero;
// Retail teleport idle is a FULL stop (StopCompletely 0x00527e40):
// resets fwd/sidestep/turn COMMANDS and zeroes velocity again so the
// motion interpreter cannot reconstruct the pre-teleport run vector
// the instant input resumes.
StopCompletelyAtPhysicsObjectBoundary();
_activeInputTurnCommand = null;
_activeInputTurnSpeed = 0f;
_activeInputTurnFromMouse = false;
_activeInputSidestepCommand = null;
_activeInputSidestepUsesRunHold = false;
_mouseLookActive = false;
_mouseTurnSamplePending = false;
_mouseTurnAdjustment = 0f;
_mouseMovementEventCandidate = false;
_mouseMovementEventPending = false;
// Retail teleport_hook @0x00514ed0 tears down any active stick/leash
// unconditionally, then HandleReceivedPosition's TELEPORT branch
// immediately re-arms the leash anchored to the just-committed
// position ONLY when ConstrainPhase is AfterPositionOperation
// (Inversion A — the opposite of
// CommitCanonicalForcePositionFrame's no-re-arm rule, itself
// route.ConstrainPhase.None for FORCE_POSITION). N4 review fix: this
// is the ONLY part of this method retail actually conditions on the
// teleport-hook phase — everything above runs unconditionally.
if (runTeleportHookTail)
{
PositionManager?.UnStick();
PositionManager?.UnConstrain();
if (rearmConstraintLeash)
RearmConstraintLeashAtCurrentPosition();
}
// Reset the edge tracker: the stop wiped the motion state, so keys
// still physically held must re-fire as press edges on the next
// Update (matches SetPositionCore's walking-straight-out-of-a-
// teleport behavior while W stays held).
_prevForwardHeld = false;
_prevBackwardHeld = false;
_prevStrafeLeftHeld = false;
_prevStrafeRightHeld = false;
_prevTurnLeftHeld = false;
_prevTurnRightHeld = false;
_prevRunHeld = false;
_hasInputSnapshot = false;
// Reset physics clock so any subsequent update_object calls start fresh.
_body.LastUpdateTime = 0.0;
_objectClock.ResetForEnterWorld();
}
private Vector3 ComputeRenderPosition()
{
float alpha = Math.Clamp(

View file

@ -130,6 +130,11 @@ internal enum RuntimeAcceptedPositionExecutionStatus : byte
/// 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
@ -170,8 +175,26 @@ public sealed class RuntimeAcceptedPositionDriveController
/// 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>
@ -218,6 +241,40 @@ public sealed class RuntimeAcceptedPositionDriveController
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
@ -244,7 +301,9 @@ public sealed class RuntimeAcceptedPositionDriveController
Func<uint> localPlayerServerGuid,
Func<PlayerMovementController?> localController,
Func<bool> usePositionFromServer,
Func<WorldSession?> session)
Func<WorldSession?> session,
Func<RuntimeLocalPlayerMovementState?>? localMovementState = null,
Func<RuntimePortalPlacementAuthority, bool>? isPortalAuthorityCurrent = null)
{
_entityObjects = entityObjects
?? throw new ArgumentNullException(nameof(entityObjects));
@ -262,12 +321,53 @@ public sealed class RuntimeAcceptedPositionDriveController
_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
@ -308,10 +408,22 @@ public sealed class RuntimeAcceptedPositionDriveController
/// 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;
@ -401,6 +513,411 @@ public sealed class RuntimeAcceptedPositionDriveController
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
@ -461,6 +978,42 @@ public sealed class RuntimeAcceptedPositionDriveController
// 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).
@ -480,9 +1033,39 @@ public sealed class RuntimeAcceptedPositionDriveController
}
// 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).
// 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,
@ -493,14 +1076,54 @@ public sealed class RuntimeAcceptedPositionDriveController
if (setPosition.IsPlacementCurrent(pending.Token))
{
_ = SubmitAndResolve(pending.Record, pending.Token, pending.Route);
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. (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.)
// 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,

View file

@ -39,9 +39,18 @@ public interface IRuntimeDirectWorldProjection
void BeginTeleport();
/// <summary>
/// C4 route 3 (D-T6): <paramref name="portal"/> is the SAME host token
/// <see cref="RuntimeLiveEntitySessionController.TryCompletePortal"/>
/// just registered via <c>TryRegisterHostProjection</c> — the producer's
/// generation/sequence/projection are all already in scope here, so no
/// new <c>WorldRevealCoordinator</c>-style exposure is needed on this
/// side either.
/// </summary>
RuntimeDestinationReadiness PrepareDestination(
long revealGeneration,
RuntimeTeleportDestination destination);
RuntimeTeleportDestination destination,
RuntimeWorldHostProjectionToken portal);
}
/// <summary>
@ -490,6 +499,43 @@ public sealed class RuntimeLiveEntitySessionController
acknowledgeProjection: null,
out _);
/// <summary>
/// A1/A3 review fix (2026-08-05): the generation/destination/projection
/// of an accepted portal reveal that registered its host projection but
/// has not yet actually placed the local player. Headless is
/// message-driven, not per-frame — <see cref="TryCompletePortal"/> used
/// to run the ENTIRE completion sequence (readiness ack, materialized
/// ack, complete, LoginComplete, EndTeleport) unconditionally in one
/// synchronous call, discarding the canonical portal arm's own status
/// (architecture review A3). A <c>DeferredCell</c> park is a NORMAL
/// headless outcome — <see cref="IRuntimeDirectWorldProjection.CenterOnAcceptedForcePosition"/>'s
/// doc explains why the narrow collision window makes a park real
/// rather than a dead end — so this field lets
/// <see cref="PumpPortalCompletion"/> retry on the host's own per-tick
/// cadence (<c>HeadlessSessionHost.Tick</c>) instead of either
/// completing a materialization that never happened or throwing on
/// every ordinary "destination not resident yet" park.
/// </summary>
private (long Generation,
RuntimeTeleportDestination Destination,
RuntimeWorldHostProjectionToken Projection)? _pendingPortalCompletion;
/// <summary>
/// B4 review fix (2026-08-05): the retry count for the CURRENT
/// <see cref="_pendingPortalCompletion"/>, reset whenever a NEW portal
/// begins. Graphical's equivalent wait has a user-visible cue (AD-2's
/// centered wait state) when a park runs long; headless had neither a
/// cue, a bound, nor a log — an indefinitely stuck park (a destination
/// landblock whose collision generation never publishes) was silent and
/// undiagnosable. This does not make the retry fatal — K4's 30-session
/// endurance profile must survive a legitimately slow-publishing
/// landblock — it only makes a stuck park OBSERVABLE via periodic log
/// lines instead of running forever in silence.
/// </summary>
private int _pendingPortalCompletionRetryCount;
private const int PendingPortalCompletionLogInterval = 100;
private void TryCompletePortal()
{
RuntimeWorldTransitState transit = _runtime.TransitOwner;
@ -517,11 +563,39 @@ public sealed class RuntimeLiveEntitySessionController
projection,
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered);
_pendingPortalCompletion = (generation, destination, projection);
_pendingPortalCompletionRetryCount = 0;
TryAdvancePortalCompletion();
}
/// <summary>
/// A1/A3 review fix: the retryable second half of
/// <see cref="TryCompletePortal"/>. Attempts the canonical placement
/// (via <see cref="_worldProjection"/>, which owns the drive controller)
/// exactly once per call; if it has not committed yet, this returns
/// having mutated nothing beyond what the attempt itself did (a
/// DeferredCell park, safely retryable by construction — see
/// <see cref="HeadlessSessionWorldProjection.PrepareDestination"/>'s own
/// doc), and <see cref="PumpPortalCompletion"/> calls this again on the
/// next host tick. Once <c>IsCollisionReady</c> comes back true — which
/// only happens after a genuine <c>Committed</c> status — the full
/// readiness/materialized/complete/LoginComplete/EndTeleport sequence
/// runs exactly as before this fix, unconditionally, in one call.
/// </summary>
private void TryAdvancePortalCompletion()
{
if (_pendingPortalCompletion is not { } pending)
return;
(long generation, RuntimeTeleportDestination destination,
RuntimeWorldHostProjectionToken projection) = pending;
RuntimeWorldTransitState transit = _runtime.TransitOwner;
bool indoor = (destination.CellId & 0xFFFFu) >= 0x0100u;
RuntimeDestinationReadiness readiness =
_worldProjection?.PrepareDestination(
generation,
destination)
destination,
projection)
?? new RuntimeDestinationReadiness(
generation,
destination.CellId,
@ -531,6 +605,32 @@ public sealed class RuntimeLiveEntitySessionController
IsRenderNeighborhoodReady: true,
AreCompositeTexturesReady: true,
IsCollisionReady: true);
if (!readiness.IsCollisionReady)
{
// Still parked - PrepareDestination attempted (or is waiting on
// an outstanding DeferredCell wake) and has not committed yet.
// Nothing acknowledged, nothing completed; PumpPortalCompletion
// retries next tick.
//
// B4 review fix: periodic diagnostic so an indefinitely-stuck
// park is observable instead of silent. Not bounded to a throw -
// a slow-publishing landblock is a legitimate transient this
// host must ride out (N3's lesson: don't make a transient
// fatal).
_pendingPortalCompletionRetryCount++;
if (_pendingPortalCompletionRetryCount % PendingPortalCompletionLogInterval == 0)
{
_log(
$"headless: portal completion still parked after "
+ $"{_pendingPortalCompletionRetryCount} retries "
+ $"generation={generation} cell=0x{destination.CellId:X8}");
}
return;
}
_pendingPortalCompletion = null;
_pendingPortalCompletionRetryCount = 0;
if (!transit.AcknowledgeDestinationReadiness(
readiness))
{
@ -581,6 +681,14 @@ public sealed class RuntimeLiveEntitySessionController
+ $"cell=0x{destination.CellId:X8}");
}
/// <summary>
/// A1/A3 review fix: called from <c>HeadlessSessionHost.Tick</c>
/// alongside <c>HeadlessSessionWorldProjection.PumpFirstEntry</c> —
/// retries a parked portal completion on the host's own per-tick
/// cadence. A no-op whenever nothing is pending.
/// </summary>
public void PumpPortalCompletion() => TryAdvancePortalCompletion();
private static void Acknowledge(
RuntimeWorldTransitState transit,
RuntimeWorldHostProjectionToken projection,