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

@ -946,6 +946,19 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
return true;
}
if (projection.Kind is RuntimePlacementProjectionKind.ExecutorCompleted)
{
// F1: acknowledge-and-ignore, same as Discard. Must NOT fall
// through to the record-lookup/spatial-load gates below - those
// legitimately reject for reasons unrelated to this receipt (no
// sidecar yet, destination backend not loaded), and a false
// return here wedges the whole ordered placement stream at the
// FIFO head (RuntimePlacementProjectionSubscription's contract).
// Provably inert today: PublishExecutorCompletion has zero
// production callers.
return true;
}
RuntimePlacementProjectionToken token = projection.Token;
if (!TryGetRuntimePlacementProjectionRecord(
token,

View file

@ -76,8 +76,21 @@ internal sealed class RuntimePlacementPresentationSink
if (!_liveEntities.TryApplyRuntimePlacementProjection(in projection))
return false;
if (projection.Kind is RuntimePlacementProjectionKind.Discard)
if (projection.Kind is RuntimePlacementProjectionKind.Discard
or RuntimePlacementProjectionKind.ExecutorCompleted)
{
// F1: ExecutorCompleted is acknowledge-and-ignore like Discard -
// no world/presentation mutation by definition. Must NOT fall
// through to the record-lookup gate below (that gate legitimately
// rejects for OTHER reasons, and this sink's caller
// (RuntimePlacementProjectionSubscription) treats a false return
// as "leave at the FIFO head" - a rejected ExecutorCompleted
// would permanently wedge the whole ordered stream). Provably
// inert today: PublishExecutorCompletion has zero production
// callers - see
// RuntimePlacementPresentationSinkTests.ExecutorCompleted_IsAckOnlyNoOpEvenWhenTokenIsStaleOrSidecarIsGone.
return true;
}
if (!_liveEntities.TryGetRecord(
projection.Token.Entity,
out LiveEntityRecord record)

View file

@ -31,6 +31,24 @@ internal sealed class HeadlessRuntimePlacementProjectionSink
return true;
}
if (projection.Kind is RuntimePlacementProjectionKind.ExecutorCompleted)
{
// F1: acknowledge-and-ignore, same as Discard - ExecutorCompleted
// is not a placement to project (no world/presentation mutation
// by definition; the executor's own drain already committed
// every Place/Withdraw this receipt follows). 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. Currently provably inert: PublishExecutorCompletion
// has zero production callers (Execute/RegisterEntityWithInitialResidence
// are both unreached in production) - see
// HeadlessSessionHostTests.ExecutorCompletedReceiptIsAcknowledgeOnlyRegardlessOfRecordValidity.
return true;
}
RuntimePlacementProjectionToken token = projection.Token;
RuntimeEntityDirectory directory = _runtime.EntityObjects.Entities;
if (!token.IsValid

View file

@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System.Numerics;
using AcDream.Core.Items;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
@ -44,7 +45,18 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
int DeferredAcceptedRelationCount = 0,
/// <summary>Round 5 R5-3: mirrors StreamDispatchFailureCount/HasLastStreamDispatchFailure for the executor's contained-replay failure surface. Diagnostic only - like its stream precedent, NOT gated by <see cref="IsConverged"/>.</summary>
long ReplayFailureCount = 0,
bool HasLastReplayFailure = false)
bool HasLastReplayFailure = false,
/// <summary>
/// F2: outstanding <see cref="RuntimeInitialCreateContinuationExecutor"/>
/// token-to-receipt correlation entries - one per completed drain whose
/// ExecutorCompleted receipt a host has not yet acknowledged. Gated by
/// <see cref="IsConverged"/>, mirroring
/// <see cref="RuntimeSetPositionOwnershipSnapshot.PendingProjectionAcknowledgementCount"/>'s
/// existing "unacknowledged receipt is outstanding debt" shape for the
/// SAME underlying receipt stream - unlike ReplayFailureCount above,
/// this is NOT a diagnostic-only counter.
/// </summary>
int PendingCompletionReceiptCount = 0)
{
public bool IsConverged =>
IsDisposed
@ -65,6 +77,7 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
&& PendingMoveCount == 0
&& InitialCreateResidenceLeaseCount == 0
&& InitialCreateExecutorProgressCount == 0
&& PendingCompletionReceiptCount == 0
&& StreamSubscriberCount == 0
&& PlacementStreamSubscriberCount == 0
&& PendingDispatchCount == 0
@ -152,6 +165,13 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
// residence state referencing the executor type directly.
InitialCreateResidences.BindRetirementNotification(
key => InitialCreateExecution.DiscardProgress(key));
// F2: reaps the executor's completion-receipt correlation entry
// exactly when a host acknowledges the ExecutorCompleted receipt it
// correlates - mirrors the residence-retirement binding immediately
// above.
Physics.SetPosition.BindExecutorCompletionAcknowledgement(
(key, sequence) =>
InitialCreateExecution.ForgetCompletionReceipt(key, sequence));
Placements = new RuntimePlacementProjectionChannel(
Events,
Physics.SetPosition);
@ -197,6 +217,13 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
// residence state referencing the executor type directly.
InitialCreateResidences.BindRetirementNotification(
key => InitialCreateExecution.DiscardProgress(key));
// F2: reaps the executor's completion-receipt correlation entry
// exactly when a host acknowledges the ExecutorCompleted receipt it
// correlates - mirrors the residence-retirement binding immediately
// above.
Physics.SetPosition.BindExecutorCompletionAcknowledgement(
(key, sequence) =>
InitialCreateExecution.ForgetCompletionReceipt(key, sequence));
Placements = new RuntimePlacementProjectionChannel(
Events,
Physics.SetPosition);
@ -242,6 +269,13 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
// residence state referencing the executor type directly.
InitialCreateResidences.BindRetirementNotification(
key => InitialCreateExecution.DiscardProgress(key));
// F2: reaps the executor's completion-receipt correlation entry
// exactly when a host acknowledges the ExecutorCompleted receipt it
// correlates - mirrors the residence-retirement binding immediately
// above.
Physics.SetPosition.BindExecutorCompletionAcknowledgement(
(key, sequence) =>
InitialCreateExecution.ForgetCompletionReceipt(key, sequence));
Placements = new RuntimePlacementProjectionChannel(
Events,
Physics.SetPosition);
@ -292,7 +326,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
_disposed,
parents.DeferredAcceptedRelationCount,
InitialCreateExecution.ReplayFailureCount,
InitialCreateExecution.LastReplayFailure is not null);
InitialCreateExecution.LastReplayFailure is not null,
InitialCreateExecution.PendingCompletionReceiptCount);
}
public void BindEventContext(
@ -306,6 +341,25 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
InitialCreateExecution.BindGeneration(generation);
}
/// <summary>
/// C0-2: forwards to <see cref="RuntimeInitialCreateContinuationExecutor.BindLiveInputs"/>,
/// the same fan-out shape <see cref="BindEventContext"/> already uses for
/// generation binding. Separate from <see cref="BindEventContext"/>
/// because <c>GameRuntime</c> constructs <c>RuntimeCharacterState</c>/
/// <c>RuntimeLocalPlayerMovementState</c> (the real source owners) AFTER
/// this lifetime, so the live-input bind necessarily happens at a later
/// point in <c>GameRuntime</c>'s construction sequence than the
/// generation bind.
/// </summary>
public void BindLiveInputs(
Func<bool> usePositionFromServer,
Func<Vector3?> localPlayerPosition)
{
EnsureNotDisposed();
InitialCreateExecution.BindLiveInputs(
usePositionFromServer, localPlayerPosition);
}
/// <summary>
/// Owns the presentation-free half of retail's CreateObject lifetime
/// transaction. An attached graphical host may synchronously retire the
@ -963,6 +1017,28 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
}
Entities.RefreshSnapshot(canonical, accepted);
// C0-4(a): this method had NO cancellation choke-point at all,
// leaving a residence lease or ordinary SetPosition operation
// dangling once ordinary placement traffic goes live - fixed with
// the SAME exactly-once ForgetInitialCreateResidence ->
// Physics.SetPosition.Forget -> PreferCancellation sequence every
// other commit in this family (CommitPositionChannelUpdate,
// TryApplyPickup, CommitAcceptedParentCellless, TryAcceptDelete)
// uses for THIS part of the job.
// F4 (deliberate, NOT an oversight): unlike CommitPositionChannelUpdate,
// this method does NOT also call Physics.CollisionReports.LeaveWorld.
// Retail set_parent (0x00515A90, lines 283832-283833) performs its
// single leave_world call gated behind the SAME add_child branch this
// method's staged/deferred-replay commit represents (App's
// EquippedChildRenderController realize sequence, the executor's own
// ParentRelationReplay) - a second LeaveWorld here would double-leave-
// world with no retail counterpart.
RuntimePlacementCancellationReceipt initialCancellation =
ForgetInitialCreateResidence(canonical);
RuntimePlacementCancellationReceipt ordinaryCancellation =
Physics.SetPosition.Forget(canonical);
RuntimePlacementCancellationReceipt cancellation =
PreferCancellation(initialCancellation, ordinaryCancellation);
Entities.AdvanceParentCommit(canonical);
ulong parentCommitVersion = canonical.ParentCommitVersion;
return AcknowledgeProjectionAndPublish(
@ -970,7 +1046,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
() => acknowledgeProjection?.Invoke(canonical),
RuntimeEntityChange.Updated,
() => canonical.ParentCommitVersion
== parentCommitVersion);
== parentCommitVersion,
cancellation);
}
public bool CommitAcceptedParentCellless(
@ -1407,9 +1484,21 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
if (!Entities.IsCurrent(canonical))
return false;
RuntimePlacementCancellationReceipt cancellation =
// C0-4(b): this cancelled the initial-create residence but never the
// ORDINARY placement family (Physics.SetPosition.Forget), unlike
// TryApplyPickup/CommitAcceptedParentCellless/TryAcceptDelete, which
// all cancel both. WithdrawLiveEntityProjectionToCellless routes
// through here, so a live ordinary SetPosition/lost-cell watch could
// dangle across a withdrawal-to-cellless once ordinary placement
// traffic goes live. Fixed symmetrically with the same
// Forget/PreferCancellation pair every sibling withdrawal uses.
RuntimePlacementCancellationReceipt initialCancellation =
ForgetInitialCreateResidence(canonical);
Physics.CollisionReports.LeaveWorld(canonical);
RuntimePlacementCancellationReceipt ordinaryCancellation =
Physics.SetPosition.Forget(canonical);
RuntimePlacementCancellationReceipt cancellation =
PreferCancellation(initialCancellation, ordinaryCancellation);
Entities.SuspendObjectClock(canonical);
Entities.SetFullCell(canonical, 0u, 0u);
ulong spatialVersion = canonical.SpatialAuthorityVersion;

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

View file

@ -1,3 +1,4 @@
using System.Numerics;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Physics;
@ -267,6 +268,22 @@ public sealed class GameRuntime
() => generationReset.ActiveRetiringGeneration
?? context.Session.Generation,
() => clock.FrameNumber);
// C0-2: bind the executor's live-input sources to the real
// Runtime owners now that both exist (RuntimeCharacterState at
// CharacterCreated, RuntimeLocalPlayerMovementState at
// MovementCreated - both after EntityObjectsCreated, so this
// cannot move earlier). UsePositionFromServer mirrors retail
// CommandInterpreter::UsePositionFromServer exactly; PlayerDistance
// is derived per Execute call from the live physics-controller
// position, matching the legacy remote path's own distance basis
// (LiveEntityNetworkUpdateController's MaxPhysicsDistance/dist).
// F3: the position source is nullable - a null Controller (the
// login-window drain, before the local player's own controller
// exists yet) must yield null, never a fabricated Vector3.Zero
// that would misclassify every remote entity as implausibly far.
context.EntityObjects.BindLiveInputs(
() => context.Character.UsePositionFromServer,
() => context.Movement.Controller?.Position);
context.Events = new GameRuntimeEventHub(
context.EntityObjects,
context.Communication,

View file

@ -18,7 +18,9 @@ public readonly record struct RuntimeCharacterOwnershipSnapshot(
int PositionCount,
int PropertyCount,
bool OptionsAreDefaults,
bool MovementSkillsAreReset)
bool MovementSkillsAreReset,
/// <summary>C0-2: <see cref="RuntimeCharacterState.AutonomyLevel"/> is back at retail's default (<see cref="RuntimeCharacterState.FullAutonomyLevel"/>).</summary>
bool AutonomyIsDefault = true)
{
public bool IsConverged =>
IsDisposed
@ -33,7 +35,8 @@ public readonly record struct RuntimeCharacterOwnershipSnapshot(
&& PositionCount == 0
&& PropertyCount == 0
&& OptionsAreDefaults
&& MovementSkillsAreReset;
&& MovementSkillsAreReset
&& AutonomyIsDefault;
}
/// <summary>
@ -47,11 +50,27 @@ public sealed class RuntimeCharacterState : IDisposable
public const uint RunSkillId = 24u;
/// <summary>ACE Skill enum ordinal for Jump (K-fix7 / pseudocode doc §5).</summary>
public const uint JumpSkillId = 22u;
/// <summary>
/// C0-2/F5(b): retail <c>CommandInterpreter</c>'s own default, set at
/// construction (pseudo-C 699752, <c>this->autonomy_level = 2;</c>, a
/// direct field write, not a <c>SetAutonomyLevel</c> call) and by the
/// command-line-only override at admission
/// (<c>command_line_autonomy_level</c>, pseudo-C 1088429, itself
/// defaulting to <c>0x2</c>). Exactly ONE retail caller of
/// <c>CommandInterpreter::SetAutonomyLevel</c> exists in the named
/// retail decomp - the startup construction path at pseudo-C 94102
/// (<c>cmdinterp-&gt;vtable-&gt;SetAutonomyLevel(cmdinterp, command_line_autonomy_level)</c>)
/// - it is a startup/debug knob, not a per-play-session gameplay
/// toggle, so acdream's own default matches retail's value exactly and
/// nothing in ordinary play ever changes it.
/// </summary>
public const uint FullAutonomyLevel = 2u;
private bool _disposed;
private long _characterRevision;
private long _spellbookRevision;
private bool _internalSubscriptionsAttached;
private uint _autonomyLevel = FullAutonomyLevel;
/// <summary>
/// Campaign P Slice P1 (2026-07-30): the pre-<c>EnchantSkill</c> base
@ -88,6 +107,39 @@ public sealed class RuntimeCharacterState : IDisposable
public IRuntimeCharacterView View { get; }
public bool IsDisposed => _disposed;
/// <summary>Retail <c>CommandInterpreter::GetAutonomyLevel</c>.</summary>
public uint AutonomyLevel => Volatile.Read(ref _autonomyLevel);
/// <summary>
/// C0-2: retail <c>CommandInterpreter::UsePositionFromServer</c>
/// (pseudo-C 699506-699512: <c>result = this->autonomy_level != 2;</c>).
/// This is the source
/// <see cref="AcDream.Runtime.Entities.RuntimeInitialCreateContinuationExecutor.BindLiveInputs"/>
/// binds for <c>RuntimeInitialCreateExecutionInputs.UsePositionFromServer</c>
/// - the local-player-only interpolate gate consumed by
/// <c>RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition</c>.
/// </summary>
public bool UsePositionFromServer => AutonomyLevel != FullAutonomyLevel;
/// <summary>
/// Retail <c>CommandInterpreter::SetAutonomyLevel</c> (pseudo-C
/// 699542-699552): rejects any value above 2, otherwise commits.
/// F5(a): retail's own setter ALSO sends <c>SendAutonomyLevelEvent</c>
/// (pseudo-C 699550) after committing - this Runtime-only port has no
/// outbound wire concept to carry that event today (autonomy level has
/// no host caller yet). Any FUTURE host exposure of this setter (e.g. a
/// debug/admin command) MUST also send the equivalent outbound event -
/// do not port only the field write.
/// </summary>
public bool TrySetAutonomyLevel(uint level)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (level > FullAutonomyLevel)
return false;
Volatile.Write(ref _autonomyLevel, level);
return true;
}
public RuntimeCharacterOwnershipSnapshot CaptureOwnership()
{
int favoriteCount = 0;
@ -144,7 +196,8 @@ public sealed class RuntimeCharacterState : IDisposable
&& MovementSkills.LastPkAttackTimestamp is null
&& _runSkillBase == -1
&& _jumpSkillBase == -1
&& _movementSkillAugmentations == default);
&& _movementSkillAugmentations == default,
AutonomyLevel == FullAutonomyLevel);
}
/// <summary>
@ -355,6 +408,7 @@ public sealed class RuntimeCharacterState : IDisposable
_runSkillBase = -1;
_jumpSkillBase = -1;
_movementSkillAugmentations = default;
Volatile.Write(ref _autonomyLevel, FullAutonomyLevel);
Try(MovementSkills.ResetSession, ref failures);
if (failures is not null)
{
@ -380,6 +434,7 @@ public sealed class RuntimeCharacterState : IDisposable
_runSkillBase = -1;
_jumpSkillBase = -1;
_movementSkillAugmentations = default;
Volatile.Write(ref _autonomyLevel, FullAutonomyLevel);
Try(MovementSkills.ResetSession, ref failures);
}
finally

