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

@ -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