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
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue