feat(runtime): bridge executor completion to the placement stream

Cutover slice C0 (docs/plans/2026-08-02-placement-cutover.md): the seam
work that lets C3 flip hosts onto a complete receipt stream instead of
growing one mid-cutover. The executor's Released exit now publishes an
acknowledge-only ExecutorCompleted receipt through the one placement
projection stream — registered before observer dispatch, correlated to
the full execution receipt, reaped exactly once on acknowledgement/
discard/session-clear, and counted in the convergence ledger. All three
production placement sinks acknowledge-and-ignore the new kind via early
returns proven behavior-preserving for every existing kind; without them
the first such receipt at cutover would permanently wedge the exact-head
FIFO behind sinks that return false. Provably inert today: the publisher
has no production caller.

Execute's live inputs now derive from Runtime's own owners bound at
GameRuntime construction: UsePositionFromServer is retail's exact
autonomy_level != 2 (CommandInterpreter::UsePositionFromServer
0x006B3B40, startup-only knob), and PlayerDistance uses the live movement
controller's position with a null-safe fallback to the caller struct —
never a fabricated origin. TryPrepareAndSubmitAuthoredPlacement chains
the prepared-collision Setup read through PrepareMover to submission with
zero validation-semantics changes. TryCommitParent and CommitWithdrawal
gain the sibling cancellation flow (residence + ordinary placement
family); TryCommitParent deliberately omits LeaveWorld — retail's
set_parent performs its single gated leave_world (0x00515A90) and a
second would have no counterpart.

Not fully dormant: the two cancellation fixes change Runtime paths
production already calls (today as no-op-adjacent hardening, since
nothing upstream begins a residence yet); everything else is reachable
only by tests. Reviewed: retail-conformance PASS + architecture/
adversarial PASS after one fix round (sink wedge, completion-receipt
lifecycle, null-controller distance). Runtime 921/921; complete Release
solution 10,716 passed / 4 intentional skips.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-02 05:22:37 +02:00
parent 27e05b99e4
commit 67f63e85e5
14 changed files with 1639 additions and 11 deletions

View file

@ -274,7 +274,26 @@ internal sealed class RuntimeInitialCreateContinuationExecutor
_applyAcceptedSpawn;
private readonly Dictionary<RuntimeEntityKey, Progress> _progress = [];
private readonly HashSet<RuntimeEntityKey> _executing = [];
/// <summary>
/// C0-1: correlates a published
/// <see cref="RuntimePlacementProjectionKind.ExecutorCompleted"/>
/// receipt back to the full execution receipt/trace, keyed by the SAME
/// public Entity/Sequence identity every other Kind uses (the receipt's
/// own <c>Token.Entity</c>/<c>Token.Sequence</c>). Overwritten (never
/// accumulated) per entity key - an entity cannot have two drains
/// completing concurrently (<see cref="Execute"/>'s own <see cref="_executing"/>
/// reentrancy guard), so only the most recent completion for a key is
/// ever meaningful; the exact-sequence check in
/// <see cref="TryGetCompletionReceipt"/> rejects a stale lookup against a
/// superseded completion under a reused key.
/// </summary>
private readonly Dictionary<RuntimeEntityKey,
(ulong Sequence, RuntimeInitialCreateExecutionReceipt Receipt)>
_completionReceipts = [];
private Func<RuntimeGenerationToken>? _generation;
private Func<bool>? _usePositionFromServer;
private Func<Vector3?>? _localPlayerPosition;
private bool _liveInputsBound;
internal RuntimeInitialCreateContinuationExecutor(
RuntimeEntityDirectory entities,
@ -308,6 +327,157 @@ internal sealed class RuntimeInitialCreateContinuationExecutor
_generation = generation;
}
/// <summary>
/// C0-2: binds Runtime's own live-input sources so no host ever computes
/// <see cref="RuntimeInitialCreateExecutionInputs.UsePositionFromServer"/>/
/// <see cref="RuntimeInitialCreateExecutionInputs.PlayerDistance"/>
/// itself. Optional/nullable exactly like <see cref="_generation"/> is
/// NOT (that one throws when unbound) - here an unbound source is a
/// legitimate, permanent state for bare-lifetime tests, which keep
/// constructing the executor without a <see cref="GameRuntime"/> and
/// keep driving <see cref="Execute"/> with an explicit caller-supplied
/// <see cref="RuntimeInitialCreateExecutionInputs"/> override (see
/// <see cref="ResolveInputs"/>). <see cref="GameRuntime"/> binds the real
/// owners - <c>RuntimeCharacterState.UsePositionFromServer</c> and the
/// live <c>RuntimeLocalPlayerMovementState.Controller</c> position -
/// once both exist (they are constructed AFTER
/// <see cref="RuntimeEntityObjectLifetime"/>/this executor, so this bind
/// cannot happen at the executor's own constructor time the way
/// <see cref="BindGeneration"/> does; it happens alongside
/// <c>BindEventContext</c> in <c>GameRuntime</c>'s construction
/// sequence). Throws if called twice, matching every other Bind* seam on
/// this class/its siblings (<see cref="BindGeneration"/>,
/// <c>RuntimeEntityObjectEventStream.BindContext"/>,
/// <c>RuntimePlacementProjectionChannel.BindGeneration</c>).
/// F3: <paramref name="localPlayerPosition"/> itself returns
/// <c>Vector3?</c>, not <c>Vector3</c> - a BOUND source with no live
/// controller yet (the login-window drain, before
/// <c>RuntimeLocalPlayerMovementState.Controller</c> exists) must yield
/// null, not a fabricated <c>Vector3.Zero</c>. <see cref="ResolveInputs"/>
/// falls back to the caller-supplied struct's PlayerDistance whenever
/// this source is unbound OR returns null - the SAME fallback rule
/// either way, never a synthetic origin-point distance that could
/// misclassify a remote entity as implausibly far (>96 m) during that
/// window.
/// </summary>
internal void BindLiveInputs(
Func<bool> usePositionFromServer,
Func<Vector3?> localPlayerPosition)
{
ArgumentNullException.ThrowIfNull(usePositionFromServer);
ArgumentNullException.ThrowIfNull(localPlayerPosition);
if (_liveInputsBound)
{
throw new InvalidOperationException(
"The initial-create continuation executor's live-input sources are already bound.");
}
_usePositionFromServer = usePositionFromServer;
_localPlayerPosition = localPlayerPosition;
_liveInputsBound = true;
}
/// <summary>
/// C0-2: resolves the EFFECTIVE inputs for one <see cref="Execute"/>
/// call. A bound source always wins; the caller-supplied
/// <paramref name="inputs"/> struct is the test-override shape (its own
/// doc comment still describes production usage now that this method
/// exists) and is used verbatim only for whichever field has no bound
/// source - a bare-lifetime test that never calls
/// <see cref="BindLiveInputs"/> gets EXACTLY the caller-supplied values,
/// preserving every existing test's behavior unchanged.
/// <see cref="RuntimeInitialCreateExecutionInputs.PlayerDistance"/> uses
/// the SAME world-space basis as today's legacy remote path
/// (<c>LiveEntityNetworkUpdateController.cs</c>'s
/// <c>MaxPhysicsDistance</c>/<c>dist</c> computation, cutover-routes.md
/// route 4: <c>Vector3.Distance(worldPos, localPlayerPos)</c> where
/// <c>localPlayerPos</c> is the live physics-CONTROLLER position, never a
/// record snapshot) - here, <c>Vector3.Distance</c> between THIS
/// entity's own currently-accepted position (the exact field
/// <c>BeginAcceptedPlacementCore</c>/<c>CanonicalSetupTableId</c> already
/// trust: <c>Snapshot.Physics?.Position ?? Snapshot.Position</c>) and the
/// bound local-player controller position. Computed ONCE per
/// <see cref="Execute"/> call, matching the one-shot-per-call granularity
/// <paramref name="inputs"/> already had before this slice (retail
/// recomputes <c>player_distance</c> per wire packet; refining this
/// executor to per-continuation freshness is out of C0-2's scope).
/// </summary>
private RuntimeInitialCreateExecutionInputs ResolveInputs(
RuntimeEntityRecord canonical,
in RuntimeInitialCreateExecutionInputs inputs)
{
bool usePositionFromServer = _usePositionFromServer is { } source
? source()
: inputs.UsePositionFromServer;
float playerDistance = inputs.PlayerDistance;
// F3: an unbound source AND a bound-but-null live position (no
// controller yet) both fall back to the caller-supplied struct
// identically - never fabricate Vector3.Zero as a stand-in.
if (_localPlayerPosition?.Invoke() is { } localPlayerPosition
&& (canonical.Snapshot.Physics?.Position
?? canonical.Snapshot.Position) is { } accepted)
{
var target = new Vector3(
accepted.PositionX, accepted.PositionY, accepted.PositionZ);
playerDistance = Vector3.Distance(target, localPlayerPosition);
}
return new RuntimeInitialCreateExecutionInputs(
usePositionFromServer, playerDistance);
}
/// <summary>
/// C0-1: reaches the full execution receipt/trace correlated with an
/// observed <see cref="RuntimePlacementProjectionKind.ExecutorCompleted"/>
/// receipt, purely via that receipt's own public
/// <c>Token.Entity</c>/<c>Token.Sequence</c> identity - the same identity
/// every other placement Kind is acknowledged by. Returns false for a
/// superseded/stale sequence under a reused entity key.
/// </summary>
internal bool TryGetCompletionReceipt(
in RuntimePlacementProjectionToken token,
out RuntimeInitialCreateExecutionReceipt receipt)
{
if (_completionReceipts.TryGetValue(
token.Entity,
out (ulong Sequence, RuntimeInitialCreateExecutionReceipt Receipt) entry)
&& entry.Sequence == token.Sequence)
{
receipt = entry.Receipt;
return true;
}
receipt = default;
return false;
}
/// <summary>
/// F2: reaps exactly one completion-receipt correlation entry, bound as
/// <see cref="RuntimeSetPositionState.BindExecutorCompletionAcknowledgement"/>'s
/// notification callback - fired the moment a host acknowledges the
/// Kind ExecutorCompleted receipt this entry correlates, never before.
/// The exact-sequence check rejects removing a NEWER completion's entry
/// under a reused key (mirrors <see cref="TryGetCompletionReceipt"/>'s
/// own currency check).
/// </summary>
internal void ForgetCompletionReceipt(RuntimeEntityKey key, ulong sequence)
{
if (_completionReceipts.TryGetValue(
key,
out (ulong Sequence, RuntimeInitialCreateExecutionReceipt Receipt) entry)
&& entry.Sequence == sequence)
{
_completionReceipts.Remove(key);
}
}
/// <summary>
/// F2: folded into <see cref="RuntimeEntityObjectOwnershipSnapshot"/>/
/// <c>IsConverged</c> - an unacknowledged completion receipt is
/// outstanding host debt, mirroring
/// <see cref="RuntimeSetPositionOwnershipSnapshot.PendingProjectionAcknowledgementCount"/>'s
/// existing "must be zero to converge" shape for the SAME underlying
/// receipt stream.
/// </summary>
internal int PendingCompletionReceiptCount => _completionReceipts.Count;
internal int ProgressCount => _progress.Count;
/// <summary>
@ -389,6 +559,15 @@ internal sealed class RuntimeInitialCreateContinuationExecutor
/// </summary>
internal void DiscardProgress(RuntimeEntityKey key)
{
// F2: reap this key's completion-receipt correlation entry
// unconditionally - DiscardProgress owns cleanup of every piece of
// state IT introduces, and this cache is exactly that (see
// _completionReceipts's own doc comment). Independent of whether
// _progress still tracks this key: a completed drain has ALREADY
// removed its own Progress entry before this correlation entry was
// ever added (see ExecuteCore's Released case), so this is the
// ONLY choke point that reaps it outside of a normal acknowledge.
_completionReceipts.Remove(key);
if (!_progress.Remove(key, out Progress? progress))
return;
if (progress.PendingContinuationPlacement.IsValid)
@ -421,6 +600,9 @@ internal sealed class RuntimeInitialCreateContinuationExecutor
_physics.SetPosition.PublishCancellation(cancellation);
}
_progress.Clear();
// F2: bulk-reap every completion-receipt correlation entry - a full
// session clear must not carry any of this cache across a reset.
_completionReceipts.Clear();
}
internal RuntimeInitialCreateExecutionStatus Execute(
@ -458,6 +640,11 @@ internal sealed class RuntimeInitialCreateContinuationExecutor
out RuntimeInitialCreateExecutionReceipt receipt)
{
receipt = default;
// C0-2: resolve ONCE per Execute call - a bound Runtime source always
// wins over the caller-supplied test-override struct (see
// ResolveInputs's own doc comment for the exact fallback rule).
RuntimeInitialCreateExecutionInputs effectiveInputs =
ResolveInputs(canonical, inputs);
// An existing Progress for a DIFFERENT (older or ABA-reused) lease
// id is discarded here, and THIS exact call fails closed - an old
@ -561,7 +748,7 @@ internal sealed class RuntimeInitialCreateContinuationExecutor
return Abandon(canonical, key);
RuntimeInitialCreateExecutionStatus applyStatus =
ApplyContinuation(canonical, token, key, continuation, inputs, progress);
ApplyContinuation(canonical, token, key, continuation, effectiveInputs, progress);
// Round 3 B2: every apply method below now rebaselines
// itself immediately after its own canonical mutation and
// BEFORE its own publish (mutate -> rebaseline -> publish),
@ -592,14 +779,36 @@ internal sealed class RuntimeInitialCreateContinuationExecutor
switch (release)
{
case RuntimeInitialCreateResidenceExecutorReleaseStatus.Released:
receipt = new RuntimeInitialCreateExecutionReceipt(
{
var completedReceipt = new RuntimeInitialCreateExecutionReceipt(
key,
residenceReceipt.FullCellId,
residenceReceipt.TeleportHookPhase,
progress.Trace.ToImmutable(),
progress.ReplayedDeferredChildCount);
receipt = completedReceipt;
_progress.Remove(key);
// C0-1: bridge the executor's own completion onto the
// SAME ordered placement receipt stream every
// Place/Withdraw/Discard uses (canonical is still
// current here - nothing between the last continuation
// apply and ConsumeExecuted's Released outcome mutates
// it). Correlate the full trace via the fresh token's
// Entity/Sequence identity - see TryGetCompletionReceipt.
// F2: registration happens INSIDE PublishExecutorCompletion's
// beforePublish callback (before the synchronous observer
// dispatch), not after this call returns - a subscriber
// reading the correlation back from inside its own
// OnPlacement callback must already find it. receipt is
// copied to a local (completedReceipt) because an `out`
// parameter cannot be captured by a lambda.
_physics.SetPosition.PublishExecutorCompletion(
canonical,
beforePublish: token =>
_completionReceipts[key] =
(token.Sequence, completedReceipt));
return RuntimeInitialCreateExecutionStatus.Completed;
}
case RuntimeInitialCreateResidenceExecutorReleaseStatus.Revised:
// A new continuation arrived mid-drain (Enqueue bumps the
// completed entry's Adoption.Revision in place). Re-fetch