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

@ -586,7 +586,21 @@ internal sealed class SessionPlayerCompositionPhase
() => d.PlayerIdentity.ServerGuid,
() => d.PlayerController.Controller,
() => d.Character.UsePositionFromServer,
() => liveSessionSource.CurrentSession);
() => liveSessionSource.CurrentSession,
// C4 route 3: the portal arm's PlayerTeleported port needs the
// J5.4 autorun latch owner, one level above the raw controller.
() => d.PlayerController,
// A2/D-T2.4 review fix (2026-08-05): the SAME idempotent query
// the Place edge itself uses (WorldRevealCoordinator.
// CanPlacePortalDestination -> RuntimeWorldTransitState) lets a
// DeferredCell wake re-validate before reconciling instead of
// running the ack suffix against a reveal that ended or was
// superseded while the park sat outstanding.
isPortalAuthorityCurrent: portal => live.WorldTransit
.CanPlacePortalDestination(
portal.RevealGeneration,
portal.TeleportSequence,
portal.Projection.DestinationCell));
// C4 route 4b-2 (2026-08-04): the graphical remote-placement drive
// controller — route 4b-1's dormant owner, now driven by the remote
// far snap. Its service window is the graphical host's near-tier
@ -886,16 +900,17 @@ internal sealed class SessionPlayerCompositionPhase
live.WorldTransit,
worldReveal,
new LocalPlayerTeleportPlacement(
d.PhysicsEngine,
live.LiveEntities,
d.PlayerIdentity,
d.PlayerController,
d.PlayerHost,
d.ChaseCameraInput,
d.WorldOrigin,
liveSpatialReconciler),
new LocalPlayerTeleportSession(liveSessionSource),
presentation);
presentation,
// C4 route 3: the portal arm shares route 2's Runtime
// SetPosition drive controller.
acceptedPositionDrive);
LocalPlayerTeleportController CreateLocalTeleportWithTunnel(
PortalTunnelPresentation portalTunnel) =>

View file

@ -2273,13 +2273,28 @@ internal sealed class LiveEntityNetworkUpdateController
// Position resolves to NoPositionOperation (retail's airborne
// no-op — writes nothing at all) or Interpolate (retail's near
// InterpolateTo queue — no direct body write here) never receives
// the generic write below. The local player never reaches this
// generic-remote code path at all. C4 route 4b-2 routes the >=96 m
// far snap and C4 route 4b-3 routes the teleport/cell-less
// classification through the canonical Runtime placement owner
// the generic write below. C4 route 4b-2 routes the >=96 m far snap
// and C4 route 4b-3 routes the teleport/cell-less classification
// through the canonical Runtime placement owner
// (ApplyRemoteContactRouting); a rejected authority or payload, and
// "no classification at all", take the stated UnroutedCatchUp
// policy (RuntimeRemoteFarSnapPosition.ResolveArm).
//
// CORRECTED 2026-08-04 (C4 route 3, process rule 6 — this comment
// used to claim "the local player never reaches this generic-remote
// code path at all", which is FALSE): for the local player,
// `earlyRemoteRoute` is null and `OwnsSteadyState(null)` is false
// (RuntimeRemoteSteadyStatePosition.cs — both pattern matches fail
// on null), so every accepted local Apply — including the portal
// DESTINATION Position itself — reaches and runs the generic write
// + rebucket below, writing the raw wire pose onto the local
// player's WorldEntity while portal space still covers the
// viewport. This is pre-existing, tolerated (AD-2/AP-131/#275
// territory — see the D-T7 discussion in
// docs/research/2026-08-04-c4-route-3-contract.md), and overwritten
// by the committed portal Place receipt's presentation suffix
// (LocalPlayerTeleportPlacement.Place) once the canonical Runtime
// commit lands. Route 3 does not suppress it.
TryApplyGenericRemoteRenderPose(
entity,

View file

@ -10,6 +10,8 @@ using AcDream.Core.Physics;
using AcDream.Core.Rendering;
using AcDream.Core.World;
using AcDream.Runtime;
using AcDream.Runtime.Physics;
using AcDream.Runtime.Session;
using AcDream.Runtime.World;
namespace AcDream.App.Streaming;
@ -173,68 +175,67 @@ internal sealed class LocalPlayerTeleportStreamingOperations
internal interface ILocalPlayerTeleportPlacement
{
void Place(Vector3 position, uint cellId, Quaternion rotation);
void Place(Quaternion rotation);
}
/// <summary>
/// Commits the local player's deferred portal arrival. It owns the exact
/// Place -&gt; root/controller/camera mutation -&gt; spatial reconcile edge.
/// C4 route 3: acknowledges the local player's deferred portal arrival —
/// the canonical placement itself now runs through
/// <see cref="RuntimeAcceptedPositionDriveController.TryExecuteAcceptedPortalArrival"/>
/// (a portal arm sharing route 2's Runtime SetPosition owner), retiring the
/// duplicate Resolve/SetPosition authority this class used to own (D1;
/// docs/research/2026-08-04-c4-route-3-contract.md D-T4). This class runs
/// AFTER that commit succeeds. A10 review fix (2026-08-05): the render
/// <see cref="WorldEntity"/> pose write and rebucket this method performs
/// are REDUNDANT repeats of a mutation the canonical Place receipt already
/// made — <c>RuntimePlacementPresentationSink.TryApply</c> →
/// <c>LiveEntityRuntime.TryApplyRuntimePlacementPlace</c> already calls
/// <c>entity.SetPosition</c>/sets <c>Rotation</c>/<c>ParentCellId</c> and
/// rebuckets, synchronously, before <c>TryPublishPlace</c>'s OWN snapshot
/// even runs (proof obligation P2's ordering). This method's writes are
/// therefore harmless-but-duplicate, not the render entity's ONLY mover as
/// an earlier revision of this comment claimed; kept because they cost
/// nothing extra and this is also where the retail teleport_hook tail's
/// remaining local-player-visible actions run (target-watcher
/// notification, camera reset, spatial reconcile).
/// <paramref name="rotation"/> is the retained accepted destination's wire
/// orientation — the resolved body orientation was already committed
/// identically by <c>CommitCanonical</c> (retail's teleport branch does not
/// independently reorient the mover), so re-deriving it here would only add
/// a second copy of the same source of truth.
/// </summary>
internal sealed class LocalPlayerTeleportPlacement : ILocalPlayerTeleportPlacement
{
private readonly PhysicsEngine _physics;
private readonly LiveEntityRuntime _liveEntities;
private readonly ILocalPlayerIdentitySource _identity;
private readonly IRuntimeLocalPlayerControllerSource _controller;
private readonly ILocalPlayerPhysicsHostSource _host;
private readonly ChaseCameraInputState _cameras;
private readonly LiveWorldOriginState _origin;
private readonly ILiveSpatialReconcilePhase _spatial;
public LocalPlayerTeleportPlacement(
PhysicsEngine physics,
LiveEntityRuntime liveEntities,
ILocalPlayerIdentitySource identity,
IRuntimeLocalPlayerControllerSource controller,
ILocalPlayerPhysicsHostSource host,
ChaseCameraInputState cameras,
LiveWorldOriginState origin,
ILiveSpatialReconcilePhase spatial)
{
_physics = physics ?? throw new ArgumentNullException(nameof(physics));
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
_identity = identity ?? throw new ArgumentNullException(nameof(identity));
_controller = controller ?? throw new ArgumentNullException(nameof(controller));
_host = host ?? throw new ArgumentNullException(nameof(host));
_cameras = cameras ?? throw new ArgumentNullException(nameof(cameras));
_origin = origin ?? throw new ArgumentNullException(nameof(origin));
_spatial = spatial ?? throw new ArgumentNullException(nameof(spatial));
}
public void Place(Vector3 position, uint cellId, Quaternion rotation)
public void Place(Quaternion rotation)
{
PlayerMovementController controller = _controller.Controller
?? throw new InvalidOperationException(
"Teleport Place ran without the local player controller.");
var resolved = _physics.Resolve(
position,
cellId,
Vector3.Zero,
controller.StepUpHeight);
var snapped = new Vector3(
resolved.Position.X,
resolved.Position.Y,
resolved.Position.Z);
uint playerGuid = _identity.ServerGuid;
controller.SetPosition(
snapped,
resolved.CellId,
CellLocalForSeed(snapped, resolved.CellId));
// SnapToCell owns the retail Position frame and may normalize an
// outdoor land-cell index from the cell-local origin. Publish that
// canonical result, not the pre-snap resolver hint, to rendering.
if (_liveEntities.TryGetWorldEntity(
playerGuid,
out WorldEntity? entity))
@ -245,12 +246,12 @@ internal sealed class LocalPlayerTeleportPlacement : ILocalPlayerTeleportPlaceme
// Retail CPhysicsObj::enter_world installs the object in its
// destination CObjCell before hidden scripts/particles resume.
// The accepted Position packet has already advanced FullCellId,
// but that wire fact alone does not move acdream's retained
// projection out of its source/pending GPU bucket. Commit both
// halves of the spatial move here, while portal space still owns
// the viewport, so CPhysicsObj::update_object's cell-gated tail
// can advance the Hidden/UnHide PES chain at retail's boundary.
// The canonical commit has already advanced FullCellId, but that
// fact alone does not move acdream's retained projection out of
// its source/pending GPU bucket. Commit both halves of the
// spatial move here, while portal space still owns the
// viewport, so CPhysicsObj::update_object's cell-gated tail can
// advance the Hidden/UnHide PES chain at retail's boundary.
if (!_liveEntities.RebucketLiveEntity(playerGuid, controller.CellId))
{
throw new InvalidOperationException(
@ -260,9 +261,13 @@ internal sealed class LocalPlayerTeleportPlacement : ILocalPlayerTeleportPlaceme
}
// Retail teleport_hook tail @ 0x00514ED0 clears the local target and
// notifies every watcher that this object teleported.
// notifies every watcher that this object teleported. The body's
// constraint leash re-arm and orientation are already the canonical
// commit's job (RuntimeAcceptedPositionDriveController
// .ReconcileAndAcknowledgePortal -> PlayerMovementController
// .CommitCanonicalTeleportFrame), so this suffix only acknowledges
// the result into presentation.
_host.Host?.NotifyTeleported();
controller.SetBodyOrientation(rotation);
_cameras.Legacy?.Update(controller.Position, controller.Yaw);
_cameras.Retail?.ResetViewerToPlayer(controller.Position, controller.Yaw);
@ -276,17 +281,6 @@ internal sealed class LocalPlayerTeleportPlacement : ILocalPlayerTeleportPlaceme
$"live: teleport materialized - snapped to {controller.Position} "
+ $"cell=0x{controller.CellId:X8}");
}
private Vector3 CellLocalForSeed(Vector3 worldPosition, uint cellId)
{
int landblockX = (int)((cellId >> 24) & 0xFFu);
int landblockY = (int)((cellId >> 16) & 0xFFu);
var origin = new Vector3(
(landblockX - _origin.CenterX) * 192f,
(landblockY - _origin.CenterY) * 192f,
0f);
return worldPosition - origin;
}
}
internal interface ILocalPlayerTeleportSession
@ -397,11 +391,28 @@ internal sealed class LocalPlayerTeleportController
private readonly ILocalPlayerTeleportPlacement _placement;
private readonly ILocalPlayerTeleportSession _session;
private readonly ILocalPlayerTeleportPresentation _presentation;
private readonly RuntimeAcceptedPositionDriveController _acceptedPositionDrive;
private Vector3 _pendingPosition;
private uint _pendingCell;
private Quaternion _pendingRotation = Quaternion.Identity;
private long _pendingRevealGeneration;
private RuntimeTeleportDestination _pendingDestination;
private bool _hasPendingDestination;
/// <summary>
/// A1 review fix (2026-08-05): true once the canonical Runtime commit
/// for THIS teleport lifetime has actually happened. See the class doc
/// on <see cref="TryAdvancePortalCommit"/> for why this exists.
/// </summary>
private bool _placementCommitted;
/// <summary>
/// A1 review fix: true while a <c>DeferredCell</c> park is outstanding
/// for the local player's one possible pending operation. See
/// <see cref="TryAdvancePortalCommit"/>.
/// </summary>
private bool _awaitingDeferredWake;
private float _holdSeconds;
private long _lifetimeGeneration;
private bool _disposed;
@ -415,7 +426,8 @@ internal sealed class LocalPlayerTeleportController
WorldRevealCoordinator worldReveal,
ILocalPlayerTeleportPlacement placement,
ILocalPlayerTeleportSession session,
ILocalPlayerTeleportPresentation presentation)
ILocalPlayerTeleportPresentation presentation,
RuntimeAcceptedPositionDriveController acceptedPositionDrive)
{
_authority = authority ?? throw new ArgumentNullException(nameof(authority));
_input = input ?? throw new ArgumentNullException(nameof(input));
@ -426,6 +438,8 @@ internal sealed class LocalPlayerTeleportController
_placement = placement ?? throw new ArgumentNullException(nameof(placement));
_session = session ?? throw new ArgumentNullException(nameof(session));
_presentation = presentation ?? throw new ArgumentNullException(nameof(presentation));
_acceptedPositionDrive = acceptedPositionDrive
?? throw new ArgumentNullException(nameof(acceptedPositionDrive));
}
public bool IsActive => _transit.IsTeleportActive;
@ -488,21 +502,41 @@ internal sealed class LocalPlayerTeleportController
bool haveDestination = _pendingCell != 0u;
bool originReady = !_streaming.IsRecenterPending;
bool ready = haveDestination
bool dataReady = haveDestination
&& originReady
&& _worldReveal.Evaluate(_pendingCell).IsReady;
if (!IsCurrentLifetime(generation, sequence))
return;
if (haveDestination && !ready)
// A1 review fix (2026-08-05, retail/architecture review): the
// sequencer's Tunnel -> TunnelContinue transition
// (TeleportAnimSequencer.cs:134-141) is unconditional and
// irreversible the instant it observes `worldReady` true; by the
// time a failed placement is discovered the stream has already left
// Tunnel with no path back, and TeleportAnimSequencer itself is
// untouched (stop condition 2 forbids sequencer timing changes). So
// the boolean fed into the sequencer must never mean "the data is
// ready" alone - it must mean "the canonical Runtime commit has
// ALREADY happened", checked/attempted fresh every tick via
// TryAdvancePortalCommit. This makes D-T5 row 2's "the NEXT Tick
// re-attempts the Place edge" real: the sequencer simply never
// leaves Tunnel while the commit keeps refusing, and by the time it
// finally does leave Tunnel and fire Place, TryAdvancePortalCommit
// has already made the Runtime side succeed - the Place-event
// handler below only ever runs the presentation suffix.
bool placementReady = dataReady && TryAdvancePortalCommit(sequence);
if (!IsCurrentLifetime(generation, sequence))
return;
if (haveDestination && !placementReady)
_holdSeconds += deltaSeconds;
_presentation.SetWaitCue(
haveDestination
&& !ready
&& !placementReady
&& _worldReveal.ObserveWait(
TimeSpan.FromSeconds(_holdSeconds)));
var (_, events) = _presentation.Tick(deltaSeconds, ready);
var (_, events) = _presentation.Tick(deltaSeconds, placementReady);
if (!IsCurrentLifetime(generation, sequence))
return;
@ -511,17 +545,32 @@ internal sealed class LocalPlayerTeleportController
switch (teleportEvent)
{
case TeleportAnimEvent.Place:
// TryAdvancePortalCommit above is the only path that
// makes `placementReady` (and, with the REAL sequencer,
// this event) true, so the canonical Runtime commit has
// ALREADY succeeded by construction on that path - this
// only runs the presentation suffix (D-T4). The
// _placementCommitted re-check stays defensive: it is
// the exact same shape of guard IsCurrentLifetime below
// already applies to every other step of this case, for
// a transit that goes stale between the gate above and
// this line being reached.
if (!_placementCommitted)
return;
// B7 review fix (2026-08-05): re-derived, not assumed -
// if the reveal was cancelled/superseded in the window
// between the commit above and this event being
// processed, ObserveMaterialized below would refuse but
// Place/the presentation suffix would already have run
// against a reveal that is no longer current. Same
// check TryExecuteCanonicalPortalPlacementCore itself
// gates on; idempotent to repeat here.
if (!_worldReveal.CanPlacePortalDestination(
_pendingRevealGeneration,
sequence,
_pendingCell))
_pendingRevealGeneration, sequence, _pendingCell))
{
return;
}
_placement.Place(
_pendingPosition,
_pendingCell,
_pendingRotation);
_placement.Place(_pendingRotation);
if (!IsCurrentLifetime(generation, sequence))
return;
_worldReveal.ObserveMaterialized(
@ -569,6 +618,193 @@ internal sealed class LocalPlayerTeleportController
_presentation.TickTunnel(deltaSeconds);
}
/// <summary>
/// A1 review fix (2026-08-05): the one gate that decides whether the
/// anim sequencer is allowed to see <c>worldReady=true</c>. Returns
/// <see langword="true"/> ONLY once the canonical Runtime commit for
/// THIS teleport lifetime has actually happened — never speculatively,
/// never optimistically. Three states:
///
/// <list type="bullet">
/// <item><description>Already committed
/// (<see cref="_placementCommitted"/>) — returns
/// <see langword="true"/> immediately, every subsequent Tick.</description></item>
/// <item><description>A <c>DeferredCell</c> park is outstanding
/// (<see cref="_awaitingDeferredWake"/>) — polls
/// <see cref="RuntimeAcceptedPositionDriveController.PendingCount"/>
/// only to decide whether to re-attempt the Runtime call: Runtime's own
/// Begin would just refuse a second overlapping attempt with
/// <c>Contention</c> while a park is outstanding (the drive tracks at
/// most one pending operation for the local player), so retrying blind
/// would only add noise. B1 review fix (2026-08-05): once
/// <c>PendingCount</c> returns to 0 the park is DONE, but "done" is not
/// "committed" — the drive's own doc names a merge-time <c>Forget</c>
/// (an ordinary ACE broadcast arriving mid-park) as the EXPECTED way a
/// park resolves without committing, and A2's own abandon branches are
/// a second way. The OLD code inferred commit from the empty slot
/// alone; this now asks
/// <see cref="RuntimeAcceptedPositionDriveController.TryConsumePortalCommit"/>,
/// which the drive latches ONLY inside a REAL
/// <c>ReconcileAndAcknowledgePortal</c> call, keyed to this exact
/// reveal generation/sequence. A "no" here is NOT a failure — it just
/// means the park ended without placing, so <see cref="_awaitingDeferredWake"/>
/// clears and the method falls through to a fresh attempt below,
/// safely (nothing is pending anymore).</description></item>
/// <item><description>Neither — attempt the canonical placement fresh
/// this tick. <c>Committed</c> latches
/// <see cref="_placementCommitted"/>; <c>DeferredCell</c> latches
/// <see cref="_awaitingDeferredWake"/>; every other status (Contention,
/// Rejected, NotApplicable, or the transit no longer owning this
/// reveal) is the D-T5 refusal shape — nothing mutates, and the SAME
/// predicate retries automatically on the NEXT Tick, which is what
/// makes D-T5 row 2's "the next Tick re-attempts the Place edge"
/// mechanism real without ever touching
/// <see cref="AcDream.Core.World.TeleportAnimSequencer"/>.</description></item>
/// </list>
/// </summary>
private bool TryAdvancePortalCommit(ushort sequence)
{
if (_placementCommitted)
return true;
if (_awaitingDeferredWake)
{
if (_acceptedPositionDrive.PendingCount != 0)
return false;
_awaitingDeferredWake = false;
if (_acceptedPositionDrive.TryConsumePortalCommit(
_pendingRevealGeneration, sequence))
{
_placementCommitted = true;
return true;
}
// The park ended without placing (Forgotten, or abandoned by
// A2's re-validation). Fall through to the fresh-attempt path
// below in this SAME call — nothing is pending, so it is safe.
}
if (!_worldReveal.CanPlacePortalDestination(
_pendingRevealGeneration,
sequence,
_pendingCell))
{
PhysicsDiagnostics.LogTeleport(
"REFUSED", _pendingCell, "cause=stale-reveal");
// R8 residual fix (2026-08-05): this refusal previously only
// logged through PhysicsDiagnostics.LogTeleport, gated by the
// DIFFERENT ACDREAM_PROBE_TELEPORT flag — invisible under
// ACDREAM_PROBE_LOCAL_TELEPORT, the gate the rest of this
// route's arrival/commit lines use. No placement was attempted,
// so there is no resolved cell/leash/autorun fact to report.
PhysicsDiagnostics.LogLocalTeleportArrival(
cause: "stale-reveal",
placementStatus: "Refused",
portalGeneration: _pendingRevealGeneration,
teleportSequence: sequence,
destinationCell: _pendingCell,
resolvedCell: 0u,
hookTailRan: false,
leashArmed: false,
autorunCancelled: false);
return false;
}
RuntimeAcceptedPositionExecutionStatus status =
TryExecuteCanonicalPortalPlacementCore(sequence);
switch (status)
{
case RuntimeAcceptedPositionExecutionStatus.Committed:
_placementCommitted = true;
return true;
case RuntimeAcceptedPositionExecutionStatus.DeferredCell:
_awaitingDeferredWake = true;
return false;
default:
return false;
}
}
/// <summary>
/// C4 route 3 (D-T1/D-T2): builds the producer's
/// <see cref="RuntimePortalPlacementAuthority"/> from live transit facts
/// and drives the canonical Runtime portal arm. No new
/// <see cref="WorldRevealCoordinator"/> exposure is needed — the host
/// token is RE-DERIVED through the transit owner's idempotent
/// <c>TryRegisterHostProjection</c> (the same generation+cell returns
/// the token <see cref="WorldRevealCoordinator.TryBeginPortal"/> already
/// registered at Aim time; a stale generation, wrong cell, cancelled, or
/// completed reveal refuses), which makes a superseded token unobtainable
/// by construction.
///
/// <para>
/// The destination itself is <see cref="_pendingDestination"/> — the
/// value <see cref="AimDestination"/> captured at Aim time — and NOT a
/// fresh <c>_transit.TryGetAcceptedTeleportDestination</c> read.
/// <see cref="RuntimeWorldTransitState.TryBeginPortalReveal"/> atomically
/// CONSUMES the transit's one accepted-destination slot the instant Aim
/// claims the reveal generation (it clears
/// <c>_hasAcceptedDestination</c> so a stale destination can never be
/// re-claimed by a later portal) — by Place time that slot is already
/// empty, so re-querying it here always fails. This mirrors why
/// <see cref="_pendingCell"/>/<see cref="_pendingRotation"/>/
/// <see cref="_pendingRevealGeneration"/> are themselves Aim-time
/// snapshots rather than live transit reads.
/// </para>
///
/// <para>
/// A9 review fix (2026-08-05): <paramref name="sequence"/> is
/// <see cref="_pendingDestination"/>'s OWN
/// <see cref="RuntimeTeleportDestination.TeleportSequence"/>, not the
/// transit's separately-tracked <c>ActiveTeleportSequence</c> the caller
/// otherwise threads through — one source for the fact this method's
/// authority carries, asserted equal to the caller's copy so the two
/// can never silently diverge.
/// </para>
/// </summary>
private RuntimeAcceptedPositionExecutionStatus
TryExecuteCanonicalPortalPlacementCore(ushort sequence)
{
System.Diagnostics.Debug.Assert(
!_hasPendingDestination
|| _pendingDestination.TeleportSequence == sequence,
"The transit's active sequence and the Aim-time destination's "
+ "own sequence must never diverge (A9).");
if (!_hasPendingDestination
|| !_transit.TryRegisterHostProjection(
_pendingRevealGeneration,
_pendingCell,
out RuntimeWorldHostProjectionToken hostToken))
{
PhysicsDiagnostics.LogTeleport(
"REFUSED", _pendingCell, "cause=host-token-unavailable");
// R8 residual fix (2026-08-05): same rationale as the
// stale-reveal refusal above — route through
// LogLocalTeleportArrival too, so ACDREAM_PROBE_LOCAL_TELEPORT
// alone is enough to see every App-side refusal cause.
PhysicsDiagnostics.LogLocalTeleportArrival(
cause: "host-token-unavailable",
placementStatus: "Refused",
portalGeneration: _pendingRevealGeneration,
teleportSequence: sequence,
destinationCell: _pendingCell,
resolvedCell: 0u,
hookTailRan: false,
leashArmed: false,
autorunCancelled: false);
return RuntimeAcceptedPositionExecutionStatus.Rejected;
}
RuntimeTeleportDestination destination = _pendingDestination;
var portal = new RuntimePortalPlacementAuthority(
Present: true,
RevealGeneration: _pendingRevealGeneration,
TeleportSequence: destination.TeleportSequence,
Projection: hostToken);
return _acceptedPositionDrive.TryExecuteAcceptedPortalArrival(
destination,
portal);
}
public void ResetSession()
{
ThrowIfDisposed();
@ -695,7 +931,6 @@ internal sealed class LocalPlayerTeleportController
if (!IsCurrentLifetime(generation, sequence))
return false;
Vector3 worldPosition;
if (transition.ChangesStreamingCenter)
{
bool isSealedDungeon = _streaming.IsSealedDungeon(
@ -709,19 +944,24 @@ internal sealed class LocalPlayerTeleportController
isSealedDungeon);
if (!IsCurrentLifetime(generation, sequence))
return false;
worldPosition = new Vector3(
position.Frame.Origin.X,
position.Frame.Origin.Y,
position.Frame.Origin.Z);
}
else
{
worldPosition = translated;
}
// C4 route 3: the App-frame-translated `translated`/`worldPosition`
// vector is no longer carried past this point — the canonical
// portal arm resolves the placement through Runtime's OWN world
// frame (resolveWorldOffsetFromRuntimeFrame: true), using the
// cell-local `destination` Position captured HERE rather than an
// App-translated snapshot (trap T4). It must be captured here and
// NOT re-read from the transit at the Place edge:
// _worldReveal.TryBeginPortal (above) drives
// RuntimeWorldTransitState.TryBeginPortalReveal, which atomically
// CONSUMES the transit's one accepted-destination slot the instant
// it claims this reveal generation — a later
// TryGetAcceptedTeleportDestination call always finds it empty.
_pendingRotation = position.Frame.Orientation;
_pendingPosition = worldPosition;
_pendingCell = position.ObjCellId;
_pendingDestination = destination;
_hasPendingDestination = true;
_holdSeconds = 0f;
PhysicsDiagnostics.LogTeleport(
"AIM",
@ -739,10 +979,13 @@ internal sealed class LocalPlayerTeleportController
{
long generation = checked(++_lifetimeGeneration);
_pendingPosition = default;
_pendingCell = 0u;
_pendingRotation = Quaternion.Identity;
_pendingRevealGeneration = 0;
_pendingDestination = default;
_hasPendingDestination = false;
_placementCommitted = false;
_awaitingDeferredWake = false;
_holdSeconds = 0f;
_streaming.ResetRecenter(clearSession);

View file

@ -102,7 +102,22 @@ internal sealed class RuntimePlacementPresentationSink
projection.Token.Portal,
projection.Token.ExactCellId))
{
return false;
// B2 review fix (2026-08-05): acknowledge-and-ignore, same shape
// as Discard/ExecutorCompleted/WithdrawalRestored above. A Place
// whose portal authority went stale (the transit ended or was
// superseded WHILE a DeferredCell park sat outstanding — the
// residual A1's readiness-hold does not close, since it only
// protects the ORDINARY in-flight case) must not be left
// refused at the FIFO head: RuntimePlacementProjectionSubscription
// .OnPlacement never calls Acknowledge on a `false` return, so a
// refused receipt wedges EVERY later entity's placement receipt
// behind it forever. This runs SYNCHRONOUSLY at publish
// (RuntimeAcceptedPositionDriveController's A2 re-validation, by
// contrast, only runs downstream of a receipt this gate ALREADY
// let through — it cannot protect this path). The canonical
// body already committed via RetryDeferred; there is simply no
// live presentation authority left to apply it to.
return true;
}
if (!_liveEntities.TryApplyRuntimePlacementProjection(in projection))

View file

@ -1171,6 +1171,63 @@ public static class PhysicsDiagnostics
$"[tp-probe] {point,-6} id=0x{id:X8} t={Environment.TickCount64} {extra}"));
}
/// <summary>
/// C4 route 3 D-T8 (2026-08-04 — TEMPORARY, strip with the rest of the
/// physics-probe family once the connected gate is scored). One line per
/// local-player portal-arrival attempt from
/// <c>RuntimeAcceptedPositionDriveController.ReconcileAndAcknowledgePortal</c>
/// — the single Runtime chokepoint both the graphical and headless hosts
/// share, so this is dual-host parity evidence, not per-host guesswork.
/// Initial state from <c>ACDREAM_PROBE_LOCAL_TELEPORT=1</c>.
/// </summary>
public static bool ProbeLocalTeleportEnabled { get; set; } =
Environment.GetEnvironmentVariable("ACDREAM_PROBE_LOCAL_TELEPORT") == "1";
/// <summary>
/// Which host process is running — set once at composition startup by
/// each host's own entry point (<c>SessionPlayerComposition</c> for
/// graphical, <c>HeadlessSessionHost</c> for headless). Runtime itself
/// stays presentation-agnostic (Slice K); this is a diagnostics-only
/// label so <see cref="LogLocalTeleportArrival"/> can report which
/// process produced a given line without threading a host parameter
/// through the drive controller's constructor.
/// </summary>
public static string LocalTeleportHostKind { get; set; } = "graphical";
/// <summary>
/// One <c>[local-tp]</c> line: cause, host, placement status, portal
/// generation/sequence, destination cell, resolved cell, and the three
/// D-T8 booleans confirming the reconcile suffix actually ran
/// (<paramref name="hookTailRan"/> = <c>CommitCanonicalTeleportFrame</c>
/// executed, <paramref name="leashArmed"/> = the constraint leash is
/// armed post-commit, <paramref name="autorunCancelled"/> =
/// <c>CancelAutoRun</c> ran). Self-guards on
/// <see cref="ProbeLocalTeleportEnabled"/>. <paramref name="cause"/> is
/// always <c>"portal"</c> today — ACE's recall/admin teleports arrive as
/// the identical TeleportAdvanced Position and are indistinguishable
/// from a doorway portal at this layer; the parameter exists so a future
/// wire-level cause signal has somewhere to land without a probe
/// signature change.
/// </summary>
public static void LogLocalTeleportArrival(
string cause,
string placementStatus,
long portalGeneration,
ushort teleportSequence,
uint destinationCell,
uint resolvedCell,
bool hookTailRan,
bool leashArmed,
bool autorunCancelled)
{
if (!ProbeLocalTeleportEnabled) return;
string hookTailText = hookTailRan ? "ran" : "skipped";
string leashText = leashArmed ? "armed" : "unarmed";
string autorunText = autorunCancelled ? "cancelled" : "unchanged";
Console.WriteLine(System.FormattableString.Invariant(
$"[local-tp] cause={cause} host={LocalTeleportHostKind} status={placementStatus} gen={portalGeneration} seq={teleportSequence} dest=0x{destinationCell:X8} resolved=0x{resolvedCell:X8} hookTail={hookTailText} leash={leashText} autorun={autorunText}"));
}
/// <summary>
/// A6.P3 issue #98 step-walk investigation (2026-05-23). When true,
/// emits one <c>[step-walk]</c> line at selected points in the transition

View file

@ -97,16 +97,26 @@ internal sealed class HeadlessRuntimePlacementProjectionSink
if (projection.Kind is not RuntimePlacementProjectionKind.Place)
return false;
return record.PositionAuthorityVersion
== token.PositionAuthorityVersion
&& record.SpatialAuthorityVersion
== token.SpatialAuthorityVersion
&& record.PlacementCommitVersion
== token.PlacementCommitVersion
&& record.FullCellId == token.ExactCellId
&& _runtime.TransitOwner.IsCurrentPlacementAuthority(
if (record.PositionAuthorityVersion != token.PositionAuthorityVersion
|| record.SpatialAuthorityVersion != token.SpatialAuthorityVersion
|| record.PlacementCommitVersion != token.PlacementCommitVersion
|| record.FullCellId != token.ExactCellId)
{
return false;
}
if (!_runtime.TransitOwner.IsCurrentPlacementAuthority(
token.Portal,
token.ExactCellId);
token.ExactCellId))
{
// B2 review fix (2026-08-05): acknowledge-and-ignore, the same
// shape and reasoning as the graphical sink's identical fix
// (RuntimePlacementPresentationSink.TryApply) — a stale portal
// authority must not wedge the ordered FIFO for every entity.
return true;
}
return true;
}
private static bool HasValidPortalShape(

View file

@ -3,6 +3,7 @@ using AcDream.Headless.Credentials;
using AcDream.Headless.Diagnostics;
using AcDream.Headless.Policies;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Runtime;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Physics;
@ -143,6 +144,16 @@ internal sealed class HeadlessSessionHost : IDisposable
private RuntimeAcceptedPositionDriveController? _acceptedPositionDrive;
private AcDream.Core.Net.WorldSession? _currentSession;
private HeadlessSessionWorldProjection? _worldProjection;
/// <summary>
/// A1/A3 review fix (2026-08-05): retained so <see cref="Tick"/> can
/// pump <see cref="RuntimeLiveEntitySessionController.PumpPortalCompletion"/>
/// alongside <see cref="_worldProjection"/>'s own
/// <c>PumpFirstEntry</c> — a parked portal placement must retry on the
/// host's own per-tick cadence rather than the completion sequence
/// running unconditionally the instant it is first attempted. Reassigned
/// on every reconnect exactly like <see cref="_worldProjection"/>.
/// </summary>
private RuntimeLiveEntitySessionController? _entities;
/// <summary>C4 route 4b-1 (N3): the exact route <see cref="CreateEventRoute"/>
/// last constructed, so <see cref="Tick"/> can republish the canonical
/// placement FIFO every tick — mirrors the graphical host's per-frame
@ -332,6 +343,12 @@ internal sealed class HeadlessSessionHost : IDisposable
// collision-generation progress and freshly accepted Creates both
// surface here, mirroring the graphical per-frame retry phase.
_worldProjection?.PumpFirstEntry();
// A1/A3 review fix (2026-08-05): retry a parked portal completion
// (see RuntimeLiveEntitySessionController.PumpPortalCompletion) on
// the SAME per-tick cadence, after first-entry so a DeferredCell
// wake first-entry's own pump just resolved is picked up the same
// tick.
_entities?.PumpPortalCompletion();
// C4 route 4b-1 (N3): republish the canonical placement FIFO LAST,
// same order as the graphical host's retry-lease callback (drives
// first, retry last) — a declined Place left at the FIFO head by
@ -621,6 +638,13 @@ internal sealed class HeadlessSessionHost : IDisposable
Radius: 0.48f,
Height: 1.835f,
RuntimeLocalPlayerShadowDisposition.ProvenShapeless));
// D-T8 (temporary probe): labels every subsequent
// PhysicsDiagnostics.LogLocalTeleportArrival line from THIS
// process as headless — Runtime itself has no host-kind concept
// (Slice K keeps it presentation-agnostic), so this is a
// diagnostics-only label set once at composition time, not a
// Runtime dependency.
PhysicsDiagnostics.LocalTeleportHostKind = "headless";
// C4 route 2: one drive controller per host, mirroring
// _firstEntryDrive exactly — same persistent Runtime lifetime,
// collision source, and clock.
@ -633,7 +657,20 @@ internal sealed class HeadlessSessionHost : IDisposable
() => Runtime.PlayerIdentity.ServerGuid,
() => Runtime.MovementOwner.Controller,
() => Runtime.CharacterOwner.UsePositionFromServer,
() => _currentSession);
() => _currentSession,
// C4 route 3: the portal arm's PlayerTeleported port needs
// the J5.4 autorun latch owner, one level above the raw
// controller.
() => Runtime.MovementOwner,
// A2/D-T2.4 review fix (2026-08-05): same wiring as the
// graphical composition (SessionPlayerComposition.cs) — the
// SAME idempotent query TryCompletePortal/PrepareDestination
// themselves use.
isPortalAuthorityCurrent: portal => Runtime.TransitOwner
.CanPlacePortalDestination(
portal.RevealGeneration,
portal.TeleportSequence,
portal.Projection.DestinationCell));
var projection = new HeadlessSessionWorldProjection(
Runtime,
content,
@ -651,6 +688,7 @@ internal sealed class HeadlessSessionHost : IDisposable
Runtime.Generation.Value),
worldProjection,
_acceptedPositionDrive);
_entities = entities;
var route = new LiveSessionEventRouter(
session,
entities.CreateSink(),

View file

@ -8,6 +8,7 @@ using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Physics;
using AcDream.Runtime.Session;
using AcDream.Runtime.World;
namespace AcDream.Headless.Hosting;
@ -585,9 +586,6 @@ internal sealed class HeadlessCollisionNeighborhood
internal sealed class HeadlessSessionWorldProjection
: IRuntimeDirectWorldProjection
{
private const float DefaultRadius = 0.48f;
private const float DefaultHeight = 1.835f;
private readonly GameRuntime _runtime;
private readonly IHeadlessCollisionNeighborhood _collision;
private readonly RuntimeFirstEntryDriveController? _firstEntry;
@ -749,90 +747,228 @@ internal sealed class HeadlessSessionWorldProjection
controller.State = PlayerState.PortalSpace;
}
/// <summary>
/// A1/A3 review fix (2026-08-05): true while a <c>DeferredCell</c> park
/// from a PRIOR call to this method is outstanding for the local
/// player's one possible pending drive operation. Mirrors
/// <c>LocalPlayerTeleportController._awaitingDeferredWake</c> on the
/// graphical side — avoids re-attempting
/// <c>TryExecuteAcceptedPortalArrival</c> while parked (Runtime's own
/// Begin would just refuse a second overlapping attempt with
/// <c>Contention</c>) by polling
/// <see cref="RuntimeAcceptedPositionDriveController.PendingCount"/>
/// instead.
/// </summary>
private bool _awaitingPortalWake;
/// <summary>
/// B3 review fix (2026-08-05): keys <see cref="_awaitingPortalWake"/> to
/// the exact reveal it was armed for. The graphical twin
/// (<c>_awaitingDeferredWake</c>) is naturally reset per teleport via
/// <c>ResetTransit</c>; this class is constructed per SESSION, not per
/// teleport, so without this a stale latch from reveal N could silently
/// steal reveal N+1's <c>PrepareDestination</c> call into polling a park
/// that belongs to a different, already-abandoned reveal — skipping the
/// new reveal's placement attempt entirely.
/// </summary>
private long _awaitingPortalWakeGeneration;
private ushort _awaitingPortalWakeSequence;
/// <summary>
/// N3 review fix (2026-08-05): bounds how many consecutive
/// <c>NotApplicable</c> attempts this host tolerates before treating the
/// condition as unrecoverable. <c>NotApplicable</c> covers hydration-race
/// transients (no canonical body yet, an active initial-Create residence
/// still owning the record) as well as a genuinely stale reveal — unlike
/// <c>Rejected</c>, it is not established to be permanent, and this host
/// must survive K4's 30-session / two-hour endurance profile without a
/// transient becoming fatal.
/// </summary>
private int _notApplicableRetryCount;
private const int NotApplicableRetryBudget = 50;
/// <summary>
/// C4 route 3 (D-T6): the portal-arrival placement runs through the
/// SAME canonical Runtime portal arm the graphical host drives
/// (<see cref="RuntimeAcceptedPositionDriveController.TryExecuteAcceptedPortalArrival"/>),
/// retiring the duplicate Resolve/ResolvePlacement/SetPosition authority
/// this method used to own directly (D2;
/// docs/research/2026-08-04-c4-route-3-contract.md D-T6).
///
/// <para>
/// A1/A3 review fix (2026-08-05): the first pass discarded the arm's
/// returned status entirely (<c>_ = ...</c>) and always reported
/// success, so <c>RuntimeLiveEntitySessionController.TryCompletePortal</c>
/// acknowledged a materialization that never happened on ANY refusal
/// (architecture review A3). This method now:
/// </para>
/// <list type="bullet">
/// <item><description>throws if no drive controller was wired — a
/// composition regression must not silently disable placement, never
/// pretend success (A3's second finding);</description></item>
/// <item><description>does not even ATTEMPT the placement until
/// <see cref="_collision"/> reports the destination resident — this
/// host's narrow collision window makes a premature attempt a
/// guaranteed <c>DeferredCell</c>, and <see cref="_collision"/>'s own
/// readiness IS the signal <see cref="IRuntimeDirectWorldProjection.CenterOnAcceptedForcePosition"/>'s
/// doc names as the precondition for a park to ever resolve;</description></item>
/// <item><description>reports <c>IsCollisionReady: false</c> — never
/// success — for every non-<c>Committed</c> outcome, so the caller's
/// retry loop (<c>RuntimeLiveEntitySessionController.PumpPortalCompletion</c>,
/// A1's headless-side fix) keeps calling this method instead of the
/// completion sequence running against an unplaced body; a genuine
/// <c>DeferredCell</c> is therefore never an error, only a wait — and
/// throws only for the two statuses that mean something is actually
/// wrong (<c>Rejected</c>/<c>NotApplicable</c> — the reveal itself is
/// stale, or the local player has no canonical body, neither of which
/// a headless bot can recover from by waiting).</description></item>
/// </list>
/// </summary>
public RuntimeDestinationReadiness PrepareDestination(
long revealGeneration,
RuntimeTeleportDestination destination)
RuntimeTeleportDestination destination,
RuntimeWorldHostProjectionToken portal)
{
_collision.CenterOn(destination.CellId);
if (_runtime.EntityObjects.Entities.TryGetActive(
destination.EntityGuid,
out RuntimeEntityRecord record))
if (_acceptedPositionDrive is null)
{
ResynchronizeLocalPlayerForPortalArrival(record);
throw new InvalidOperationException(
"Headless portal placement requires a wired "
+ "RuntimeAcceptedPositionDriveController - a composition "
+ "regression must not silently disable placement (A3).");
}
if (_runtime.MovementOwner.Controller is { } controller)
// B3: a latch armed for a DIFFERENT reveal must not be consulted
// for this one — fall through to a fresh attempt below instead.
if (_awaitingPortalWake
&& (_awaitingPortalWakeGeneration != revealGeneration
|| _awaitingPortalWakeSequence != destination.TeleportSequence))
{
_awaitingPortalWake = false;
}
bool committed;
if (_awaitingPortalWake)
{
if (_acceptedPositionDrive.PendingCount != 0)
{
committed = false;
}
else
{
// B1 review fix (2026-08-05): "not pending" is not
// "committed" — the drive's own doc names a merge-time
// Forget (an ordinary ACE broadcast arriving mid-park) as
// the EXPECTED way a park resolves without committing.
// TryConsumePortalCommit is the drive's OWN record of
// whether ITS commit actually happened for this exact
// reveal/sequence, latched only inside a real
// ReconcileAndAcknowledgePortal call — never inferred.
_awaitingPortalWake = false;
committed = _acceptedPositionDrive.TryConsumePortalCommit(
revealGeneration, destination.TeleportSequence);
// A "no" here falls through to committed=false below; the
// NEXT PrepareDestination call re-attempts fresh since
// _awaitingPortalWake is now false and nothing is pending.
}
}
else if (!_collision.IsReady(destination.CellId))
{
committed = false;
}
else
{
var authority = new RuntimePortalPlacementAuthority(
Present: true,
RevealGeneration: revealGeneration,
TeleportSequence: destination.TeleportSequence,
Projection: portal);
RuntimeAcceptedPositionExecutionStatus status =
_acceptedPositionDrive.TryExecuteAcceptedPortalArrival(
destination,
authority);
switch (status)
{
case RuntimeAcceptedPositionExecutionStatus.Committed:
committed = true;
_notApplicableRetryCount = 0;
break;
case RuntimeAcceptedPositionExecutionStatus.DeferredCell:
_awaitingPortalWake = true;
_awaitingPortalWakeGeneration = revealGeneration;
_awaitingPortalWakeSequence = destination.TeleportSequence;
committed = false;
break;
case RuntimeAcceptedPositionExecutionStatus.Contention:
// Transient - some other operation still owns the
// entity's placement token. Retried next pump; never a
// hard error, matching the graphical arm's D-T5
// refusal shape.
committed = false;
break;
case RuntimeAcceptedPositionExecutionStatus.NotApplicable:
// N3 review fix (2026-08-05): NotApplicable covers
// hydration-race transients (record.PhysicsBody is
// null, or an active initial-Create residence still
// owns the record — RuntimeAcceptedPositionDriveController's
// own guard) as well as a genuinely stale reveal; unlike
// Rejected it is not established to be permanent.
// Bounded, loud retry rather than an immediate throw —
// this host must survive K4's 30-session/two-hour
// endurance profile without a transient becoming fatal.
_notApplicableRetryCount++;
PhysicsDiagnostics.LogTeleport(
"REFUSED",
destination.CellId,
$"cause=NotApplicable attempt={_notApplicableRetryCount}");
if (_notApplicableRetryCount > NotApplicableRetryBudget)
{
throw new InvalidOperationException(
"Headless portal placement stayed NotApplicable "
+ $"for {_notApplicableRetryCount} consecutive "
+ "attempts (no canonical body, or an active "
+ "initial-Create residence still owns the "
+ "record) - exceeded the bounded retry budget.");
}
committed = false;
break;
default:
throw new InvalidOperationException(
$"Headless portal placement refused with "
+ $"status={status} even though the destination's "
+ "collision neighborhood reported ready - the "
+ "reveal itself is stale, not recoverable by "
+ "waiting (contract §4 item 5 forbids "
+ "acknowledging a materialization that did not "
+ "happen).");
}
}
if (committed && _runtime.MovementOwner.Controller is { } controller)
controller.State = PlayerState.InWorld;
bool ready = _collision.IsReady(destination.CellId);
bool indoor = (destination.CellId & 0xFFFFu) >= 0x0100u;
return new RuntimeDestinationReadiness(
revealGeneration,
destination.CellId,
indoor,
IsUnhydratable: !ready,
// N2 review fix (2026-08-05): hardcoded false, NOT DERIVED.
// AD-2's "loud unhydratable-placement path" (a claim beyond
// NumCells) is a graphical-only concept today —
// WorldRevealReadinessBarrier's render/composite-texture domains
// have no headless analogue, so there is no local predicate this
// no-window host could evaluate. A genuinely unhydratable
// destination therefore reports IsCollisionReady=false forever
// (via the bounded DeferredCell retry above) rather than taking
// AD-2's loud path — headless does not model unhydratable
// claims. If headless ever gains its own resident-cell-set
// concept, derive the real predicate here instead of leaving
// this hardcoded.
IsUnhydratable: false,
RequiredRenderRadius: indoor ? 0 : 1,
IsRenderNeighborhoodReady: true,
AreCompositeTexturesReady: true,
IsCollisionReady: ready);
}
/// <summary>
/// TODO-C4 (route 3): portal-arrival re-synchronization only. The
/// route-1/8 initial-entry hand-copy (controller construction + first
/// resolve/placement) was deleted at C3c — the first-entry conductor's
/// publication chain owns it — but the portal route is unflipped, so its
/// arrival re-resolve keeps today's exact behavior against the
/// already-published controller until C4 routes it through
/// RuntimePortalPlacementAuthority.
/// </summary>
private void ResynchronizeLocalPlayerForPortalArrival(
RuntimeEntityRecord record)
{
if (record.ServerGuid
!= _runtime.PlayerIdentity.ServerGuid
|| record.Snapshot.Position is not { } position
|| _runtime.MovementOwner.Controller is not { } controller)
{
return;
}
_collision.CenterOn(position.LandblockId);
Vector3 wirePosition = new(
position.PositionX,
position.PositionY,
position.PositionZ);
Quaternion orientation = new(
position.RotationX,
position.RotationY,
position.RotationZ,
position.RotationW);
ResolveResult resolved =
_runtime.EntityObjects.Physics.Engine.Resolve(
wirePosition,
position.LandblockId,
Vector3.Zero,
100f);
ResolveResult placement =
_runtime.EntityObjects.Physics.Engine.ResolvePlacement(
resolved.Position,
resolved.CellId,
DefaultRadius,
DefaultHeight,
controller.StepUpHeight,
controller.StepDownHeight,
ObjectInfoState.IsPlayer
| ObjectInfoState.EdgeSlide,
record.LocalEntityId ?? 0u);
if (placement.Ok)
resolved = placement;
controller.LocalEntityId = record.LocalEntityId ?? 0u;
controller.SetPosition(
resolved.Position,
resolved.CellId,
wirePosition);
controller.SetBodyOrientation(orientation);
IsCollisionReady: committed);
}
}

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,