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>
132 lines
5.4 KiB
C#
132 lines
5.4 KiB
C#
using AcDream.Runtime;
|
|
using AcDream.Runtime.Entities;
|
|
using AcDream.Runtime.Physics;
|
|
using AcDream.Runtime.World;
|
|
|
|
namespace AcDream.Headless.Hosting;
|
|
|
|
/// <summary>
|
|
/// Validation-only no-window observer for canonical Runtime SetPosition
|
|
/// receipts. A headless host has no graphical sidecar to move or hide, so a
|
|
/// valid receipt is acknowledged without re-running placement or mutating
|
|
/// Runtime's body, controller, shadows, clocks, or worksets.
|
|
/// </summary>
|
|
internal sealed class HeadlessRuntimePlacementProjectionSink
|
|
: IRuntimePlacementProjectionSink
|
|
{
|
|
private readonly GameRuntime _runtime;
|
|
|
|
internal HeadlessRuntimePlacementProjectionSink(GameRuntime runtime)
|
|
{
|
|
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
|
}
|
|
|
|
public bool TryApply(
|
|
in RuntimePlacementProjectionSnapshot projection)
|
|
{
|
|
if (projection.Kind is RuntimePlacementProjectionKind.Discard)
|
|
{
|
|
// Discard cancels only an unacknowledged observation. It is valid
|
|
// even after its entity/session authority has been superseded.
|
|
return true;
|
|
}
|
|
|
|
if (projection.Kind is RuntimePlacementProjectionKind.ExecutorCompleted)
|
|
{
|
|
// F1: acknowledge-and-ignore, same as Discard - ExecutorCompleted
|
|
// is not a placement to project (a headless host has no
|
|
// presentation to bind off the completed initial drain; the
|
|
// executor's own drain already committed every canonical fact).
|
|
// It must NOT fall through to the record-lookup gate below: that
|
|
// gate can validly reject an unrelated entity/session mismatch,
|
|
// and this sink's caller (RuntimePlacementProjectionSubscription)
|
|
// treats a false return as "leave at the FIFO head" - a rejected
|
|
// ExecutorCompleted would permanently wedge the entire ordered
|
|
// placement stream behind it.
|
|
return true;
|
|
}
|
|
|
|
if (projection.Kind
|
|
is RuntimePlacementProjectionKind.WithdrawalRestored)
|
|
{
|
|
// Acknowledge-and-ignore for the same reason: the receipt rolls
|
|
// back the PRESENTATION half of a cancelled park's withdrawal, and
|
|
// a headless host has no graphical sidecar, plugin world state,
|
|
// effect poses, or visibility sinks to restore - Runtime already
|
|
// restored every canonical fact before publishing it. Refusing it
|
|
// would wedge the whole ordered stream.
|
|
return true;
|
|
}
|
|
|
|
RuntimePlacementProjectionToken token = projection.Token;
|
|
RuntimeEntityDirectory directory = _runtime.EntityObjects.Entities;
|
|
if (projection.Kind is RuntimePlacementProjectionKind.Place
|
|
or RuntimePlacementProjectionKind.Withdraw
|
|
&& token.IsValid
|
|
&& directory.TryGetByLocalId(
|
|
token.Entity.LocalEntityId,
|
|
out RuntimeEntityRecord residenceCandidate)
|
|
&& directory.IsCurrent(residenceCandidate)
|
|
&& residenceCandidate.Key == token.Entity
|
|
&& _runtime.EntityObjects.TryGetInitialCreateResidence(
|
|
residenceCandidate,
|
|
out _))
|
|
{
|
|
// C3c: a Place/Withdraw for an entity still holding its
|
|
// initial-create residence belongs to the first-entry conductor
|
|
// machinery, which acknowledges its own receipts at the exact
|
|
// FIFO head. Leave it there for the drive pump; validating or
|
|
// acknowledging it here would starve the conductor forever.
|
|
return false;
|
|
}
|
|
if (!token.IsValid
|
|
|| token.SessionLifetimeVersion
|
|
!= directory.SessionLifetimeVersion
|
|
|| !directory.TryGetByLocalId(
|
|
token.Entity.LocalEntityId,
|
|
out RuntimeEntityRecord record)
|
|
|| !directory.IsCurrent(record)
|
|
|| record.Key != token.Entity
|
|
|| !HasValidPortalShape(token))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (projection.Kind is RuntimePlacementProjectionKind.Withdraw)
|
|
return true;
|
|
if (projection.Kind is not RuntimePlacementProjectionKind.Place)
|
|
return false;
|
|
|
|
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))
|
|
{
|
|
// 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(
|
|
in RuntimePlacementProjectionToken token)
|
|
{
|
|
RuntimePortalPlacementAuthority portal = token.Portal;
|
|
if (!portal.Present)
|
|
return portal.IsEmpty;
|
|
|
|
return portal.IsValid
|
|
&& portal.Projection.DestinationCell == token.ExactCellId;
|
|
}
|
|
}
|