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

@ -65,7 +65,7 @@ reconciling #281's 43 test failures.
gate may or may not exclude Create during that window, and #280 says other
work does continue arriving through it. Two owners of one fact; the campaign
answer is that Runtime owns the frame and App projects it. Route-3 adjacent.
- **#284OPEN — a placement that cannot resolve parks forever with no
- **#284DONE (2026-08-03) — a placement that cannot resolve parks forever with no
diagnostic.** A first-entry placement whose world frame is absent returns
`RetrySetupUnavailable` (`RuntimeSetPositionState.PrepareMover:1535-1543`)
and is re-Advanced every pump indefinitely. Nothing counts it, names its
@ -77,6 +77,23 @@ reconciling #281's 43 test failures.
exception pattern established in `01f4791e`. Doing this FIRST makes #282,
#283, and every C4 route cheaper to diagnose and lets the connected gates
fail on nonzero parked entries.
**Landed 2026-08-03 (S1).** `RetryWorldFrameUnavailable` splits the
world-frame park from the Setup park, so a missing frame stops reporting
itself as an asset problem; call sites now test `IsRetryable()` instead of
one reason, so a future reason cannot be silently demoted to a rejection.
The operation retains its `RuntimeSetPositionParkReason`, and
`RuntimeSetPositionOwnershipSnapshot` reports
`ParkedAwaitingSetupCollisionCount` / `ParkedAwaitingWorldFrameCount` /
`ParkedPlacementCount`. `ObserveLocalPlayerCreate` records the accepted
local-player Create even when it carries no landblock, and
`ThrowIfWorldFrameUnreachable` makes that contradiction terminal instead of
an infinite silent retry.
**Deliberately NOT folded into `IsConverged`:** #277 documents a far Create
legitimately parking for the whole session, so a parked entry at teardown is
not automatically a defect. The counts are exposed for gate assertions at
stable checkpoints; wiring them into the connected gates' `report.json` is
carried with #277's service-window conversion, where "legitimately parked"
becomes precisely definable.
## C3c placement cutover — 2026-08-02

View file

@ -556,17 +556,17 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
Func<RuntimeEntityRecord, Exception?>? retirePriorProjection)
{
EnsureNotDisposed();
if (beginInitialResidence
&& isLocalPlayer
&& (incoming.Physics?.Position ?? incoming.Position)
is { LandblockId: not 0u } initialPlayerPosition)
if (beginInitialResidence && isLocalPlayer)
{
// The accepted local Create establishes the shared world frame
// before any remote first-entry conductor converts its authored
// landblock-local coordinates.
Physics.ObserveLocalWorldFrame(
initialPlayerPosition.LandblockId,
teleportAdvanced: false);
// landblock-local coordinates. #284: observe the Create itself
// even when it carries no usable landblock - that is precisely
// the case in which no frame is ever published, and every remote
// placement would otherwise park forever without a diagnostic.
Physics.ObserveLocalPlayerCreate(
(incoming.Physics?.Position ?? incoming.Position)
?.LandblockId ?? 0u);
}
if (_sessionClearInProgress)
{

View file

@ -348,8 +348,10 @@ internal sealed class RuntimeRemoteFirstEntryState
gameTime,
out RuntimeSetPositionCommand command,
resolveWorldOffsetFromRuntimeFrame: true);
if (moverStatus
== RuntimeSetPositionMoverPreparationStatus.RetrySetupUnavailable)
// #284: every retryable reason resumes on a later pump. Comparing
// against one reason would silently reclassify a new one as a
// hard rejection.
if (moverStatus.IsRetryable())
{
return RuntimeRemoteFirstEntryStatus.AwaitingCollisionSource;
}

View file

@ -364,8 +364,10 @@ internal sealed class RuntimeLocalPlayerFirstEntryState
collisionSource,
gameTime,
out RuntimeSetPositionCommand command);
if (moverStatus
== RuntimeSetPositionMoverPreparationStatus.RetrySetupUnavailable)
// #284: every retryable reason resumes on a later pump. Comparing
// against one reason would silently reclassify a new one as a
// hard rejection.
if (moverStatus.IsRetryable())
{
return RuntimeLocalPlayerFirstEntryStatus
.AwaitingCollisionSource;

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;

View file

@ -9,11 +9,72 @@ namespace AcDream.Runtime.Physics;
internal enum RuntimeSetPositionMoverPreparationStatus
{
Prepared,
/// <summary>
/// The authored Setup collision payload has not been read yet. Resolves
/// when the prepared-asset read completes.
/// </summary>
RetrySetupUnavailable,
/// <summary>
/// #284: Runtime's world frame is not published yet, so a landblock-local
/// Create origin cannot be converted
/// (<see cref="RuntimePhysicsState.TryGetWorldFrameOffset"/>). Previously
/// this reported itself as <see cref="RetrySetupUnavailable"/>, which sent
/// anyone diagnosing a parked placement to the asset pipeline instead of
/// the missing local-player Create that actually publishes the frame.
/// Retryable exactly like Setup - only the reason differs.
/// </summary>
RetryWorldFrameUnavailable,
RejectedAuthority,
InvalidData,
}
internal static class RuntimeSetPositionMoverPreparationStatusExtensions
{
/// <summary>
/// True for the statuses whose work can still succeed on a later pump.
/// Call sites must use this rather than comparing against one reason, so
/// a newly added retry reason cannot be silently treated as a rejection.
/// </summary>
internal static bool IsRetryable(
this RuntimeSetPositionMoverPreparationStatus status) =>
status is RuntimeSetPositionMoverPreparationStatus
.RetrySetupUnavailable
or RuntimeSetPositionMoverPreparationStatus
.RetryWorldFrameUnavailable;
/// <summary>
/// The parked-placement reason a retryable status contributes to the
/// ownership ledger, or <see cref="RuntimeSetPositionParkReason.None"/>
/// when the status is not a park.
/// </summary>
internal static RuntimeSetPositionParkReason ParkReason(
this RuntimeSetPositionMoverPreparationStatus status) =>
status switch
{
RuntimeSetPositionMoverPreparationStatus.RetrySetupUnavailable =>
RuntimeSetPositionParkReason.AwaitingSetupCollision,
RuntimeSetPositionMoverPreparationStatus
.RetryWorldFrameUnavailable =>
RuntimeSetPositionParkReason.AwaitingWorldFrame,
_ => RuntimeSetPositionParkReason.None,
};
}
/// <summary>
/// #284: why a first-entry placement is currently parked. Retained on the
/// operation so <c>CaptureOwnership</c> can report parked work by cause
/// instead of leaving it invisible until a downstream symptom appears.
/// </summary>
internal enum RuntimeSetPositionParkReason
{
None,
AwaitingSetupCollision,
AwaitingWorldFrame,
}
/// <summary>
/// Distinguishes a Setup payload which has not arrived yet from a completed
/// lookup whose result is absent. Retail supplies its dummy placement sphere

View file

@ -273,8 +273,18 @@ internal readonly record struct RuntimeSetPositionOwnershipSnapshot(
int AcknowledgedPlacementCompletionCount,
int CollisionPrefixQuiescenceCount,
int PendingQuiescenceProjectionCount,
int PooledOperationCount)
int PooledOperationCount,
int ParkedAwaitingSetupCollisionCount = 0,
int ParkedAwaitingWorldFrameCount = 0)
{
/// <summary>
/// #284: placements that could not be prepared on their last attempt and
/// are waiting for a later pump, by cause. Non-zero at a stable
/// checkpoint means work is stuck; the reason names where to look.
/// </summary>
internal int ParkedPlacementCount =>
ParkedAwaitingSetupCollisionCount + ParkedAwaitingWorldFrameCount;
internal bool IndexesConsistent =>
LostDeadlineCount == LostDeadlineNodeCount
&& LostDeadlineCount == LostDeadlineIndexCount
@ -399,6 +409,13 @@ internal sealed class RuntimeSetPositionState : IDisposable
internal bool InheritedLostDeadline { get; set; }
internal bool EnteringWorldFromCelllessResidence { get; set; }
internal bool DormantLocalActivation { get; set; }
/// <summary>
/// #284: why this operation's mover preparation is currently parked,
/// or <see cref="RuntimeSetPositionParkReason.None"/> when it is not.
/// Observability only - it never gates work.
/// </summary>
internal RuntimeSetPositionParkReason ParkReason { get; set; }
internal RuntimeSetPositionCommand? PreparedCommandAwaitingWithdrawalAck
{
get;
@ -464,6 +481,7 @@ internal sealed class RuntimeSetPositionState : IDisposable
InheritedLostDeadline = false;
EnteringWorldFromCelllessResidence = false;
DormantLocalActivation = false;
ParkReason = RuntimeSetPositionParkReason.None;
PreparedCommandAwaitingWithdrawalAck = null;
InPool = false;
}
@ -726,6 +744,8 @@ internal sealed class RuntimeSetPositionState : IDisposable
{
int deferred = 0;
int awaitingPreparation = 0;
int parkedAwaitingSetup = 0;
int parkedAwaitingWorldFrame = 0;
foreach (Operation operation in _operations.Values)
{
if (operation.Stage
@ -735,6 +755,15 @@ internal sealed class RuntimeSetPositionState : IDisposable
}
if (operation.WakeableLostCell)
deferred++;
switch (operation.ParkReason)
{
case RuntimeSetPositionParkReason.AwaitingSetupCollision:
parkedAwaitingSetup++;
break;
case RuntimeSetPositionParkReason.AwaitingWorldFrame:
parkedAwaitingWorldFrame++;
break;
}
}
int pendingQuiescenceProjections = 0;
foreach (CollisionPrefixQuiescence quiescence
@ -764,7 +793,9 @@ internal sealed class RuntimeSetPositionState : IDisposable
_acknowledgedPlacementCompletions.Count,
_collisionPrefixQuiescence.Count,
pendingQuiescenceProjections,
_operationPool.Count);
_operationPool.Count,
parkedAwaitingSetup,
parkedAwaitingWorldFrame);
}
internal int PendingProjectionCount => _pendingProjection.Count;
@ -1518,8 +1549,10 @@ internal sealed class RuntimeSetPositionState : IDisposable
if (!preparation.Setup.IsResolved)
{
return RuntimeSetPositionMoverPreparationStatus
.RetrySetupUnavailable;
return Park(
operation,
RuntimeSetPositionMoverPreparationStatus
.RetrySetupUnavailable);
}
RuntimeSetPositionMoverPreparation effectivePreparation = preparation;
@ -1537,8 +1570,19 @@ internal sealed class RuntimeSetPositionState : IDisposable
out float worldOffsetX,
out float worldOffsetY))
{
return RuntimeSetPositionMoverPreparationStatus
.RetrySetupUnavailable;
// #284: the frame is published once, by the accepted
// local-player Create, and is never withdrawn inside a
// session. If that Create has already been observed, waiting
// is not a wait - no later pump can ever supply the frame, so
// parking here would retry forever in silence. Surface the
// contradiction instead, exactly as 01f4791e made a violated
// receipt-ledger invariant terminal rather than resumable.
_physics.ThrowIfWorldFrameUnreachable(
authority.AcceptedPosition.LandblockId);
return Park(
operation,
RuntimeSetPositionMoverPreparationStatus
.RetryWorldFrameUnavailable);
}
effectivePreparation = preparation with
@ -1569,9 +1613,24 @@ internal sealed class RuntimeSetPositionState : IDisposable
PreparedCommand = command,
};
operation.ParkReason = RuntimeSetPositionParkReason.None;
return RuntimeSetPositionMoverPreparationStatus.Prepared;
}
/// <summary>
/// #284: records why this operation could not be prepared, so parked work
/// is visible in <see cref="CaptureOwnership"/> by cause rather than only
/// as a downstream symptom. Retains the reason on the operation until the
/// preparation succeeds or the operation is retired.
/// </summary>
private static RuntimeSetPositionMoverPreparationStatus Park(
Operation operation,
RuntimeSetPositionMoverPreparationStatus status)
{
operation.ParkReason = status.ParkReason();
return status;
}
/// <summary>
/// C0-3: chains the exact-Setup mover pipeline end-to-end for an
/// authored placement (initial-Create or any other authored-mover

