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:
parent
cd3129e9d6
commit
e0f96a55bf
24 changed files with 5261 additions and 243 deletions
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue