fix(runtime): transient collision-seal failure no longer terminal for login (#357)

Login could hang forever at reveal ready=True with the world never
revealed: UI and sky drawn, geometry absent, client healthy. The player's
first-entry conductor was being terminally dropped by a TRANSIENT
condition.

Mechanism, pinned by probes: the C3c-F2 rearm guard validates the exact
destination cell's prefix admissibility before moving the dormant lease
out of AwaitingCell, but the placement transaction's ring search touches
NEIGHBOUR landblocks and TrySealCollisionEvaluationAuthority covers every
touched prefix. A hard login recenter admits nine landblocks at once, so
a rearm taken while a neighbour's admission was still registered passed
the guard and failed the seal. The operation was left in
AwaitingPreparation, IsDormantLocalActivationAwaitingCell went false, and
EvaluateActivation had no way to say 'retry' - it fell through to
RejectedAuthority, which RuntimeFirstEntryDriveController treats as
terminal. The local player left the pump (pending=0), the movement
controller never published, auto-entry never fired, the reveal never
completed. Timing-flipped: the same binary worked when the rearm landed
outside a neighbour's admission window, then lost that race consistently.

Fix is classification, not state: EvaluateActivation reports DeferredCell
when the abort happens while the dormant lease is still current
(IsDormantLocalActivationLeaseCurrent), so the conductor keeps retrying.
The operation deliberately stays in AwaitingPreparation - the retry
re-runs the full evaluation against fresh state, which is the recovery
contract the publication-state tests already pin (the SAME token
evaluates Evaluated once the authority settles). Genuine discards still
report RejectedAuthority. A first attempt that re-parked the lease to
AwaitingCell was rejected by the test matrix: recovery would then need
the rearm gate, which is stricter than the seal, and the
reentrant-restriction-mutation recoveries hung in DeferredCell.

Seven publication-state tests move their transient-abort assertion from
RejectedAuthority to DeferredCell; the two genuinely-terminal tests
(lease retired) are unchanged. The [wake]/[rearm]/[pump] probes that
pinned the mechanism stay behind ACDREAM_PROBE_PARK=1 with the rest of
the C4 family.

Exonerated by experiment before the fix: ACE (wire capture shows
PlayerCreate sent; retail logs in fine) and the portal-cue commit
2914e43a (full revert stalled identically).

Verified: 2/2 live logins reach auto-entered player mode and reveal
event=complete, with the probe showing seal-refused -> retry -> recovery
in flight; full Release suite 11,740 passed / 4 skipped / 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-09 12:32:53 +02:00
parent b80ba797cf
commit 78b981cca0
5 changed files with 177 additions and 12 deletions

View file

@ -510,6 +510,28 @@ internal sealed class RuntimeLocalPlayerPhysicsPublicationState : IDisposable
{
return RuntimeLocalPlayerPhysicsActivationStatus.DeferredCell;
}
// #357: an evaluation abort while the dormant lease is STILL
// CURRENT is transient — the collision-authority seal was refused
// by a still-registered admission (a login recenter admits nine
// landblocks at once and the placement's ring search can touch a
// neighbour mid-admission) or a reentrant collision/restriction
// mutation observed mid-transaction. The operation remains in
// AwaitingPreparation and the SAME token evaluates successfully
// once the authority settles, so the correct status is
// DeferredCell (retry). Falling through to RejectedAuthority here
// is what terminally dropped the local player from the
// first-entry conductor and hung login at ready=True with the
// world never revealed.
if (ReferenceEquals(_activation, activation)
&& IsActivationCurrent(activation)
&& _physics.SetPosition.IsDormantLocalActivationLeaseCurrent(
activation.Record,
activation.Body,
token.Placement,
activation.PlacementCommand))
{
return RuntimeLocalPlayerPhysicsActivationStatus.DeferredCell;
}
if (ReferenceEquals(_activation, activation)
&& !IsActivationCurrent(activation))
{

View file

@ -2067,6 +2067,24 @@ internal sealed class RuntimeSetPositionState : IDisposable
objectTableAuthority,
out RuntimeCollisionEvaluationAuthority collisionAuthority))
{
// #357: a failed seal is a TRANSIENT abort — an authority moved
// between the evaluation snapshot and the seal (a still-registered
// neighbour admission during a login recenter, a reentrant
// collision/restriction mutation observed mid-transaction). The
// operation is deliberately left in AwaitingPreparation: the next
// EvaluateActivation call re-runs the full placement against fresh
// state, which is the recovery path the publication-state tests
// pin. The retryable-vs-terminal CLASSIFICATION happens in
// RuntimeLocalPlayerPhysicsPublicationState.EvaluateActivation,
// which reports DeferredCell while this lease is still current —
// before #357 it could only report RejectedAuthority here, which
// the first-entry conductor treats as terminal, dropping the
// local player and hanging login at ready=True forever.
if (Core.Physics.PhysicsDiagnostics.ProbeParkEnabled)
{
Console.WriteLine(FormattableString.Invariant(
$"[rearm] guid=0x{record.ServerGuid:X8} seal-refused (transient; lease retained)"));
}
return false;
}
@ -2170,13 +2188,36 @@ internal sealed class RuntimeSetPositionState : IDisposable
in RuntimeEntityPlacementToken token,
in RuntimeSetPositionCommand command)
{
if (!IsExactDormantLocalActivationCurrent(
record,
body,
token,
command,
out Operation? operation,
allowDeferredLease: true)
bool current = IsExactDormantLocalActivationCurrent(
record,
body,
token,
command,
out Operation? operation,
allowDeferredLease: true);
if (Core.Physics.PhysicsDiagnostics.ProbeParkEnabled)
{
string why = !current || operation is null
? "not-current"
: operation.Stage is not RuntimeEntityPlacementStage.AwaitingCell
? $"stage={operation.Stage}"
: !operation.DormantLocalActivation ? "not-dormant"
: !operation.WakeableLostCell ? "not-wakeable"
: !operation.CollisionGenerationReady ? "gen-not-ready"
: operation.ProjectionSequence != 0UL ? "proj-seq"
: operation.CollisionGeneration != _physics
.CollisionGenerationAuthority(operation.ExactCellId)
? $"gen-mismatch({operation.CollisionGeneration}!={_physics.CollisionGenerationAuthority(operation.ExactCellId)})"
: !_physics.Engine.IsSpawnCellReady(operation.ExactCellId)
? "spawn-not-ready"
: !_physics.IsCollisionEvaluationPrefixAdmissible(
operation.ExactCellId)
? "prefix-inadmissible"
: "OK";
Console.WriteLine(FormattableString.Invariant(
$"[rearm] guid=0x{record.ServerGuid:X8} verdict={why}"));
}
if (!current
|| operation is null
|| operation.Stage is not RuntimeEntityPlacementStage.AwaitingCell
|| !operation.DormantLocalActivation
@ -4281,6 +4322,11 @@ internal sealed class RuntimeSetPositionState : IDisposable
EnsureNotDisposed();
if (generation == 0UL)
throw new ArgumentOutOfRangeException(nameof(generation));
if (Core.Physics.PhysicsDiagnostics.ProbeParkEnabled)
{
Console.WriteLine(FormattableString.Invariant(
$"[wake] begin lb=0x{landblockId:X8} gen={generation} unboundCells={_unboundDeferredCellOrder.Count} buckets={_deferredBucketOrder.Count}"));
}
uint prefix = landblockId & 0xFFFF0000u;
for (int index = 0; index < _unboundDeferredCellOrder.Count;)
{
@ -4352,6 +4398,11 @@ internal sealed class RuntimeSetPositionState : IDisposable
internal void CancelCollisionGeneration(uint landblockId, ulong generation)
{
EnsureNotDisposed();
if (Core.Physics.PhysicsDiagnostics.ProbeParkEnabled)
{
Console.WriteLine(FormattableString.Invariant(
$"[wake] cancel lb=0x{landblockId:X8} gen={generation} buckets={_deferredBucketOrder.Count}"));
}
if (_deferredBucketOrder.Count == 0)
return;
uint prefix = landblockId & 0xFFFF0000u;
@ -4371,6 +4422,11 @@ internal sealed class RuntimeSetPositionState : IDisposable
bool ready)
{
EnsureNotDisposed();
if (Core.Physics.PhysicsDiagnostics.ProbeParkEnabled)
{
Console.WriteLine(FormattableString.Invariant(
$"[wake] commit lb=0x{landblockId:X8} gen={generation} ready={ready} buckets={_deferredBucketOrder.Count}"));
}
if (!ready || _deferredBucketOrder.Count == 0)
return;
@ -4396,9 +4452,19 @@ internal sealed class RuntimeSetPositionState : IDisposable
RuntimeEntityKey[] exact = indexed.ToArray();
if (!_physics.Engine.IsSpawnCellReady(cell.CellId))
{
if (Core.Physics.PhysicsDiagnostics.ProbeParkEnabled)
{
Console.WriteLine(FormattableString.Invariant(
$"[wake] STRAND cell=0x{cell.CellId:X8} gen={cell.CollisionGeneration} spawnReady=false ops={exact.Length} -> unbound"));
}
UnbindDeferredBucket(cell);
continue;
}
if (Core.Physics.PhysicsDiagnostics.ProbeParkEnabled)
{
Console.WriteLine(FormattableString.Invariant(
$"[wake] wake cell=0x{cell.CellId:X8} gen={cell.CollisionGeneration} ops={exact.Length}"));
}
RemoveDeferredBucket(cell);
// Round 3 audit: safe - `exact` snapshots KEYS (RuntimeEntityKey
// values), never Operation references, so nothing here can go
@ -4425,6 +4491,11 @@ internal sealed class RuntimeSetPositionState : IDisposable
continue;
}
operation.CollisionGenerationReady = true;
if (Core.Physics.PhysicsDiagnostics.ProbeParkEnabled)
{
Console.WriteLine(FormattableString.Invariant(
$"[wake] op guid=0x{operation.Record.ServerGuid:X8} dormant={operation.DormantLocalActivation} ack={operation.WithdrawalAcknowledged} stage={operation.Stage} reqPrep={operation.RequiresPreparation} projSeq={operation.ProjectionSequence}"));
}
if (operation.WithdrawalAcknowledged)
RetryDeferred(operation);
}

View file

@ -125,8 +125,16 @@ internal sealed class RuntimeFirstEntryDriveController
/// synchronous callbacks reaching a host pump) fail closed into the next
/// outer pump instead of interleaving.
/// </summary>
private long _driveAllCalls;
internal void DriveAll()
{
if (Core.Physics.PhysicsDiagnostics.ProbeParkEnabled
&& (++_driveAllCalls <= 5 || _driveAllCalls % 300 == 0))
{
Console.WriteLine(FormattableString.Invariant(
$"[pump] DriveAll #{_driveAllCalls} pending={_pending.Count}"));
}
if (_driving || _pending.Count == 0)
return;
_driving = true;