View file

@ -2404,6 +2404,7 @@ public sealed class RuntimeSetPositionStateTests
"InheritedLostDeadline",
"EnteringWorldFromCelllessResidence",
"DormantLocalActivation",
"ParkReason",
"PreparedCommandAwaitingWithdrawalAck",
"InPool",
];

View file

@ -3,6 +3,7 @@ using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Physics;
namespace AcDream.Runtime.Tests.Physics;
@ -148,6 +149,80 @@ public sealed class RuntimeWorldFrameTests
Assert.Equal(-192f, sourceX);
}
/// <summary>
/// #284: waiting for the world frame is legitimate only while the
/// local-player Create is still outstanding. The frame is published once
/// per session from that Create, so once it has been accepted WITHOUT
/// publishing one - a local-player CreateObject carrying no landblock -
/// no later pump can ever supply it, and every remote placement would
/// retry forever in silence. That is a violated invariant, not a wait.
/// </summary>
[Fact]
public void ALocalPlayerCreateWithNoLandblock_MakesTheFrameUnreachable()
{
using var lifetime = new RuntimeEntityObjectLifetime();
BindGeneration(lifetime);
// Before the local player is seen at all, waiting is legitimate.
lifetime.Physics.ThrowIfWorldFrameUnreachable(CenterCell);
lifetime.Physics.ObserveLocalPlayerCreate(0u);
InvalidOperationException error =
Assert.Throws<InvalidOperationException>(() =>
lifetime.Physics.ThrowIfWorldFrameUnreachable(CenterCell));
Assert.Contains("world frame is unreachable", error.Message);
}
[Fact]
public void AnAcceptedLocalPlayerCreate_LeavesTheFrameReachable()
{
using var lifetime = new RuntimeEntityObjectLifetime();
BindGeneration(lifetime);
lifetime.Physics.ObserveLocalPlayerCreate(CenterCell);
// The frame exists, so nothing is unreachable and nothing throws.
lifetime.Physics.ThrowIfWorldFrameUnreachable(CenterCell);
Assert.True(lifetime.Physics.TryGetWorldFrameOffset(
CenterCell,
out _,
out _));
}
/// <summary>
/// #284: a missing world frame used to report itself as
/// RetrySetupUnavailable, sending anyone diagnosing a parked placement to
/// the asset pipeline rather than the absent local-player Create. Both
/// remain retryable - only the reported cause differs.
/// </summary>
[Fact]
public void ParkReasons_AreDistinctAndBothRetryable()
{
Assert.True(RuntimeSetPositionMoverPreparationStatus
.RetrySetupUnavailable.IsRetryable());
Assert.True(RuntimeSetPositionMoverPreparationStatus
.RetryWorldFrameUnavailable.IsRetryable());
Assert.False(RuntimeSetPositionMoverPreparationStatus
.RejectedAuthority.IsRetryable());
Assert.False(RuntimeSetPositionMoverPreparationStatus
.InvalidData.IsRetryable());
Assert.False(RuntimeSetPositionMoverPreparationStatus
.Prepared.IsRetryable());
Assert.Equal(
RuntimeSetPositionParkReason.AwaitingSetupCollision,
RuntimeSetPositionMoverPreparationStatus.RetrySetupUnavailable
.ParkReason());
Assert.Equal(
RuntimeSetPositionParkReason.AwaitingWorldFrame,
RuntimeSetPositionMoverPreparationStatus.RetryWorldFrameUnavailable
.ParkReason());
Assert.Equal(
RuntimeSetPositionParkReason.None,
RuntimeSetPositionMoverPreparationStatus.Prepared.ParkReason());
}
[Fact]
public void AZeroCellIdNeitherPublishesNorResolves()
{