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