View file

@ -1,5 +1,6 @@
using System.Collections.Immutable;
using System.Numerics;
using AcDream.Content;
using AcDream.Core.Items;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
@ -59,6 +60,17 @@ public enum RuntimePlacementProjectionKind
Withdraw,
Place,
Discard,
/// <summary>
/// C0-1: the initial-create continuation executor's own FIFO drain has
/// finished for this entity (not itself a SetPosition operation - the
/// residence lease is already released by the time this publishes).
/// Published on the SAME ordered stream every Place/Withdraw/Discard
/// receipt uses so a host learns "this entity's placement committed and
/// its FIFO drained" through the one already-built observer seam,
/// instead of a second stream/queue. Acknowledge-only, like Discard -
/// see AcknowledgeProjection's dedicated branch.
/// </summary>
ExecutorCompleted,
}
public readonly record struct RuntimePortalPlacementAuthority(
@ -432,6 +444,7 @@ internal sealed class RuntimeSetPositionState : IDisposable
private ulong _nextCollisionPrefixQuiescenceOperationId;
private ulong _nextLostDeadlineSequence;
private RuntimeEntityObjectEventStream? _events;
private Action<RuntimeEntityKey, ulong>? _executorCompletionAcknowledged;
private bool _disposed;
private readonly record struct LostDeadlineEntry(
@ -770,6 +783,87 @@ internal sealed class RuntimeSetPositionState : IDisposable
}
}
/// <summary>
/// C0-1: publishes the initial-create continuation executor's own
/// completion (its FIFO drain has finished and its residence lease is
/// already released) on the SAME ordered receipt stream every
/// Place/Withdraw/Discard uses - the pinned contract forbids a second
/// stream/queue. This is NOT an Operation-backed receipt (the executor's
/// own placement operation, if any, already committed and was
/// acknowledged earlier in the drain - see
/// RuntimeInitialCreateContinuationExecutor.ExecuteCore's call site) so
/// the token is built the same way PublishProjection assembles one, but
/// from the CANONICAL record's current authority/version/cell facts
/// (self-consistent: nothing else can move them synchronously between
/// the executor's release and this publish) instead of a now-gone
/// Operation. AcknowledgeProjection's dedicated ExecutorCompleted branch
/// treats it exactly like Discard - acknowledge-only, no operation to
/// resume or commit against.
/// </summary>
internal RuntimePlacementProjectionToken PublishExecutorCompletion(
RuntimeEntityRecord record,
Action<RuntimePlacementProjectionToken>? beforePublish = null,
RuntimePortalPlacementAuthority portal = default)
{
EnsureNotDisposed();
ArgumentNullException.ThrowIfNull(record);
if (record.Key is not { } key)
return default;
PhysicsBody? body = record.PhysicsBody;
ulong sequence = checked(++_nextProjectionSequence);
var token = new RuntimePlacementProjectionToken(
sequence,
Revision: 1UL,
key,
record.PositionAuthorityVersion,
record.SpatialAuthorityVersion,
record.PlacementCommitVersion,
_entities.SessionLifetimeVersion,
record.FullCellId,
_physics.ExpectedCollisionGeneration(record.FullCellId),
portal);
var snapshot = new RuntimePlacementProjectionSnapshot(
token,
RuntimePlacementProjectionKind.ExecutorCompleted,
body?.Position ?? Vector3.Zero,
body?.Orientation ?? Quaternion.Identity,
body?.CellPosition.Frame.Origin ?? Vector3.Zero,
body?.InContact ?? false,
body?.OnWalkable ?? false);
_pendingProjection.Add(sequence, snapshot);
// F2: register-before-publish - beforePublish runs while the token is
// already in _pendingProjection but before PublishPlacement's
// synchronous observer dispatch, so a subscriber reading back the
// executor's correlation entry from inside its OWN OnPlacement
// callback always finds it.
beforePublish?.Invoke(token);
PublishPlacement(snapshot);
return token;
}
/// <summary>
/// F2: binds the ONE notification fired when a Kind ExecutorCompleted
/// receipt is acknowledged (mirrors
/// RuntimeInitialCreateResidenceState.BindRetirementNotification's
/// existing one-bound-delegate shape). The executor uses this to reap
/// its own token-to-receipt correlation entry exactly when the receipt
/// it correlates is consumed - never before (a host might still be
/// mid-retry) and never left dangling after (unbounded per-completed-
/// entity retention).
/// </summary>
internal void BindExecutorCompletionAcknowledgement(
Action<RuntimeEntityKey, ulong> acknowledged)
{
ArgumentNullException.ThrowIfNull(acknowledged);
if (_executorCompletionAcknowledged is not null)
{
throw new InvalidOperationException(
"The executor-completion acknowledgement notification is already bound.");
}
_executorCompletionAcknowledged = acknowledged;
}
internal void ResetSession()
{
EnsureNotDisposed();
@ -1132,6 +1226,95 @@ internal sealed class RuntimeSetPositionState : IDisposable
return RuntimeSetPositionMoverPreparationStatus.Prepared;
}
/// <summary>
/// C0-3: chains the exact-Setup mover pipeline end-to-end for an
/// authored placement (initial-Create or any other authored-mover
/// operation) whose route performs SetPosition - PrepareMover /
/// RuntimeSetPositionMoverPreparer.TryBuild /
/// IPreparedCollisionSource.ReadSetupCollision already exist piecewise
/// (inventory gap a); this is the missing wiring, not a behavior change.
/// Reads the CANONICAL Setup table id from the record via
/// <see cref="CanonicalSetupTableId"/> - the SAME field
/// <see cref="CapturePreparationAuthority"/> already trusts - rather than
/// a caller-supplied id, so this can never be pointed at the wrong
/// Setup. A record with no authored Setup at all (id 0) takes retail's
/// genuine "no Setup" dummy-sphere path
/// (<see cref="RuntimeSetPositionMoverSetup.ResolvedAbsent"/>) instead of
/// reading anything; a record WITH an id but an unavailable/corrupt
/// asynchronous read yields <see cref="RuntimeSetPositionMoverPreparationStatus.RetrySetupUnavailable"/>
/// so the caller retries once the prepared-collision package lands,
/// mirroring <see cref="RuntimeSetPositionMoverSetup"/>'s own
/// doc-comment distinction between "not arrived yet" and "resolved
/// absent". Dormant: internal, no production caller - a residence
/// lease's own <c>Placement</c>/<c>Route.OperationKind</c>/
/// <c>Route.SetPositionFlags</c> are exactly the token/kind/flags this
/// takes.
/// </summary>
internal RuntimeSetPositionMoverPreparationStatus
TryPrepareAndSubmitAuthoredPlacement(
RuntimeEntityRecord record,
in RuntimeEntityPlacementToken token,
RuntimeSetPositionOperationKind operationKind,
PhysicsSetPositionFlags flags,
IPreparedCollisionSource collisionSource,
double gameTime,
out RuntimeSetPositionOutcome outcome,
PhysicsPlacementClass placementClass = PhysicsPlacementClass.Ordinary,
RuntimePortalPlacementAuthority portal = default,
Vector3 line = default,
float scatterRadiusX = 0f,
float scatterRadiusY = 0f,
uint scatterAttempts = 0u,
float shadowWorldOffsetX = 0f,
float shadowWorldOffsetY = 0f)
{
EnsureNotDisposed();
ArgumentNullException.ThrowIfNull(record);
ArgumentNullException.ThrowIfNull(collisionSource);
outcome = default;
uint setupTableId = CanonicalSetupTableId(record);
RuntimeSetPositionMoverSetup setup;
if (setupTableId == 0u)
{
setup = RuntimeSetPositionMoverSetup.ResolvedAbsent;
}
else
{
PreparedCollisionReadResult<FlatSetupCollision> read =
collisionSource.ReadSetupCollision(setupTableId);
if (read.Status != PreparedAssetReadStatus.Loaded
|| read.Data is null)
{
return RuntimeSetPositionMoverPreparationStatus
.RetrySetupUnavailable;
}
setup = RuntimeSetPositionMoverSetup.Resolved(
setupTableId, read.Data);
}
var preparation = new RuntimeSetPositionMoverPreparation(
setup,
operationKind,
gameTime,
placementClass,
flags,
line,
scatterRadiusX,
scatterRadiusY,
scatterAttempts,
shadowWorldOffsetX,
shadowWorldOffsetY,
portal);
RuntimeSetPositionMoverPreparationStatus status = PrepareMover(
token, preparation, out RuntimeSetPositionCommand command);
if (status != RuntimeSetPositionMoverPreparationStatus.Prepared)
return status;
outcome = SubmitPreparedPlacement(token, command);
return RuntimeSetPositionMoverPreparationStatus.Prepared;
}
internal bool IsExactPreparedPlacementCurrent(
RuntimeEntityRecord record,
in RuntimeEntityPlacementToken token,
@ -2313,10 +2496,24 @@ internal sealed class RuntimeSetPositionState : IDisposable
{
return false;
}
if (pending.Kind is RuntimePlacementProjectionKind.Discard)
if (pending.Kind is RuntimePlacementProjectionKind.Discard
or RuntimePlacementProjectionKind.ExecutorCompleted)
{
// C0-1: an ExecutorCompleted receipt is never Operation-backed
// (see PublishExecutorCompletion) - there is nothing to resume or
// commit against, exactly like Discard.
_pendingProjection.Remove(token.Sequence);
RetireQuiescenceProjectionSequence(token.Sequence);
if (pending.Kind is RuntimePlacementProjectionKind.ExecutorCompleted)
{
// F2: notify the executor so it can reap its own
// token-to-receipt correlation entry now - not before (a
// host might still be mid-retry against this exact
// unacknowledged receipt) and not left dangling after.
_executorCompletionAcknowledged?.Invoke(
token.Entity,
token.Sequence);
}
return true;
}
if (!_operations.TryGetValue(