fix(runtime): name why a placement is parked and fail closed when it cannot resolve

Fixes #284 (plan S1).

A first-entry placement that could not be prepared returned
RetrySetupUnavailable and was re-Advanced every pump forever. Nothing counted
it, nothing named its cause, and nothing distinguished "waiting for something
that will arrive" from "waiting for something that never can". That is why
#281's 43 test failures presented as four unrelated symptoms across App and
Runtime instead of one cause, and why a stuck entity in the live client simply
never appears with no log line to follow.

Worse, the two causes were conflated: 670f307c's missing-world-frame park
reported itself as RetrySetupUnavailable, sending anyone diagnosing it to the
prepared-asset pipeline rather than to the absent local-player Create that
actually publishes the frame.

- RetryWorldFrameUnavailable splits the two causes. Call sites now ask
  IsRetryable() instead of comparing against one reason, so a future retry
  reason cannot be silently reclassified as a hard rejection - the exact way
  this class of bug hides.
- The operation retains its RuntimeSetPositionParkReason, and
  RuntimeSetPositionOwnershipSnapshot reports parked work by cause
  (ParkedAwaitingSetupCollisionCount / ParkedAwaitingWorldFrameCount /
  ParkedPlacementCount), so parked placements appear wherever ledgers are
  already asserted.
- ObserveLocalPlayerCreate records the accepted local-player Create even when
  it carries no landblock - precisely the case where no frame is ever
  published - and ThrowIfWorldFrameUnreachable makes that contradiction
  terminal. Waiting is legitimate only while that Create is outstanding; after
  it, no later pump can supply the frame. Same shape as 01f4791e, which made a
  violated receipt-ledger invariant terminal rather than resumable.

This is observability plus fail-fast. There is no timeout, no retry cap, and
no grace period anywhere in it; retryable work still retries exactly as before
and no placement behaviour changed.

The parked counts are deliberately NOT folded into IsConverged: #277 documents
a far Create legitimately parking for a whole session, so a parked entry at
teardown is not automatically a defect. Wiring them into the connected gates
is carried with #277's service-window conversion, where "legitimately parked"
becomes definable.

Runtime 1,012/1,012. Complete Release solution: 10,834 passed / 4 skipped /
0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-03 13:55:16 +02:00
parent 95ebc03af4
commit 97d11e6c7f
9 changed files with 273 additions and 19 deletions

View file

@ -449,6 +449,7 @@ public sealed class RuntimePhysicsState : IDisposable
private long _nextCollisionPreparationSequence;
private ulong _collisionWorldAuthority = 1UL;
private uint _worldFrameCenterLandblockId;
private bool _localPlayerCreateObserved;
private readonly List<Action<RuntimeCollisionGenerationCommitted>>
_collisionGenerationCommittedObservers = new();
private bool _disposed;
@ -545,6 +546,40 @@ public sealed class RuntimePhysicsState : IDisposable
}
}
/// <summary>
/// #284: records that this session's local-player Create was accepted,
/// independently of whether it carried a usable landblock. The frame is
/// published exactly once per session from that Create, so after it has
/// been observed a still-absent frame can never be supplied by a later
/// pump - see <see cref="ThrowIfWorldFrameUnreachable"/>.
/// </summary>
internal void ObserveLocalPlayerCreate(uint fullCellId)
{
EnsureNotDisposed();
_localPlayerCreateObserved = true;
ObserveLocalWorldFrame(fullCellId, teleportAdvanced: false);
}
/// <summary>
/// #284: converts an unresolvable world-frame wait into a loud failure.
/// Parking is legitimate only while the local-player Create is still
/// outstanding; once it has been accepted without publishing a frame,
/// every remote placement would retry forever in silence and the entities
/// would simply never appear. Terminal by design - this is a violated
/// invariant, not resumable work.
/// </summary>
internal void ThrowIfWorldFrameUnreachable(uint fullCellId)
{
if (!_localPlayerCreateObserved || _worldFrameCenterLandblockId != 0u)
return;
throw new InvalidOperationException(
"Runtime's world frame is unreachable: the local-player Create "
+ "was accepted without publishing a frame, so the placement for "
+ $"landblock 0x{fullCellId & 0xFFFF0000u:X8} can never resolve. "
+ "A local-player CreateObject must carry a non-zero landblock.");
}
/// <summary>
/// Converts a retail landblock-local network frame into the Runtime's
/// current world frame without consulting presentation or waiting for
@ -1363,6 +1398,7 @@ public sealed class RuntimePhysicsState : IDisposable
SetPosition.ResetSession();
CollisionReports.ResetSession();
_worldFrameCenterLandblockId = 0u;
_localPlayerCreateObserved = false;
AdvanceCollisionWorldAuthority();
Volatile.Write(ref _collisionMutationThreadId, 0);
}
@ -2056,6 +2092,7 @@ public sealed class RuntimePhysicsState : IDisposable
_collisionAdmissions.Clear();
_collisionGenerations.Clear();
_worldFrameCenterLandblockId = 0u;
_localPlayerCreateObserved = false;
CellCommitted = null;
_collisionGenerationCommittedObservers.Clear();
_disposed = true;