diff --git a/src/AcDream.App/World/LiveEntityRuntime.cs b/src/AcDream.App/World/LiveEntityRuntime.cs
index e4080dee..9c818d22 100644
--- a/src/AcDream.App/World/LiveEntityRuntime.cs
+++ b/src/AcDream.App/World/LiveEntityRuntime.cs
@@ -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,
diff --git a/src/AcDream.App/World/RuntimePlacementPresentationSink.cs b/src/AcDream.App/World/RuntimePlacementPresentationSink.cs
index 79e23e9c..9f062087 100644
--- a/src/AcDream.App/World/RuntimePlacementPresentationSink.cs
+++ b/src/AcDream.App/World/RuntimePlacementPresentationSink.cs
@@ -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)
diff --git a/src/AcDream.Headless/Hosting/HeadlessRuntimePlacementProjectionSink.cs b/src/AcDream.Headless/Hosting/HeadlessRuntimePlacementProjectionSink.cs
index 51f3d156..3e325cdc 100644
--- a/src/AcDream.Headless/Hosting/HeadlessRuntimePlacementProjectionSink.cs
+++ b/src/AcDream.Headless/Hosting/HeadlessRuntimePlacementProjectionSink.cs
@@ -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
diff --git a/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs b/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs
index a98e8c4b..2571d719 100644
--- a/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs
+++ b/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs
@@ -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,
/// Round 5 R5-3: mirrors StreamDispatchFailureCount/HasLastStreamDispatchFailure for the executor's contained-replay failure surface. Diagnostic only - like its stream precedent, NOT gated by .
long ReplayFailureCount = 0,
- bool HasLastReplayFailure = false)
+ bool HasLastReplayFailure = false,
+ ///
+ /// F2: outstanding
+ /// token-to-receipt correlation entries - one per completed drain whose
+ /// ExecutorCompleted receipt a host has not yet acknowledged. Gated by
+ /// , mirroring
+ /// 's
+ /// existing "unacknowledged receipt is outstanding debt" shape for the
+ /// SAME underlying receipt stream - unlike ReplayFailureCount above,
+ /// this is NOT a diagnostic-only counter.
+ ///
+ 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);
}
+ ///
+ /// C0-2: forwards to ,
+ /// the same fan-out shape already uses for
+ /// generation binding. Separate from
+ /// because GameRuntime constructs RuntimeCharacterState/
+ /// RuntimeLocalPlayerMovementState (the real source owners) AFTER
+ /// this lifetime, so the live-input bind necessarily happens at a later
+ /// point in GameRuntime's construction sequence than the
+ /// generation bind.
+ ///
+ public void BindLiveInputs(
+ Func usePositionFromServer,
+ Func localPlayerPosition)
+ {
+ EnsureNotDisposed();
+ InitialCreateExecution.BindLiveInputs(
+ usePositionFromServer, localPlayerPosition);
+ }
+
///
/// 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;
diff --git a/src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs b/src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs
index 9905b538..d545fe78 100644
--- a/src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs
+++ b/src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs
@@ -274,7 +274,26 @@ internal sealed class RuntimeInitialCreateContinuationExecutor
_applyAcceptedSpawn;
private readonly Dictionary _progress = [];
private readonly HashSet _executing = [];
+ ///
+ /// C0-1: correlates a published
+ ///
+ /// receipt back to the full execution receipt/trace, keyed by the SAME
+ /// public Entity/Sequence identity every other Kind uses (the receipt's
+ /// own Token.Entity/Token.Sequence). Overwritten (never
+ /// accumulated) per entity key - an entity cannot have two drains
+ /// completing concurrently ('s own
+ /// reentrancy guard), so only the most recent completion for a key is
+ /// ever meaningful; the exact-sequence check in
+ /// rejects a stale lookup against a
+ /// superseded completion under a reused key.
+ ///
+ private readonly Dictionary
+ _completionReceipts = [];
private Func? _generation;
+ private Func? _usePositionFromServer;
+ private Func? _localPlayerPosition;
+ private bool _liveInputsBound;
internal RuntimeInitialCreateContinuationExecutor(
RuntimeEntityDirectory entities,
@@ -308,6 +327,157 @@ internal sealed class RuntimeInitialCreateContinuationExecutor
_generation = generation;
}
+ ///
+ /// C0-2: binds Runtime's own live-input sources so no host ever computes
+ /// /
+ ///
+ /// itself. Optional/nullable exactly like 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 and
+ /// keep driving with an explicit caller-supplied
+ /// override (see
+ /// ). binds the real
+ /// owners - RuntimeCharacterState.UsePositionFromServer and the
+ /// live RuntimeLocalPlayerMovementState.Controller position -
+ /// once both exist (they are constructed AFTER
+ /// /this executor, so this bind
+ /// cannot happen at the executor's own constructor time the way
+ /// does; it happens alongside
+ /// BindEventContext in GameRuntime's construction
+ /// sequence). Throws if called twice, matching every other Bind* seam on
+ /// this class/its siblings (,
+ /// RuntimeEntityObjectEventStream.BindContext"/>,
+ /// RuntimePlacementProjectionChannel.BindGeneration).
+ /// F3: itself returns
+ /// Vector3?, not Vector3 - a BOUND source with no live
+ /// controller yet (the login-window drain, before
+ /// RuntimeLocalPlayerMovementState.Controller exists) must yield
+ /// null, not a fabricated Vector3.Zero.
+ /// 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.
+ ///
+ internal void BindLiveInputs(
+ Func usePositionFromServer,
+ Func 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;
+ }
+
+ ///
+ /// C0-2: resolves the EFFECTIVE inputs for one
+ /// call. A bound source always wins; the caller-supplied
+ /// 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
+ /// gets EXACTLY the caller-supplied values,
+ /// preserving every existing test's behavior unchanged.
+ /// uses
+ /// the SAME world-space basis as today's legacy remote path
+ /// (LiveEntityNetworkUpdateController.cs's
+ /// MaxPhysicsDistance/dist computation, cutover-routes.md
+ /// route 4: Vector3.Distance(worldPos, localPlayerPos) where
+ /// localPlayerPos is the live physics-CONTROLLER position, never a
+ /// record snapshot) - here, Vector3.Distance between THIS
+ /// entity's own currently-accepted position (the exact field
+ /// BeginAcceptedPlacementCore/CanonicalSetupTableId already
+ /// trust: Snapshot.Physics?.Position ?? Snapshot.Position) and the
+ /// bound local-player controller position. Computed ONCE per
+ /// call, matching the one-shot-per-call granularity
+ /// already had before this slice (retail
+ /// recomputes player_distance per wire packet; refining this
+ /// executor to per-continuation freshness is out of C0-2's scope).
+ ///
+ 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);
+ }
+
+ ///
+ /// C0-1: reaches the full execution receipt/trace correlated with an
+ /// observed
+ /// receipt, purely via that receipt's own public
+ /// Token.Entity/Token.Sequence identity - the same identity
+ /// every other placement Kind is acknowledged by. Returns false for a
+ /// superseded/stale sequence under a reused entity key.
+ ///
+ 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;
+ }
+
+ ///
+ /// F2: reaps exactly one completion-receipt correlation entry, bound as
+ /// '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 's
+ /// own currency check).
+ ///
+ internal void ForgetCompletionReceipt(RuntimeEntityKey key, ulong sequence)
+ {
+ if (_completionReceipts.TryGetValue(
+ key,
+ out (ulong Sequence, RuntimeInitialCreateExecutionReceipt Receipt) entry)
+ && entry.Sequence == sequence)
+ {
+ _completionReceipts.Remove(key);
+ }
+ }
+
+ ///
+ /// F2: folded into /
+ /// IsConverged - an unacknowledged completion receipt is
+ /// outstanding host debt, mirroring
+ /// 's
+ /// existing "must be zero to converge" shape for the SAME underlying
+ /// receipt stream.
+ ///
+ internal int PendingCompletionReceiptCount => _completionReceipts.Count;
+
internal int ProgressCount => _progress.Count;
///
@@ -389,6 +559,15 @@ internal sealed class RuntimeInitialCreateContinuationExecutor
///
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
diff --git a/src/AcDream.Runtime/GameRuntime.cs b/src/AcDream.Runtime/GameRuntime.cs
index 08898a69..44a5bf5f 100644
--- a/src/AcDream.Runtime/GameRuntime.cs
+++ b/src/AcDream.Runtime/GameRuntime.cs
@@ -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,
diff --git a/src/AcDream.Runtime/Gameplay/RuntimeCharacterState.cs b/src/AcDream.Runtime/Gameplay/RuntimeCharacterState.cs
index f9acaf5a..587d1f92 100644
--- a/src/AcDream.Runtime/Gameplay/RuntimeCharacterState.cs
+++ b/src/AcDream.Runtime/Gameplay/RuntimeCharacterState.cs
@@ -18,7 +18,9 @@ public readonly record struct RuntimeCharacterOwnershipSnapshot(
int PositionCount,
int PropertyCount,
bool OptionsAreDefaults,
- bool MovementSkillsAreReset)
+ bool MovementSkillsAreReset,
+ /// C0-2: is back at retail's default ().
+ bool AutonomyIsDefault = true)
{
public bool IsConverged =>
IsDisposed
@@ -33,7 +35,8 @@ public readonly record struct RuntimeCharacterOwnershipSnapshot(
&& PositionCount == 0
&& PropertyCount == 0
&& OptionsAreDefaults
- && MovementSkillsAreReset;
+ && MovementSkillsAreReset
+ && AutonomyIsDefault;
}
///
@@ -47,11 +50,27 @@ public sealed class RuntimeCharacterState : IDisposable
public const uint RunSkillId = 24u;
/// ACE Skill enum ordinal for Jump (K-fix7 / pseudocode doc ยง5).
public const uint JumpSkillId = 22u;
+ ///
+ /// C0-2/F5(b): retail CommandInterpreter's own default, set at
+ /// construction (pseudo-C 699752, this->autonomy_level = 2;, a
+ /// direct field write, not a SetAutonomyLevel call) and by the
+ /// command-line-only override at admission
+ /// (command_line_autonomy_level, pseudo-C 1088429, itself
+ /// defaulting to 0x2). Exactly ONE retail caller of
+ /// CommandInterpreter::SetAutonomyLevel exists in the named
+ /// retail decomp - the startup construction path at pseudo-C 94102
+ /// (cmdinterp->vtable->SetAutonomyLevel(cmdinterp, command_line_autonomy_level))
+ /// - 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.
+ ///
+ public const uint FullAutonomyLevel = 2u;
private bool _disposed;
private long _characterRevision;
private long _spellbookRevision;
private bool _internalSubscriptionsAttached;
+ private uint _autonomyLevel = FullAutonomyLevel;
///
/// Campaign P Slice P1 (2026-07-30): the pre-EnchantSkill base
@@ -88,6 +107,39 @@ public sealed class RuntimeCharacterState : IDisposable
public IRuntimeCharacterView View { get; }
public bool IsDisposed => _disposed;
+ /// Retail CommandInterpreter::GetAutonomyLevel.
+ public uint AutonomyLevel => Volatile.Read(ref _autonomyLevel);
+
+ ///
+ /// C0-2: retail CommandInterpreter::UsePositionFromServer
+ /// (pseudo-C 699506-699512: result = this->autonomy_level != 2;).
+ /// This is the source
+ ///
+ /// binds for RuntimeInitialCreateExecutionInputs.UsePositionFromServer
+ /// - the local-player-only interpolate gate consumed by
+ /// RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition.
+ ///
+ public bool UsePositionFromServer => AutonomyLevel != FullAutonomyLevel;
+
+ ///
+ /// Retail CommandInterpreter::SetAutonomyLevel (pseudo-C
+ /// 699542-699552): rejects any value above 2, otherwise commits.
+ /// F5(a): retail's own setter ALSO sends SendAutonomyLevelEvent
+ /// (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.
+ ///
+ 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);
}
///
@@ -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
diff --git a/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs b/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs
index 4f6dc5bb..68efb27f 100644
--- a/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs
+++ b/src/AcDream.Runtime/Physics/RuntimeSetPositionState.cs
@@ -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,
+ ///
+ /// 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.
+ ///
+ 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? _executorCompletionAcknowledged;
private bool _disposed;
private readonly record struct LostDeadlineEntry(
@@ -770,6 +783,87 @@ internal sealed class RuntimeSetPositionState : IDisposable
}
}
+ ///
+ /// 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.
+ ///
+ internal RuntimePlacementProjectionToken PublishExecutorCompletion(
+ RuntimeEntityRecord record,
+ Action? 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;
+ }
+
+ ///
+ /// 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).
+ ///
+ internal void BindExecutorCompletionAcknowledgement(
+ Action 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;
}
+ ///
+ /// 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
+ /// - the SAME field
+ /// 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
+ /// () instead of
+ /// reading anything; a record WITH an id but an unavailable/corrupt
+ /// asynchronous read yields
+ /// so the caller retries once the prepared-collision package lands,
+ /// mirroring 's own
+ /// doc-comment distinction between "not arrived yet" and "resolved
+ /// absent". Dormant: internal, no production caller - a residence
+ /// lease's own Placement/Route.OperationKind/
+ /// Route.SetPositionFlags are exactly the token/kind/flags this
+ /// takes.
+ ///
+ 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 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(
diff --git a/tests/AcDream.App.Tests/World/RuntimePlacementPresentationSinkTests.cs b/tests/AcDream.App.Tests/World/RuntimePlacementPresentationSinkTests.cs
index 7c6e252f..1d293203 100644
--- a/tests/AcDream.App.Tests/World/RuntimePlacementPresentationSinkTests.cs
+++ b/tests/AcDream.App.Tests/World/RuntimePlacementPresentationSinkTests.cs
@@ -155,6 +155,44 @@ public sealed class RuntimePlacementPresentationSinkTests
Assert.Equal(priorVisible, record.IsSpatiallyVisible);
}
+ [Fact]
+ public void ExecutorCompleted_IsAckOnlyNoOpEvenWhenTokenIsStaleOrSidecarIsGone()
+ {
+ // F1: mirrors Discard_IsAckOnlyNoOpEvenWhenTokenIsStaleOrSidecarIsGone
+ // exactly - proves ExecutorCompleted is acknowledged unconditionally
+ // (never wedges the FIFO on a record-lookup failure) and never
+ // mutates presentation state, even under a completely bogus token.
+ Fixture fixture = Fixture.Create();
+ LiveEntityRecord record = fixture.Materialize(Spawn(Guid, 1, SourceCell));
+ WorldEntity entity = record.WorldEntity!;
+ RuntimePlacementProjectionSnapshot completion = Placement(
+ fixture,
+ record,
+ RuntimePlacementProjectionKind.ExecutorCompleted,
+ new Vector3(900f),
+ Quaternion.CreateFromAxisAngle(Vector3.UnitX, 1f)) with
+ {
+ Token = Placement(fixture, record,
+ RuntimePlacementProjectionKind.Place,
+ Vector3.Zero,
+ Quaternion.Identity).Token with
+ {
+ SessionLifetimeVersion = ulong.MaxValue,
+ PositionAuthorityVersion = ulong.MaxValue,
+ ExactCellId = 0xDEAD0001u,
+ },
+ };
+ Vector3 priorPosition = entity.Position;
+ Quaternion priorRotation = entity.Rotation;
+ bool priorVisible = record.IsSpatiallyVisible;
+
+ Assert.True(fixture.Sink.TryApply(in completion));
+
+ Assert.Equal(priorPosition, entity.Position);
+ Assert.Equal(priorRotation, entity.Rotation);
+ Assert.Equal(priorVisible, record.IsSpatiallyVisible);
+ }
+
[Fact]
public void Place_RejectsStaleCanonicalVersionsWithoutChangingSidecar()
{
diff --git a/tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs b/tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs
index 131b4b6e..7e4c2886 100644
--- a/tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs
+++ b/tests/AcDream.Headless.Tests/HeadlessSessionHostTests.cs
@@ -479,6 +479,56 @@ public sealed class HeadlessSessionHostTests
Assert.True(projection.TryApply(in discard));
}
+ [Fact]
+ public void ExecutorCompletedReceiptIsAcknowledgeOnlyRegardlessOfRecordValidity()
+ {
+ // F1: mirrors PlacementReceiptUsesExactIncarnationAndDiscardIsAckOnly's
+ // stale-token half - proves ExecutorCompleted is acknowledged
+ // unconditionally (never gated by the record-lookup/portal-shape
+ // checks Place/Withdraw depend on), so a genuinely stale/mismatched
+ // token can never wedge the FIFO behind it.
+ var operations = new FixtureSessionOperations();
+ using var credential = new HeadlessCredentialSecret(
+ "fixture",
+ "password");
+ using var host = new HeadlessSessionHost(
+ Descriptor(),
+ credential,
+ new HeadlessDiagnosticWriter(TextWriter.Null),
+ operations);
+ GameRuntime runtime = host.Runtime;
+ RuntimeEntityRecord record = runtime.EntityObjects
+ .RegisterEntity(Spawn(0x50000006u))
+ .Canonical!;
+ Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
+ record,
+ record.CreateIntegrationVersion,
+ record.Snapshot,
+ replaceGeneration: false));
+ var sink = new HeadlessRuntimePlacementProjectionSink(runtime);
+ RuntimePlacementProjectionSnapshot completion = Placement(
+ runtime,
+ record,
+ RuntimePlacementProjectionKind.ExecutorCompleted,
+ Vector3.One,
+ Quaternion.Identity);
+ RuntimePlacementProjectionSnapshot stale = completion with
+ {
+ Token = completion.Token with
+ {
+ Entity = completion.Token.Entity with
+ {
+ Incarnation = unchecked((ushort)(
+ completion.Token.Entity.Incarnation + 1)),
+ },
+ SessionLifetimeVersion = ulong.MaxValue,
+ },
+ };
+
+ Assert.True(sink.TryApply(in completion));
+ Assert.True(sink.TryApply(in stale));
+ }
+
[Fact]
public void SessionEventRouteOwnsOneObserverAndUnsubscribesBeforeNetworkDetach()
{
diff --git a/tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateContinuationExecutorTests.cs b/tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateContinuationExecutorTests.cs
index ab1b8a9b..fb81a88c 100644
--- a/tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateContinuationExecutorTests.cs
+++ b/tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateContinuationExecutorTests.cs
@@ -4223,6 +4223,480 @@ public sealed class RuntimeInitialCreateContinuationExecutorTests
Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership().ActiveOperationCount);
}
+ // ---------------------------------------------------------------
+ // F. C0-1: executor-completion bridge / C0-2: live-input binding
+ // ---------------------------------------------------------------
+
+ [Fact]
+ public void ExecutorCompletion_PublishesOnTheSamePlacementStreamCorrelatedWithTheFullReceipt()
+ {
+ using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
+ Bind(lifetime, 400UL);
+ const uint guid = 0x70024000u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease lease));
+ AttachDormantBody(lifetime, canonical);
+ CompleteInitialPlacement(lifetime, lease);
+
+ var observed = new List();
+ using IDisposable subscription = lifetime.Events.SubscribePlacement(
+ new PlacementObserver(delta => observed.Add(delta.Placement)));
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, NoContact);
+
+ RuntimePlacementProjectionSnapshot completion = Assert.Single(observed);
+ Assert.Equal(
+ RuntimePlacementProjectionKind.ExecutorCompleted,
+ completion.Kind);
+ Assert.Equal(canonical.Key, completion.Token.Entity);
+ Assert.True(lifetime.InitialCreateExecution.TryGetCompletionReceipt(
+ completion.Token,
+ out RuntimeInitialCreateExecutionReceipt correlated));
+ Assert.Equal(receipt, correlated);
+
+ // Acknowledge-only, exact-head, same as every other Kind.
+ Assert.Equal(1, lifetime.Physics.SetPosition.PendingProjectionCount);
+ Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
+ completion.Token));
+ Assert.Equal(0, lifetime.Physics.SetPosition.PendingProjectionCount);
+ }
+
+ [Fact]
+ public void ExecutorCompletion_ObservedOnlyAfterAnyContinuationPlacementInFifoOrder()
+ {
+ using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
+ Bind(lifetime, 401UL);
+ const uint guid = 0x70024001u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease lease));
+ AttachDormantBody(lifetime, canonical);
+ CompleteInitialPlacement(lifetime, lease);
+
+ // A teleport-advanced Position continuation performs its OWN
+ // authored SetPosition - the continuation placement C0-1's contract
+ // says already flows through the channel unchanged.
+ WorldSession.EntityPositionUpdate update = PositionUpdate(
+ guid, positionSequence: 2, teleportSequence: 1,
+ forcePositionSequence: 0, positionX: 40f);
+ Assert.True(lifetime.TryApplyPosition(
+ update, isLocalPlayer: true, null, null, true, null,
+ out PositionTimestampDisposition disposition, out _, out _));
+ Assert.Equal(PositionTimestampDisposition.Apply, disposition);
+
+ var observed = new List();
+ using IDisposable subscription = lifetime.Events.SubscribePlacement(
+ new PlacementObserver(delta => observed.Add(delta.Placement)));
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, NoContact);
+
+ Assert.Equal(
+ [
+ RuntimePlacementProjectionKind.Place,
+ RuntimePlacementProjectionKind.ExecutorCompleted,
+ ],
+ observed.Select(static s => s.Kind));
+ Assert.Equal(canonical.Key, observed[0].Token.Entity);
+ Assert.True(lifetime.InitialCreateExecution.TryGetCompletionReceipt(
+ observed[1].Token,
+ out RuntimeInitialCreateExecutionReceipt correlated));
+ Assert.Equal(receipt, correlated);
+ // The continuation Place receipt was already acknowledged by
+ // RunToCompletion's own CompletePendingContinuationPlacement helper
+ // before Execute ever reached Completed - only the fresh
+ // ExecutorCompleted receipt is still outstanding.
+ Assert.Equal(1, lifetime.Physics.SetPosition.PendingProjectionCount);
+ Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
+ observed[1].Token));
+ }
+
+ [Fact]
+ public void ExecutorCompletion_ReceiptIsReadableFromWithinTheSameSynchronousOnPlacementDispatch()
+ {
+ // F2: registration must happen BEFORE PublishPlacement's synchronous
+ // observer dispatch - a subscriber reading the correlation back from
+ // inside its OWN OnPlacement callback must already find it, not only
+ // after RunToCompletion returns.
+ using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
+ Bind(lifetime, 410UL);
+ const uint guid = 0x70024010u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease lease));
+ AttachDormantBody(lifetime, canonical);
+ CompleteInitialPlacement(lifetime, lease);
+
+ RuntimeInitialCreateExecutionReceipt? observedFromInsideDispatch = null;
+ using IDisposable subscription = lifetime.Events.SubscribePlacement(
+ new PlacementObserver(delta =>
+ {
+ if (delta.Placement.Kind
+ is not RuntimePlacementProjectionKind.ExecutorCompleted)
+ {
+ return;
+ }
+ Assert.True(lifetime.InitialCreateExecution.TryGetCompletionReceipt(
+ delta.Placement.Token,
+ out RuntimeInitialCreateExecutionReceipt receipt));
+ observedFromInsideDispatch = receipt;
+ }));
+
+ RuntimeInitialCreateExecutionReceipt receiptReturned = RunToCompletion(
+ lifetime, canonical, lease.Token, NoContact);
+
+ Assert.NotNull(observedFromInsideDispatch);
+ Assert.Equal(receiptReturned, observedFromInsideDispatch!.Value);
+ }
+
+ [Fact]
+ public void ExecutorCompletion_ConvergenceLedgerCountsAnUnacknowledgedReceiptAsOutstandingDebtUntilAcknowledged()
+ {
+ // F2: PendingCompletionReceiptCount mirrors
+ // RuntimeSetPositionOwnershipSnapshot.PendingProjectionAcknowledgementCount's
+ // existing "unacknowledged receipt is outstanding debt" shape for
+ // the SAME underlying receipt stream - non-zero while unacknowledged,
+ // reaped to zero exactly on acknowledge (never before, never left
+ // dangling after).
+ using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
+ Bind(lifetime, 411UL);
+ const uint guid = 0x70024011u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease lease));
+ AttachDormantBody(lifetime, canonical);
+ CompleteInitialPlacement(lifetime, lease);
+ Assert.Equal(
+ 0,
+ lifetime.CaptureOwnership().PendingCompletionReceiptCount);
+
+ RunToCompletion(lifetime, canonical, lease.Token, NoContact);
+
+ Assert.Equal(
+ 1,
+ lifetime.CaptureOwnership().PendingCompletionReceiptCount);
+ Assert.True(lifetime.Physics.SetPosition.TryPeekProjection(
+ out RuntimePlacementProjectionSnapshot completion));
+ Assert.Equal(
+ RuntimePlacementProjectionKind.ExecutorCompleted,
+ completion.Kind);
+
+ Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
+ completion.Token));
+
+ Assert.Equal(
+ 0,
+ lifetime.CaptureOwnership().PendingCompletionReceiptCount);
+ Assert.False(lifetime.InitialCreateExecution.TryGetCompletionReceipt(
+ completion.Token,
+ out _));
+ }
+
+ [Fact]
+ public void ExecutorCompletion_CorrelationEntryIsReapedByDiscardProgress()
+ {
+ // F2: DiscardProgress (reached via ForgetInitialCreateResidence in
+ // production) must reap this correlation cache too - it is exactly
+ // the kind of executor-introduced state that method already owns
+ // cleaning up. The drain already removed _progress[key] before
+ // publishing the completion (see ExecuteCore's Released case), so
+ // this proves DiscardProgress reaps _completionReceipts
+ // UNCONDITIONALLY, not only when _progress still tracks the key.
+ // Left deliberately UNACKNOWLEDGED in _pendingProjection (a single-
+ // entity lifetime, so there is no exact-head contention to worry
+ // about) - proving DiscardProgress reaps the correlation cache
+ // independently of the normal acknowledge path.
+ using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
+ Bind(lifetime, 413UL);
+ const uint guid = 0x70024013u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease lease));
+ AttachDormantBody(lifetime, canonical);
+ CompleteInitialPlacement(lifetime, lease);
+ RunToCompletion(lifetime, canonical, lease.Token, NoContact);
+ Assert.Equal(
+ 1,
+ lifetime.CaptureOwnership().PendingCompletionReceiptCount);
+
+ lifetime.InitialCreateExecution.DiscardProgress(canonical.Key!.Value);
+
+ Assert.Equal(
+ 0,
+ lifetime.CaptureOwnership().PendingCompletionReceiptCount);
+ }
+
+ [Fact]
+ public void ExecutorCompletion_CorrelationEntryIsReapedByDiscardAll()
+ {
+ // F2: a full session clear (DiscardAll, reached via
+ // RuntimeInitialCreateResidenceState.Clear's call site) must never
+ // carry this correlation cache across a reset.
+ using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
+ Bind(lifetime, 414UL);
+ const uint guid = 0x70024014u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease lease));
+ AttachDormantBody(lifetime, canonical);
+ CompleteInitialPlacement(lifetime, lease);
+ RunToCompletion(lifetime, canonical, lease.Token, NoContact);
+ Assert.Equal(
+ 1,
+ lifetime.CaptureOwnership().PendingCompletionReceiptCount);
+
+ lifetime.InitialCreateExecution.DiscardAll();
+
+ Assert.Equal(
+ 0,
+ lifetime.CaptureOwnership().PendingCompletionReceiptCount);
+ }
+
+ [Fact]
+ public void BindLiveInputs_DrivesClassificationFromTheBoundSourcesInsteadOfTheCallerStruct()
+ {
+ using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
+ Bind(lifetime, 402UL);
+ const uint guid = 0x70024002u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease lease));
+ AttachDormantBody(lifetime, canonical);
+ CompleteInitialPlacement(lifetime, lease);
+
+ WorldSession.EntityPositionUpdate update = PositionUpdate(
+ guid, positionSequence: 2, teleportSequence: 0,
+ forcePositionSequence: 0, positionX: 15f, isGrounded: true);
+ Assert.True(lifetime.TryApplyPosition(
+ update, isLocalPlayer: true, null, null, false, null,
+ out PositionTimestampDisposition disposition, out _, out _));
+ Assert.Equal(PositionTimestampDisposition.Apply, disposition);
+
+ bool usePositionFromServer = true;
+ // Position 15 units from a local player parked far away (100 units)
+ // is irrelevant here (PlayerDistance only matters for the
+ // Remote/Projectile near/far branch, not LocalPlayer's own
+ // interpolate gate) - it exists purely to prove the DISTANCE source
+ // is read at all.
+ lifetime.InitialCreateExecution.BindLiveInputs(
+ () => usePositionFromServer,
+ () => new Vector3(115f, 20f, 7f));
+
+ // The caller-supplied struct says UsePositionFromServer:false - if
+ // the bound source is actually driving classification, retail's
+ // "UsePositionFromServer && wire-contact" local-ordinary gate must
+ // still interpolate.
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ lifetime, canonical, lease.Token, NoContact);
+ RuntimeInitialCreateExecutedAction positionAction = Assert.Single(
+ receipt.Trace,
+ static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Position);
+ Assert.Equal(
+ RuntimeAuthoritativePositionDisposition.Interpolate,
+ positionAction.PositionDisposition);
+
+ // C0-1: Completing the first entity's drain also published its own
+ // ExecutorCompleted receipt on the SAME exact-head stream - it must
+ // be acknowledged before a SECOND entity's own placement receipt can
+ // ever become the head.
+ Assert.True(lifetime.Physics.SetPosition.TryPeekProjection(
+ out RuntimePlacementProjectionSnapshot firstCompletion));
+ Assert.Equal(
+ RuntimePlacementProjectionKind.ExecutorCompleted,
+ firstCompletion.Kind);
+ Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
+ firstCompletion.Token));
+
+ // Flip the bound source off and confirm the SAME struct now takes
+ // the non-interpolating branch - proves it is read live, not cached
+ // at bind time.
+ usePositionFromServer = false;
+ const uint secondGuid = 0x70024003u;
+ RuntimeEntityRecord second = lifetime
+ .RegisterEntityWithInitialResidence(Spawn(secondGuid, 1), isLocalPlayer: true)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ second,
+ out RuntimeInitialCreateResidenceLease secondLease));
+ AttachDormantBody(lifetime, second);
+ CompleteInitialPlacement(lifetime, secondLease);
+ WorldSession.EntityPositionUpdate secondUpdate = PositionUpdate(
+ secondGuid, positionSequence: 2, teleportSequence: 0,
+ forcePositionSequence: 0, positionX: 16f, isGrounded: true);
+ Assert.True(lifetime.TryApplyPosition(
+ secondUpdate, isLocalPlayer: true, null, null, false, null,
+ out PositionTimestampDisposition secondDisposition, out _, out _));
+ Assert.Equal(PositionTimestampDisposition.Apply, secondDisposition);
+ RuntimeInitialCreateExecutionReceipt secondReceipt = RunToCompletion(
+ lifetime, second, secondLease.Token, NoContact);
+ RuntimeInitialCreateExecutedAction secondPositionAction = Assert.Single(
+ secondReceipt.Trace,
+ static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Position);
+ Assert.Equal(
+ RuntimeAuthoritativePositionDisposition.NoPositionOperation,
+ secondPositionAction.PositionDisposition);
+ }
+
+ [Fact]
+ public void BindLiveInputs_ThrowsOnASecondBindAndUnboundExecutorsUseTheCallerStructUnchanged()
+ {
+ using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
+ lifetime.InitialCreateExecution.BindLiveInputs(
+ static () => true, static () => Vector3.Zero);
+ Assert.Throws(() =>
+ lifetime.InitialCreateExecution.BindLiveInputs(
+ static () => false, static () => Vector3.Zero));
+
+ // A SEPARATE, never-bound lifetime still honors the caller-supplied
+ // struct verbatim - the existing bare-lifetime test contract is
+ // unchanged by this slice.
+ using RuntimeEntityObjectLifetime unbound = EngineLifetime();
+ Bind(unbound, 403UL);
+ const uint guid = 0x70024004u;
+ RuntimeEntityRecord canonical = unbound
+ .RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
+ .Canonical!;
+ Assert.True(unbound.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease lease));
+ AttachDormantBody(unbound, canonical);
+ CompleteInitialPlacement(unbound, lease);
+ WorldSession.EntityPositionUpdate update = PositionUpdate(
+ guid, positionSequence: 2, teleportSequence: 0,
+ forcePositionSequence: 0, positionX: 15f, isGrounded: true);
+ Assert.True(unbound.TryApplyPosition(
+ update, isLocalPlayer: true, null, null, false, null,
+ out PositionTimestampDisposition disposition, out _, out _));
+ Assert.Equal(PositionTimestampDisposition.Apply, disposition);
+
+ RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
+ unbound, canonical, lease.Token, NoContact);
+ RuntimeInitialCreateExecutedAction positionAction = Assert.Single(
+ receipt.Trace,
+ static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Position);
+ // NoContact (UsePositionFromServer:false) -> not interpolated.
+ Assert.Equal(
+ RuntimeAuthoritativePositionDisposition.NoPositionOperation,
+ positionAction.PositionDisposition);
+ }
+
+ [Fact]
+ public void BindLiveInputs_PlayerDistanceIsReadFromANonNullBoundSourceAndFallsBackToTheCallerStructWhenNull()
+ {
+ // F3: proves BOTH directions of the nullable local-player-position
+ // source. Entity 1: the bound source returns a REAL near position -
+ // the caller struct claims a FAR distance (200f), so if the bound
+ // source is genuinely read (not ignored), the entity's own near
+ // distance must win and classify Interpolate. Entity 2: the SAME
+ // bound source now returns null (e.g. the login-window drain before
+ // RuntimeLocalPlayerMovementState.Controller exists) - it must fall
+ // back to the caller struct's FAR distance exactly like an unbound
+ // source would, never fabricate Vector3.Zero (which would compute a
+ // small, misleadingly-near distance to the entity's own position and
+ // wrongly classify Interpolate instead of the far SetPositionSimple
+ // hard-snap).
+ using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
+ Bind(lifetime, 421UL);
+ Vector3? boundPosition = new Vector3(30f, 20f, 7f);
+ lifetime.InitialCreateExecution.BindLiveInputs(
+ static () => false,
+ () => boundPosition);
+
+ const uint nearGuid = 0x70024021u;
+ RuntimeEntityRecord near = lifetime
+ .RegisterEntityWithInitialResidence(Spawn(nearGuid, 1), isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ near, out RuntimeInitialCreateResidenceLease nearLease));
+ AttachDormantBody(lifetime, near);
+ CompleteInitialPlacement(lifetime, nearLease);
+ WorldSession.EntityPositionUpdate nearUpdate = PositionUpdate(
+ nearGuid, positionSequence: 2, teleportSequence: 0,
+ forcePositionSequence: 0, positionX: 25f, isGrounded: true);
+ Assert.True(lifetime.TryApplyPosition(
+ nearUpdate, isLocalPlayer: false, null, null, false, null,
+ out PositionTimestampDisposition nearDisposition, out _, out _));
+ Assert.Equal(PositionTimestampDisposition.Apply, nearDisposition);
+ var farStruct = new RuntimeInitialCreateExecutionInputs(
+ UsePositionFromServer: false, PlayerDistance: 200f);
+
+ RuntimeInitialCreateExecutionReceipt nearReceipt = RunToCompletion(
+ lifetime, near, nearLease.Token, farStruct);
+
+ RuntimeInitialCreateExecutedAction nearAction = Assert.Single(
+ nearReceipt.Trace,
+ static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Position);
+ Assert.Equal(
+ RuntimeAuthoritativePositionDisposition.Interpolate,
+ nearAction.PositionDisposition);
+
+ // C0-1: the completed near entity published its own ExecutorCompleted
+ // receipt on the SAME exact-head stream - acknowledge it before the
+ // far entity's own Place receipt can ever become the head.
+ Assert.True(lifetime.Physics.SetPosition.TryPeekProjection(
+ out RuntimePlacementProjectionSnapshot nearCompletion));
+ Assert.Equal(
+ RuntimePlacementProjectionKind.ExecutorCompleted,
+ nearCompletion.Kind);
+ Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
+ nearCompletion.Token));
+
+ boundPosition = null;
+ const uint farGuid = 0x70024022u;
+ RuntimeEntityRecord far = lifetime
+ .RegisterEntityWithInitialResidence(Spawn(farGuid, 1), isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ far, out RuntimeInitialCreateResidenceLease farLease));
+ AttachDormantBody(lifetime, far);
+ CompleteInitialPlacement(lifetime, farLease);
+ WorldSession.EntityPositionUpdate farUpdate = PositionUpdate(
+ farGuid, positionSequence: 2, teleportSequence: 0,
+ forcePositionSequence: 0, positionX: 25f, isGrounded: true);
+ Assert.True(lifetime.TryApplyPosition(
+ farUpdate, isLocalPlayer: false, null, null, false, null,
+ out PositionTimestampDisposition farDisposition, out _, out _));
+ Assert.Equal(PositionTimestampDisposition.Apply, farDisposition);
+
+ RuntimeInitialCreateExecutionStatus farStatus = lifetime
+ .InitialCreateExecution.Execute(
+ far, farLease.Token, farStruct, out _);
+
+ Assert.Equal(
+ RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement,
+ farStatus);
+ RuntimeEntityKey farKey = far.Key!.Value;
+ Assert.True(lifetime.InitialCreateExecution.TryGetPendingContinuationRoute(
+ farKey, out RuntimeAuthoritativePositionRoute farRoute));
+ Assert.Equal(
+ RuntimeAuthoritativePositionDisposition.SetPositionSimple,
+ farRoute.Disposition);
+ Assert.True(farRoute.StopInterpolating);
+ }
+
// ---------------------------------------------------------------
// Harness
// ---------------------------------------------------------------
@@ -4461,4 +4935,12 @@ public sealed class RuntimeInitialCreateContinuationExecutorTests
{
}
}
+
+ private sealed class PlacementObserver(
+ Action onPlacement)
+ : IRuntimePlacementObserver
+ {
+ public void OnPlacement(in RuntimePlacementDelta delta) =>
+ onPlacement(delta);
+ }
}
diff --git a/tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateResidenceStateTests.cs b/tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateResidenceStateTests.cs
index 046333a4..57ed44e8 100644
--- a/tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateResidenceStateTests.cs
+++ b/tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateResidenceStateTests.cs
@@ -2379,6 +2379,82 @@ public sealed class RuntimeInitialCreateResidenceStateTests
Assert.Single(retained.Continuations).Kind);
}
+ // ---------------------------------------------------------------
+ // C0-4: TryCommitParent/CommitWithdrawal cancellation asymmetries
+ // ---------------------------------------------------------------
+
+ [Fact]
+ public void TryCommitParent_CancelsActiveInitialResidenceAndItsPendingPlacement()
+ {
+ var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() };
+ engine.AddLandblock(
+ Landblock,
+ new TerrainSurface(new byte[81], new float[256]),
+ Array.Empty(),
+ Array.Empty(),
+ worldOffsetX: 0f,
+ worldOffsetY: 0f);
+ using var lifetime = new RuntimeEntityObjectLifetime(engine);
+ Bind(lifetime, 200UL);
+ const uint guid = 0x70004001u;
+ RuntimeEntityRecord canonical = lifetime
+ .RegisterEntityWithInitialResidence(
+ Spawn(guid, 1, setupId: null),
+ isLocalPlayer: false)
+ .Canonical!;
+ Assert.True(lifetime.TryGetInitialCreateResidence(
+ canonical,
+ out RuntimeInitialCreateResidenceLease lease));
+ // Unparented -> the classifier's route performs SetPosition, so
+ // Own() has already begun the lease's own placement operation.
+ // Submit it (a live Place receipt now sits unacknowledged) so
+ // cancellation has an ACTUAL outstanding receipt to discard, not
+ // merely an unpublished AwaitingPreparation operation.
+ Assert.True(lease.Placement.IsValid);
+ AttachDormantBody(lifetime, canonical);
+ RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition
+ .SubmitPreparedPlacement(
+ lease.Placement,
+ Prepare(lifetime, lease, RuntimeSetPositionMoverSetup.ResolvedAbsent));
+ Assert.Equal(
+ RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
+ outcome.Status);
+ Assert.Equal(1, lifetime.Physics.SetPosition.PendingProjectionCount);
+
+ var discards = new List();
+ using IDisposable subscription = lifetime.Events.SubscribePlacement(
+ new PlacementObserver(delta =>
+ {
+ if (delta.Placement.Kind is RuntimePlacementProjectionKind.Discard)
+ discards.Add(delta.Placement);
+ }));
+
+ var relation = new ParentAttachmentRelation(
+ ParentGuid: 0x70004100u,
+ ChildGuid: guid,
+ ParentLocation: 1u,
+ PlacementId: 1u,
+ ParentInstanceSequence: 1,
+ ChildPositionSequence: 1);
+ Assert.True(lifetime.TryCommitParent(relation, null, out _));
+
+ Assert.Equal(0, lifetime.CaptureOwnership()
+ .InitialCreateResidenceLeaseCount);
+ Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership()
+ .ActiveOperationCount);
+ // The old Place receipt is REPLACED by a Discard at the same
+ // sequence, not removed outright - still awaiting host ack.
+ Assert.Equal(1, lifetime.Physics.SetPosition.PendingProjectionCount);
+ Assert.False(lifetime.Physics.SetPosition.IsPlacementCurrent(
+ lease.Placement));
+ RuntimePlacementProjectionSnapshot discard = Assert.Single(discards);
+ Assert.Equal(canonical.Key, discard.Token.Entity);
+ Assert.Equal(outcome.Projection.Sequence, discard.Token.Sequence);
+ Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
+ discard.Token));
+ Assert.Equal(0, lifetime.Physics.SetPosition.PendingProjectionCount);
+ }
+
private static RuntimeSetPositionCommand Prepare(
RuntimeEntityObjectLifetime lifetime,
in RuntimeInitialCreateResidenceLease lease,
diff --git a/tests/AcDream.Runtime.Tests/Gameplay/RuntimeCharacterStateTests.cs b/tests/AcDream.Runtime.Tests/Gameplay/RuntimeCharacterStateTests.cs
index eee36fca..9179d1e9 100644
--- a/tests/AcDream.Runtime.Tests/Gameplay/RuntimeCharacterStateTests.cs
+++ b/tests/AcDream.Runtime.Tests/Gameplay/RuntimeCharacterStateTests.cs
@@ -340,6 +340,51 @@ public sealed class RuntimeCharacterStateTests
Assert.False(state.CaptureOwnership().MovementSkillsAreReset);
}
+ // ---------------------------------------------------------------
+ // C0-2: retail CommandInterpreter::autonomy_level/UsePositionFromServer
+ // ---------------------------------------------------------------
+
+ [Fact]
+ public void AutonomyLevel_DefaultsToFullAndMirrorsRetailUsePositionFromServer()
+ {
+ using var state = new RuntimeCharacterState();
+
+ Assert.Equal(RuntimeCharacterState.FullAutonomyLevel, state.AutonomyLevel);
+ Assert.False(state.UsePositionFromServer);
+ Assert.True(state.CaptureOwnership().AutonomyIsDefault);
+
+ Assert.True(state.TrySetAutonomyLevel(0u));
+ Assert.Equal(0u, state.AutonomyLevel);
+ Assert.True(state.UsePositionFromServer);
+ Assert.False(state.CaptureOwnership().AutonomyIsDefault);
+
+ Assert.True(state.TrySetAutonomyLevel(1u));
+ Assert.True(state.UsePositionFromServer);
+
+ // Retail's SetAutonomyLevel rejects anything above 2; the level and
+ // the derived UsePositionFromServer gate stay exactly as they were.
+ Assert.False(state.TrySetAutonomyLevel(3u));
+ Assert.Equal(1u, state.AutonomyLevel);
+
+ Assert.True(state.TrySetAutonomyLevel(RuntimeCharacterState.FullAutonomyLevel));
+ Assert.False(state.UsePositionFromServer);
+ Assert.True(state.CaptureOwnership().AutonomyIsDefault);
+ }
+
+ [Fact]
+ public void ResetSession_RestoresAutonomyLevelToFull()
+ {
+ using var state = new RuntimeCharacterState();
+ Assert.True(state.TrySetAutonomyLevel(0u));
+ Assert.True(state.UsePositionFromServer);
+
+ state.ResetSession();
+
+ Assert.Equal(RuntimeCharacterState.FullAutonomyLevel, state.AutonomyLevel);
+ Assert.False(state.UsePositionFromServer);
+ Assert.True(state.CaptureOwnership().AutonomyIsDefault);
+ }
+
private static ActiveEnchantmentRecord MakeVitae(uint spellId, float val) =>
new(
spellId, LayerId: 0u, Duration: -1f, CasterGuid: 0u,
diff --git a/tests/AcDream.Runtime.Tests/Physics/RuntimeSetPositionStateTests.cs b/tests/AcDream.Runtime.Tests/Physics/RuntimeSetPositionStateTests.cs
index c3c34ab2..7b685664 100644
--- a/tests/AcDream.Runtime.Tests/Physics/RuntimeSetPositionStateTests.cs
+++ b/tests/AcDream.Runtime.Tests/Physics/RuntimeSetPositionStateTests.cs
@@ -1,5 +1,7 @@
using System.Collections.Immutable;
using System.Numerics;
+using AcDream.Content;
+using AcDream.Content.Pak;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
@@ -2247,6 +2249,263 @@ public sealed class RuntimeSetPositionStateTests
placed.Token));
}
+ // ---------------------------------------------------------------
+ // C0-3: exact-Setup mover chain end-to-end
+ // ---------------------------------------------------------------
+
+ [Fact]
+ public void TryPrepareAndSubmitAuthoredPlacement_ChainsSetupReadThroughPrepareMoverToSubmit()
+ {
+ PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
+ using var lifetime = new RuntimeEntityObjectLifetime(engine);
+ RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001101u, 1);
+ AttachBody(lifetime, record, SourceCell);
+ RuntimeEntityPlacementToken token = lifetime.Physics.SetPosition
+ .BeginAuthoredPlacement(
+ record,
+ record.PositionAuthorityVersion,
+ RuntimeSetPositionOperationKind.RemoteAuthoritative);
+ Assert.True(token.IsValid);
+
+ // Spawn's own default SetupTableId (0x02000001u) - the exact
+ // CanonicalSetupTableId CapturePreparationAuthority already trusts;
+ // the chain must read THIS id, not a caller-supplied one.
+ ImmutableArray spheres =
+ [
+ new FlatCollisionSphere(new Vector3(0f, 0f, 0.5f), 0.4f),
+ new FlatCollisionSphere(new Vector3(0f, 0f, 1.2f), 0.4f),
+ ];
+ var source = new FakeCollisionSource(
+ 0x02000001u,
+ new FlatSetupCollision(
+ ImmutableArray.Empty,
+ spheres,
+ height: 0f,
+ radius: 0f,
+ stepUpHeight: 0.4f,
+ stepDownHeight: 0.35f));
+
+ Assert.Equal(
+ RuntimeSetPositionMoverPreparationStatus.Prepared,
+ lifetime.Physics.SetPosition.TryPrepareAndSubmitAuthoredPlacement(
+ record,
+ token,
+ RuntimeSetPositionOperationKind.RemoteAuthoritative,
+ PhysicsSetPositionFlags.Placement
+ | PhysicsSetPositionFlags.Slide,
+ source,
+ gameTime: 10d,
+ out RuntimeSetPositionOutcome outcome));
+
+ Assert.Equal(
+ RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
+ outcome.Status);
+ Assert.Equal(1, source.ReadCount);
+ Assert.True(lifetime.Physics.SetPosition.TryGetPreparedMoverSphereCount(
+ record,
+ out int sphereCount));
+ Assert.Equal(2, sphereCount);
+ Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
+ outcome.Projection));
+ }
+
+ [Fact]
+ public void TryPrepareAndSubmitAuthoredPlacement_YieldsRetryOnAMissingSetupReadWithoutMutatingStage()
+ {
+ PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
+ using var lifetime = new RuntimeEntityObjectLifetime(engine);
+ RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001102u, 1);
+ AttachBody(lifetime, record, SourceCell);
+ RuntimeEntityPlacementToken token = lifetime.Physics.SetPosition
+ .BeginAuthoredPlacement(
+ record,
+ record.PositionAuthorityVersion,
+ RuntimeSetPositionOperationKind.RemoteAuthoritative);
+ Assert.True(token.IsValid);
+
+ var source = new FakeCollisionSource(
+ 0x02000001u,
+ setup: null,
+ status: PreparedAssetReadStatus.Missing);
+
+ Assert.Equal(
+ RuntimeSetPositionMoverPreparationStatus.RetrySetupUnavailable,
+ lifetime.Physics.SetPosition.TryPrepareAndSubmitAuthoredPlacement(
+ record,
+ token,
+ RuntimeSetPositionOperationKind.RemoteAuthoritative,
+ PhysicsSetPositionFlags.Placement
+ | PhysicsSetPositionFlags.Slide,
+ source,
+ gameTime: 10d,
+ out RuntimeSetPositionOutcome outcome));
+
+ Assert.Equal(default, outcome);
+ Assert.Equal(1, source.ReadCount);
+ // Never manufactured a fallback while the read is in flight - the
+ // token can still be prepared once the asset lands.
+ Assert.True(lifetime.Physics.SetPosition.IsPlacementCurrent(token));
+ Assert.False(lifetime.Physics.SetPosition.TryGetPreparedMoverSphereCount(
+ record,
+ out _));
+ }
+
+ // ---------------------------------------------------------------
+ // C0-1: executor completion on the SAME ordered placement stream
+ // ---------------------------------------------------------------
+
+ [Fact]
+ public void PublishExecutorCompletion_PublishesAcknowledgeOnlyReceiptAndConverges()
+ {
+ PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
+ using var lifetime = new RuntimeEntityObjectLifetime(engine);
+ RuntimeEntityRecord record = CreateRecord(lifetime, 0x70001201u, 1);
+ PhysicsBody body = AttachBody(lifetime, record, SourceCell);
+ var observer = new PlacementObserver();
+ using IDisposable subscription =
+ lifetime.Events.SubscribePlacement(observer);
+
+ RuntimePlacementProjectionToken token =
+ lifetime.Physics.SetPosition.PublishExecutorCompletion(record);
+
+ Assert.True(token.IsValid);
+ Assert.Equal(record.Key, token.Entity);
+ RuntimePlacementProjectionSnapshot published =
+ Assert.Single(observer.Deltas).Placement;
+ Assert.Equal(RuntimePlacementProjectionKind.ExecutorCompleted,
+ published.Kind);
+ Assert.Equal(token, published.Token);
+ Assert.Equal(body.Position, published.WorldPosition);
+ Assert.Equal(1, lifetime.Physics.SetPosition.PendingProjectionCount);
+
+ // Acknowledge-only, exactly like Discard - no operation to resume.
+ Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(token));
+ Assert.Equal(0, lifetime.Physics.SetPosition.PendingProjectionCount);
+ // A stale re-acknowledge of the already-consumed receipt fails.
+ Assert.False(lifetime.Physics.SetPosition.AcknowledgeProjection(token));
+ }
+
+ [Fact]
+ public void PublishExecutorCompletion_RespectsExactHeadOrderingAcrossEntities()
+ {
+ PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
+ using var lifetime = new RuntimeEntityObjectLifetime(engine);
+ RuntimeEntityRecord first = CreateRecord(lifetime, 0x70001202u, 1);
+ AttachBody(lifetime, first, SourceCell);
+ RuntimeEntityRecord second = CreateRecord(lifetime, 0x70001203u, 1);
+ AttachBody(lifetime, second, SourceCell);
+
+ RuntimePlacementProjectionToken firstToken =
+ lifetime.Physics.SetPosition.PublishExecutorCompletion(first);
+ RuntimePlacementProjectionToken secondToken =
+ lifetime.Physics.SetPosition.PublishExecutorCompletion(second);
+
+ Assert.Equal(2, lifetime.Physics.SetPosition.PendingProjectionCount);
+ // The exact head (first) must acknowledge before the second, matching
+ // the ordered-stream contract every other Kind already honors.
+ Assert.False(lifetime.Physics.SetPosition.AcknowledgeProjection(
+ secondToken));
+ Assert.Equal(2, lifetime.Physics.SetPosition.PendingProjectionCount);
+ Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
+ firstToken));
+ Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
+ secondToken));
+ Assert.Equal(0, lifetime.Physics.SetPosition.PendingProjectionCount);
+ }
+
+ // ---------------------------------------------------------------
+ // C0-4: TryCommitParent/CommitWithdrawal cancellation asymmetries
+ // (the ordinary Physics.SetPosition.Forget half, isolated from any
+ // initial-create residence)
+ // ---------------------------------------------------------------
+
+ [Fact]
+ public void TryCommitParent_CancelsASeparateActiveOrdinaryPendingPlacement()
+ {
+ PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
+ using var lifetime = new RuntimeEntityObjectLifetime(engine);
+ const uint guid = 0x70001301u;
+ RuntimeEntityRecord record = CreateRecord(lifetime, guid, 1);
+ AttachBody(lifetime, record, SourceCell);
+ RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply(
+ record,
+ record.PositionAuthorityVersion,
+ Command(Request(SourceCell, new Vector3(12f, 18f, 7f))));
+ Assert.Equal(
+ RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
+ outcome.Status);
+ Assert.Equal(1, lifetime.Physics.SetPosition.PendingProjectionCount);
+
+ var discards = new List();
+ using IDisposable subscription = lifetime.Events.SubscribePlacement(
+ new PlacementObserver(delta =>
+ {
+ if (delta.Placement.Kind is RuntimePlacementProjectionKind.Discard)
+ discards.Add(delta.Placement);
+ }));
+
+ var relation = new ParentAttachmentRelation(
+ ParentGuid: 0x70001400u,
+ ChildGuid: guid,
+ ParentLocation: 1u,
+ PlacementId: 1u,
+ ParentInstanceSequence: 1,
+ ChildPositionSequence: 1);
+ Assert.True(lifetime.TryCommitParent(relation, null, out _));
+
+ Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership()
+ .ActiveOperationCount);
+ // The old Place receipt is REPLACED by a Discard at the same
+ // sequence, not removed outright - it still awaits host
+ // acknowledgement, exactly like every other cancelled-in-flight
+ // receipt.
+ Assert.Equal(1, lifetime.Physics.SetPosition.PendingProjectionCount);
+ RuntimePlacementProjectionSnapshot discard = Assert.Single(discards);
+ Assert.Equal(record.Key, discard.Token.Entity);
+ Assert.Equal(outcome.Projection.Sequence, discard.Token.Sequence);
+ Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
+ discard.Token));
+ Assert.Equal(0, lifetime.Physics.SetPosition.PendingProjectionCount);
+ }
+
+ [Fact]
+ public void CommitWithdrawal_CancelsAnActiveOrdinaryPendingPlacementSymmetricallyWithPickup()
+ {
+ PhysicsEngine engine = FlatEngine(SourceLandblock, 0f);
+ using var lifetime = new RuntimeEntityObjectLifetime(engine);
+ const uint guid = 0x70001302u;
+ RuntimeEntityRecord record = CreateRecord(lifetime, guid, 1);
+ AttachBody(lifetime, record, SourceCell);
+ RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition.Apply(
+ record,
+ record.PositionAuthorityVersion,
+ Command(Request(SourceCell, new Vector3(12f, 18f, 7f))));
+ Assert.Equal(
+ RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
+ outcome.Status);
+ Assert.Equal(1, lifetime.Physics.SetPosition.PendingProjectionCount);
+
+ var discards = new List();
+ using IDisposable subscription = lifetime.Events.SubscribePlacement(
+ new PlacementObserver(delta =>
+ {
+ if (delta.Placement.Kind is RuntimePlacementProjectionKind.Discard)
+ discards.Add(delta.Placement);
+ }));
+
+ Assert.True(lifetime.CommitWithdrawal(record));
+
+ Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership()
+ .ActiveOperationCount);
+ Assert.Equal(1, lifetime.Physics.SetPosition.PendingProjectionCount);
+ RuntimePlacementProjectionSnapshot discard = Assert.Single(discards);
+ Assert.Equal(record.Key, discard.Token.Entity);
+ Assert.Equal(outcome.Projection.Sequence, discard.Token.Sequence);
+ Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
+ discard.Token));
+ Assert.Equal(0, lifetime.Physics.SetPosition.PendingProjectionCount);
+ }
+
private static void VerifyPositionChannelCancellation(
CancellationChannel channel)
{
@@ -2618,6 +2877,73 @@ public sealed class RuntimeSetPositionStateTests
}
}
+ ///
+ /// C0-3 test double: a minimal
+ /// serving exactly one Setup id (matching CanonicalSetupTableId's
+ /// field), so
+ /// can prove ReadSetupCollision -> PrepareMover ->
+ /// SubmitPreparedPlacement actually chains, not merely that each
+ /// step works in isolation (the existing preparer tests' coverage).
+ ///
+ private sealed class FakeCollisionSource(
+ uint expectedSetupTableId,
+ FlatSetupCollision? setup,
+ PreparedAssetReadStatus status = PreparedAssetReadStatus.Loaded)
+ : IPreparedCollisionSource
+ {
+ internal int ReadCount { get; private set; }
+
+ public PreparedAssetPresence ProbeCollision(
+ PakAssetType type, uint sourceFileId) =>
+ PreparedAssetPresence.Available;
+
+ public PreparedCollisionReadResult
+ ReadGfxObjCollision(
+ uint sourceFileId,
+ CancellationToken cancellationToken = default) =>
+ throw new NotSupportedException(
+ "Only ReadSetupCollision is exercised by C0-3.");
+
+ public PreparedCollisionReadResult
+ ReadSetupCollision(
+ uint sourceFileId,
+ CancellationToken cancellationToken = default)
+ {
+ ReadCount++;
+ Assert.Equal(expectedSetupTableId, sourceFileId);
+ return status switch
+ {
+ PreparedAssetReadStatus.Loaded when setup is not null =>
+ PreparedCollisionReadResult.Loaded(
+ setup),
+ PreparedAssetReadStatus.Corrupt =>
+ PreparedCollisionReadResult.Corrupt,
+ _ => PreparedCollisionReadResult.Missing,
+ };
+ }
+
+ public PreparedCollisionReadResult
+ ReadCellStructureCollision(
+ uint sourceFileId,
+ CancellationToken cancellationToken = default) =>
+ throw new NotSupportedException(
+ "Only ReadSetupCollision is exercised by C0-3.");
+
+ public PreparedCollisionReadResult
+ ReadEnvCellTopology(
+ uint sourceFileId,
+ CancellationToken cancellationToken = default) =>
+ throw new NotSupportedException(
+ "Only ReadSetupCollision is exercised by C0-3.");
+
+ public PreparedCollisionSourceStats CollisionStats =>
+ new(ReadCount, ReadCount, ReadCount, 0, 0);
+
+ public void Dispose()
+ {
+ }
+ }
+
private sealed class CollisionReportObserver
: IRuntimeCollisionReportObserver
{