From fe02c4f56d365affe5daa5e7cbe0a289b02e0b76 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 2 Aug 2026 08:17:12 +0200 Subject: [PATCH] feat(runtime): public initial-Create completion surface for hosts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cutover slice C3-1 (the C3 flip's Runtime prerequisite, landed separately after the flip itself was halted with structural findings — see the plan's C3a/b/c decomposition). Hosts can now read the executor-completion facts they must bind at cutover through one public, generation-gated channel accessor: RuntimePlacementProjectionChannel.TryGetInitialCreateCompletion returns RuntimeInitialCreatePlacementCompletion — the teleport-hook phase, resident cell, replay outcomes, and per-Position route facts (disposition, constrain phase, hook phase, stop-interpolation/zero-velocity/preserve- heading/send-position flags) via public 1:1 mirror enums of the internal classifier vocabulary. The projection is built once at completion, cached in the same reaped entry as the internal receipt (identical acknowledge/ discard/clear lifecycle, ledger-covered), and read allocation-free. Mirror maps enumerate every value explicitly with throwing catch-alls, guarded by a sabotage-verified arity/round-trip reflection test. Doc comments pin the two consumption rules: unparent/placement-frame are already applied to the canonical snapshot (hosts must not re-apply), and array order — not Sequence — is the authoritative Position-fact ordering. Reviewed: architecture PASS + retail-conformance PASS (mirrors verified member-for-member against the retail phase semantics; the route-fact selection confirmed to cover exactly the host-bindable deferrals). Runtime 932/932; complete Release solution 10,727 passed / 4 skips. Co-Authored-By: Claude Fable 5 --- .../Entities/RuntimeEntityObjectLifetime.cs | 9 +- ...untimeInitialCreateContinuationExecutor.cs | 297 +++++++++++++++++- .../RuntimePlacementProjectionChannel.cs | 32 +- ...eInitialCreateContinuationExecutorTests.cs | 259 +++++++++++++++ 4 files changed, 588 insertions(+), 9 deletions(-) diff --git a/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs b/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs index 2571d719..c3a1e293 100644 --- a/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs +++ b/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs @@ -174,7 +174,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable InitialCreateExecution.ForgetCompletionReceipt(key, sequence)); Placements = new RuntimePlacementProjectionChannel( Events, - Physics.SetPosition); + Physics.SetPosition, + InitialCreateExecution); } internal RuntimeEntityObjectLifetime( @@ -226,7 +227,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable InitialCreateExecution.ForgetCompletionReceipt(key, sequence)); Placements = new RuntimePlacementProjectionChannel( Events, - Physics.SetPosition); + Physics.SetPosition, + InitialCreateExecution); } internal RuntimeEntityObjectLifetime( @@ -278,7 +280,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable InitialCreateExecution.ForgetCompletionReceipt(key, sequence)); Placements = new RuntimePlacementProjectionChannel( Events, - Physics.SetPosition); + Physics.SetPosition, + InitialCreateExecution); } public RuntimeEntityDirectory Entities { get; } diff --git a/src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs b/src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs index d545fe78..4cea0c23 100644 --- a/src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs +++ b/src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs @@ -194,6 +194,99 @@ internal readonly record struct RuntimeInitialCreateExecutionReceipt( ImmutableArray Trace, int ReplayedDeferredChildCount); +/// +/// C3-1: public projection of . A +/// separate public enum (rather than widening the internal one's +/// accessibility) keeps the classifier/executor's internal vocabulary free to +/// evolve without becoming a host-facing contract; values map 1:1 today. +/// +public enum RuntimeInitialCreateTeleportHookPhase : byte +{ + None, + BeforePositionOperation, + AfterPositionOperation, + AfterEnterWorld, +} + +/// C3-1: public projection of . +public enum RuntimeInitialCreatePositionDisposition : byte +{ + RejectedAuthority, + RejectedData, + AwaitFreshPosition, + NoPositionOperation, + Interpolate, + SetPosition, + SetPositionSimple, +} + +/// C3-1: public projection of . +public enum RuntimeInitialCreatePositionConstrainPhase : byte +{ + None, + BeforePositionOperation, + AfterPositionOperation, +} + +/// +/// C3-1: one Position continuation's route facts from the executor's trace, +/// projected to a public shape so a host can bind constrain/interpolation +/// presentation (retail's ConstrainTo placement, stop-interpolate, +/// zero-velocity, preserve-heading, send-position-immediately) without +/// reaching into internal Runtime route-classifier types. +/// Review addendum (2026-08-02): the route's UnparentBeforeRouting +/// ("unset_parent") and ApplyPlacementFrameBeforeRouting +/// ("SetPlacementFrame") facts are deliberately NOT projected here - the +/// executor's own merge (ApplyAcceptedPositionSnapshot's +/// clearParent/installPlacementFrame parameters) already +/// applies both directly to the canonical snapshot, synchronously, before +/// the trace entry carrying this fact is even built. A host reading this +/// record must NOT re-apply either one - the facts this type DOES carry +/// (, , +/// , , +/// , ) are +/// exactly the bindings still DEFERRED to the host at presentation time; +/// everything already-applied is intentionally excluded. +/// +public readonly record struct RuntimeInitialCreatePositionRouteFact( + ulong Sequence, + RuntimeInitialCreatePositionDisposition Disposition, + RuntimeInitialCreateTeleportHookPhase HookPhase, + RuntimeInitialCreatePositionConstrainPhase ConstrainPhase, + bool StopInterpolating, + bool ZeroVelocity, + bool PreserveHeading, + bool SendPositionImmediately); + +/// +/// C3-1: the public host consumption shape for one executor drain's +/// completion, reached via +/// +/// using the correlated +/// receipt's own Entity/Sequence identity. Built exactly once, at completion +/// time, and cached alongside the internal receipt it projects (see +/// ) - +/// a host retrying TryGetInitialCreateCompletion across multiple polls +/// never triggers a second allocation. +/// Review addendum (2026-08-02): 's ARRAY +/// ORDER is the authoritative ordering, not +/// alone - Position facts drained from the SAME same-incarnation envelope +/// ( distinguishes +/// them internally, a field this projection does not carry) share one +/// continuation Sequence, so two entries can legitimately have equal +/// Sequence values. +/// walks the executor's trace strictly in construction order (itself the +/// exact FIFO drain order) and appends without reordering or deduplicating, +/// so array index - never a sort or group-by on Sequence - is the +/// only reliable way to recover drain order from this array. +/// +public readonly record struct RuntimeInitialCreatePlacementCompletion( + RuntimeEntityKey Entity, + uint FullCellId, + RuntimeInitialCreateTeleportHookPhase TeleportHookPhase, + ImmutableArray PositionRouteFacts, + int ReplayedDeferredChildCount); + /// /// Applies one entity's completed initial-Create residence: adopts the /// initial placement exactly once, emits the AfterEnterWorld teleport-hook @@ -285,10 +378,16 @@ internal sealed class RuntimeInitialCreateContinuationExecutor /// 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. + /// superseded completion under a reused key. C3-1: the tuple's third + /// slot is the SAME receipt already projected once to the public + /// shape (see + /// ) - stored here, not recomputed per + /// read, so a host polling TryGetInitialCreateCompletion across + /// retries never allocates a second time for the same completion. /// private readonly Dictionary + (ulong Sequence, RuntimeInitialCreateExecutionReceipt Receipt, + RuntimeInitialCreatePlacementCompletion Public)> _completionReceipts = []; private Func? _generation; private Func? _usePositionFromServer; @@ -438,7 +537,8 @@ internal sealed class RuntimeInitialCreateContinuationExecutor { if (_completionReceipts.TryGetValue( token.Entity, - out (ulong Sequence, RuntimeInitialCreateExecutionReceipt Receipt) entry) + out (ulong Sequence, RuntimeInitialCreateExecutionReceipt Receipt, + RuntimeInitialCreatePlacementCompletion Public) entry) && entry.Sequence == token.Sequence) { receipt = entry.Receipt; @@ -448,6 +548,185 @@ internal sealed class RuntimeInitialCreateContinuationExecutor return false; } + /// + /// C3-1: the public host consumption surface for + /// - same exact-sequence + /// correlation rule, but returns the cached public projection instead of + /// the internal receipt/trace. Reached via + /// . + /// + internal bool TryGetCompletion( + in RuntimePlacementProjectionToken token, + out RuntimeInitialCreatePlacementCompletion completion) + { + if (_completionReceipts.TryGetValue( + token.Entity, + out (ulong Sequence, RuntimeInitialCreateExecutionReceipt Receipt, + RuntimeInitialCreatePlacementCompletion Public) entry) + && entry.Sequence == token.Sequence) + { + completion = entry.Public; + return true; + } + completion = default; + return false; + } + + /// + /// Review fix (2026-08-02, architecture pass): every arm is now listed + /// explicitly and the catch-all throws instead of silently folding an + /// unmapped future internal value into None. A value this method + /// cannot map must never reach a host disguised as "nothing to bind" - + /// that would silently drop presentation behavior (e.g. a real + /// teleport-hook phase host code never runs). See + /// 's + /// reflection-based completeness test, which walks every declared + /// value through this exact + /// method and fails if a new internal value is ever added without a + /// matching arm here. + /// + private static RuntimeInitialCreateTeleportHookPhase MapHookPhase( + RuntimeTeleportHookPhase phase) => phase switch + { + RuntimeTeleportHookPhase.None => + RuntimeInitialCreateTeleportHookPhase.None, + RuntimeTeleportHookPhase.BeforePositionOperation => + RuntimeInitialCreateTeleportHookPhase.BeforePositionOperation, + RuntimeTeleportHookPhase.AfterPositionOperation => + RuntimeInitialCreateTeleportHookPhase.AfterPositionOperation, + RuntimeTeleportHookPhase.AfterEnterWorld => + RuntimeInitialCreateTeleportHookPhase.AfterEnterWorld, + _ => throw new ArgumentOutOfRangeException( + nameof(phase), + phase, + $"Unmapped {nameof(RuntimeTeleportHookPhase)} value - add an explicit arm to {nameof(MapHookPhase)} and to the public {nameof(RuntimeInitialCreateTeleportHookPhase)} projection."), + }; + + /// + /// Review fix (2026-08-02): see 's remarks - + /// same explicit-arms-plus-throwing-catch-all discipline. + /// + private static RuntimeInitialCreatePositionDisposition MapDisposition( + RuntimeAuthoritativePositionDisposition disposition) => disposition switch + { + RuntimeAuthoritativePositionDisposition.RejectedAuthority => + RuntimeInitialCreatePositionDisposition.RejectedAuthority, + RuntimeAuthoritativePositionDisposition.RejectedData => + RuntimeInitialCreatePositionDisposition.RejectedData, + RuntimeAuthoritativePositionDisposition.AwaitFreshPosition => + RuntimeInitialCreatePositionDisposition.AwaitFreshPosition, + RuntimeAuthoritativePositionDisposition.NoPositionOperation => + RuntimeInitialCreatePositionDisposition.NoPositionOperation, + RuntimeAuthoritativePositionDisposition.Interpolate => + RuntimeInitialCreatePositionDisposition.Interpolate, + RuntimeAuthoritativePositionDisposition.SetPosition => + RuntimeInitialCreatePositionDisposition.SetPosition, + RuntimeAuthoritativePositionDisposition.SetPositionSimple => + RuntimeInitialCreatePositionDisposition.SetPositionSimple, + _ => throw new ArgumentOutOfRangeException( + nameof(disposition), + disposition, + $"Unmapped {nameof(RuntimeAuthoritativePositionDisposition)} value - add an explicit arm to {nameof(MapDisposition)} and to the public {nameof(RuntimeInitialCreatePositionDisposition)} projection."), + }; + + /// + /// Review fix (2026-08-02): see 's remarks - + /// same explicit-arms-plus-throwing-catch-all discipline. + /// + private static RuntimeInitialCreatePositionConstrainPhase MapConstrainPhase( + RuntimePositionConstrainPhase phase) => phase switch + { + RuntimePositionConstrainPhase.None => + RuntimeInitialCreatePositionConstrainPhase.None, + RuntimePositionConstrainPhase.BeforePositionOperation => + RuntimeInitialCreatePositionConstrainPhase.BeforePositionOperation, + RuntimePositionConstrainPhase.AfterPositionOperation => + RuntimeInitialCreatePositionConstrainPhase.AfterPositionOperation, + _ => throw new ArgumentOutOfRangeException( + nameof(phase), + phase, + $"Unmapped {nameof(RuntimePositionConstrainPhase)} value - add an explicit arm to {nameof(MapConstrainPhase)} and to the public {nameof(RuntimeInitialCreatePositionConstrainPhase)} projection."), + }; + + /// + /// C3-1: projects an internal + /// to the public + /// shape exactly once, at completion time (see the + /// _completionReceipts assignment in ). + /// Only Position-kind trace entries carry route facts meaningful for + /// constrain/interpolation binding; every other action kind + /// (InitialAdoption, TeleportHookRequest, replay, envelope stages, ...) + /// is intentionally excluded from - + /// widening this to every trace entry would require making the whole + /// internal action-kind vocabulary public, which the pinned contract + /// explicitly prefers to avoid. + /// + private static RuntimeInitialCreatePlacementCompletion ProjectCompletion( + in RuntimeInitialCreateExecutionReceipt receipt) + { + ImmutableArray trace = receipt.Trace; + int positionCount = 0; + for (int i = 0; i < trace.Length; i++) + { + if (trace[i].Kind == RuntimeInitialCreateExecutedActionKind.Position) + positionCount++; + } + + ImmutableArray positionFacts; + if (positionCount == 0) + { + positionFacts = ImmutableArray.Empty; + } + else + { + var builder = ImmutableArray.CreateBuilder( + positionCount); + for (int i = 0; i < trace.Length; i++) + { + RuntimeInitialCreateExecutedAction action = trace[i]; + if (action.Kind != RuntimeInitialCreateExecutedActionKind.Position) + continue; + // Review fix (2026-08-02): PositionDisposition is nullable + // on RuntimeInitialCreateExecutedAction because it is only + // meaningful for Position/hook-request entries in general - + // but BuildPositionTrace is the SOLE constructor of + // Kind.Position entries (grep-confirmed, 5 call sites, all + // through BuildPositionTrace) and it always passes + // route.Disposition, a non-nullable enum, into this slot. + // Null is therefore NOT a legitimate state for a Position- + // kind entry specifically - a silent `?? NoPositionOperation` + // fallback here would have hidden a real bug (a future + // Position-trace producer that forgot to set it) behind a + // plausible-looking default. Fail loudly instead. + if (action.PositionDisposition is not { } disposition) + { + throw new InvalidOperationException( + "A Position-kind executor trace entry must always " + + "carry a non-null PositionDisposition - " + + "BuildPositionTrace (the sole producer of Kind.Position " + + "entries) always supplies route.Disposition."); + } + builder.Add(new RuntimeInitialCreatePositionRouteFact( + action.Sequence, + MapDisposition(disposition), + MapHookPhase(action.HookPhase), + MapConstrainPhase(action.ConstrainPhase), + action.StopInterpolating, + action.ZeroVelocity, + action.PreserveHeading, + action.SendPositionImmediately)); + } + positionFacts = builder.MoveToImmutable(); + } + + return new RuntimeInitialCreatePlacementCompletion( + receipt.Entity, + receipt.FullCellId, + MapHookPhase(receipt.TeleportHookPhase), + positionFacts, + receipt.ReplayedDeferredChildCount); + } + /// /// F2: reaps exactly one completion-receipt correlation entry, bound as /// 's @@ -461,7 +740,8 @@ internal sealed class RuntimeInitialCreateContinuationExecutor { if (_completionReceipts.TryGetValue( key, - out (ulong Sequence, RuntimeInitialCreateExecutionReceipt Receipt) entry) + out (ulong Sequence, RuntimeInitialCreateExecutionReceipt Receipt, + RuntimeInitialCreatePlacementCompletion Public) entry) && entry.Sequence == sequence) { _completionReceipts.Remove(key); @@ -787,6 +1067,13 @@ internal sealed class RuntimeInitialCreateContinuationExecutor progress.Trace.ToImmutable(), progress.ReplayedDeferredChildCount); receipt = completedReceipt; + // C3-1: project to the public host-consumption shape + // exactly once here, alongside the internal receipt - + // never recomputed per host read/retry (see + // ProjectCompletion's and _completionReceipts's own doc + // comments). + RuntimeInitialCreatePlacementCompletion publicCompletion = + ProjectCompletion(completedReceipt); _progress.Remove(key); // C0-1: bridge the executor's own completion onto the // SAME ordered placement receipt stream every @@ -806,7 +1093,7 @@ internal sealed class RuntimeInitialCreateContinuationExecutor canonical, beforePublish: token => _completionReceipts[key] = - (token.Sequence, completedReceipt)); + (token.Sequence, completedReceipt, publicCompletion)); return RuntimeInitialCreateExecutionStatus.Completed; } case RuntimeInitialCreateResidenceExecutorReleaseStatus.Revised: diff --git a/src/AcDream.Runtime/Physics/RuntimePlacementProjectionChannel.cs b/src/AcDream.Runtime/Physics/RuntimePlacementProjectionChannel.cs index 807c178d..d1dc3d79 100644 --- a/src/AcDream.Runtime/Physics/RuntimePlacementProjectionChannel.cs +++ b/src/AcDream.Runtime/Physics/RuntimePlacementProjectionChannel.cs @@ -12,16 +12,20 @@ public sealed class RuntimePlacementProjectionChannel { private readonly RuntimeEntityObjectEventStream _events; private readonly RuntimeSetPositionState _setPosition; + private readonly RuntimeInitialCreateContinuationExecutor _initialCreateExecution; private Func _generation = static () => default; private bool _generationBound; internal RuntimePlacementProjectionChannel( RuntimeEntityObjectEventStream events, - RuntimeSetPositionState setPosition) + RuntimeSetPositionState setPosition, + RuntimeInitialCreateContinuationExecutor initialCreateExecution) { _events = events ?? throw new ArgumentNullException(nameof(events)); _setPosition = setPosition ?? throw new ArgumentNullException(nameof(setPosition)); + _initialCreateExecution = initialCreateExecution + ?? throw new ArgumentNullException(nameof(initialCreateExecution)); } /// @@ -81,6 +85,32 @@ public sealed class RuntimePlacementProjectionChannel public int PendingCount => _setPosition.PendingProjectionCount; + /// + /// C3-1: the public host consumption shape for an initial-Create + /// continuation-executor drain's completion. Reached with the exact + /// carried by a + /// receipt + /// observed through - the same correlation + /// identity (Entity/Sequence) every other placement Kind uses. Exposes + /// exactly the facts a cutover host needs to bind presentation off an + /// initial placement (the teleport-hook phase, the drained Position + /// continuations' route facts for constrain/interpolation binding, and + /// the replayed-deferred-child count) without widening any internal + /// Runtime type's accessibility. Returns false for a generation + /// mismatch or a stale/superseded/unknown token, mirroring every other + /// generation-gated method on this channel. + /// + public bool TryGetInitialCreateCompletion( + RuntimeGenerationToken expectedGeneration, + in RuntimePlacementProjectionToken token, + out RuntimeInitialCreatePlacementCompletion completion) + { + if (IsCurrent(expectedGeneration)) + return _initialCreateExecution.TryGetCompletion(token, out completion); + completion = default; + return false; + } + private bool IsCurrent(RuntimeGenerationToken expectedGeneration) => _generationBound && expectedGeneration.Value != 0UL diff --git a/tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateContinuationExecutorTests.cs b/tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateContinuationExecutorTests.cs index fb81a88c..7fcd8795 100644 --- a/tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateContinuationExecutorTests.cs +++ b/tests/AcDream.Runtime.Tests/Entities/RuntimeInitialCreateContinuationExecutorTests.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using System.Numerics; +using System.Reflection; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; @@ -4319,6 +4320,264 @@ public sealed class RuntimeInitialCreateContinuationExecutorTests observed[1].Token)); } + // --------------------------------------------------------------- + // C3-1: the public RuntimePlacementProjectionChannel host consumption + // surface for an executor completion (TryGetInitialCreateCompletion). + // --------------------------------------------------------------- + + [Fact] + public void PlacementChannel_TryGetInitialCreateCompletion_ProjectsHookPhaseCellAndReplayCount() + { + using RuntimeEntityObjectLifetime lifetime = EngineLifetime(); + Bind(lifetime, 420UL); + const uint guid = 0x70024020u; + 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); + var generation = new RuntimeGenerationToken(420UL); + Assert.True(lifetime.Placements.TryGetInitialCreateCompletion( + generation, + completion.Token, + out RuntimeInitialCreatePlacementCompletion publicCompletion)); + Assert.Equal(canonical.Key, publicCompletion.Entity); + Assert.Equal(receipt.Entity, publicCompletion.Entity); + Assert.Equal(receipt.FullCellId, publicCompletion.FullCellId); + Assert.Equal(receipt.ReplayedDeferredChildCount, publicCompletion.ReplayedDeferredChildCount); + // This is a local-player Create (login): retail's init_player path + // requests the AfterEnterWorld teleport hook (see RunInitialTail) - + // the exact fact route-1/8's cutover caller needs to know whether to + // run the after-enter teleport suffix. No Position continuation ran + // in this scenario, so the route-fact array projects empty. + Assert.Equal( + RuntimeInitialCreateTeleportHookPhase.AfterEnterWorld, + publicCompletion.TeleportHookPhase); + Assert.Equal(RuntimeTeleportHookPhase.AfterEnterWorld, receipt.TeleportHookPhase); + Assert.Empty(publicCompletion.PositionRouteFacts); + } + + [Fact] + public void PlacementChannel_TryGetInitialCreateCompletion_ProjectsPositionRouteFactsForConstrainInterpolationBinding() + { + using RuntimeEntityObjectLifetime lifetime = EngineLifetime(); + Bind(lifetime, 421UL); + const uint guid = 0x70024021u; + 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 and lands a Position trace entry with real + // route facts - the same scenario as + // ExecutorCompletion_ObservedOnlyAfterAnyContinuationPlacementInFifoOrder. + 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); + + RuntimePlacementProjectionSnapshot executorCompletion = observed[^1]; + Assert.Equal( + RuntimePlacementProjectionKind.ExecutorCompleted, + executorCompletion.Kind); + + RuntimeInitialCreateExecutedAction internalPositionTrace = Assert.Single( + receipt.Trace.Where( + static a => a.Kind == RuntimeInitialCreateExecutedActionKind.Position)); + + var generation = new RuntimeGenerationToken(421UL); + Assert.True(lifetime.Placements.TryGetInitialCreateCompletion( + generation, + executorCompletion.Token, + out RuntimeInitialCreatePlacementCompletion publicCompletion)); + RuntimeInitialCreatePositionRouteFact fact = Assert.Single( + publicCompletion.PositionRouteFacts); + Assert.Equal(internalPositionTrace.Sequence, fact.Sequence); + Assert.Equal(internalPositionTrace.StopInterpolating, fact.StopInterpolating); + Assert.Equal(internalPositionTrace.ZeroVelocity, fact.ZeroVelocity); + Assert.Equal(internalPositionTrace.PreserveHeading, fact.PreserveHeading); + Assert.Equal( + internalPositionTrace.SendPositionImmediately, + fact.SendPositionImmediately); + Assert.Equal( + internalPositionTrace.PositionDisposition!.Value.ToString(), + fact.Disposition.ToString()); + Assert.Equal( + internalPositionTrace.ConstrainPhase.ToString(), + fact.ConstrainPhase.ToString()); + Assert.Equal( + internalPositionTrace.HookPhase.ToString(), + fact.HookPhase.ToString()); + } + + [Fact] + public void PlacementChannel_TryGetInitialCreateCompletion_RejectsWrongGeneration() + { + using RuntimeEntityObjectLifetime lifetime = EngineLifetime(); + Bind(lifetime, 422UL); + const uint guid = 0x70024022u; + 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))); + RunToCompletion(lifetime, canonical, lease.Token, NoContact); + RuntimePlacementProjectionSnapshot completion = Assert.Single(observed); + + Assert.False(lifetime.Placements.TryGetInitialCreateCompletion( + new RuntimeGenerationToken(999UL), + completion.Token, + out RuntimeInitialCreatePlacementCompletion stale)); + Assert.Equal(default, stale); + } + + [Fact] + public void PlacementChannel_TryGetInitialCreateCompletion_ReturnsFalseAfterAcknowledgeReapsTheCorrelationEntry() + { + using RuntimeEntityObjectLifetime lifetime = EngineLifetime(); + Bind(lifetime, 423UL); + const uint guid = 0x70024023u; + 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.True(lifetime.Physics.SetPosition.TryPeekProjection( + out RuntimePlacementProjectionSnapshot completion)); + var generation = new RuntimeGenerationToken(423UL); + Assert.True(lifetime.Placements.TryGetInitialCreateCompletion( + generation, + completion.Token, + out _)); + + Assert.True(lifetime.Placements.Acknowledge(generation, completion.Token)); + + Assert.False(lifetime.Placements.TryGetInitialCreateCompletion( + generation, + completion.Token, + out RuntimeInitialCreatePlacementCompletion afterAck)); + Assert.Equal(default, afterAck); + } + + /// + /// Review fix (2026-08-02): the compiler's exhaustiveness net for the + /// three enum-mirror switches (MapHookPhase/MapDisposition/ + /// MapConstrainPhase) is gone the moment a catch-all arm exists - + /// that is exactly why those catch-alls now throw instead of silently + /// defaulting. This reflection-based test is the runtime replacement: + /// for each (internal enum, public projection enum, private mapper + /// method) triple it asserts equal arity AND drives every declared + /// internal value through the mapper via reflection (the methods are + /// `private static`), asserting the mapped public value's NAME equals + /// the internal value's name (every mapper is a literal 1:1 name + /// mirror by design - see each public enum's own doc comment). Adding a + /// new member to either enum without updating the other and the mapper + /// fails this test immediately, mirroring + /// OperationResetAllFieldsToDefaultTouchesEveryDeclaredField's + /// reflection-based completeness guard for + /// 's pooled Operation fields. + /// Sabotage-verified during development: temporarily adding an extra + /// member to RuntimeTeleportHookPhase (with no matching arm in + /// MapHookPhase or the public + /// enum) failed this + /// test exactly as predicted - both the arity assertion and the + /// unhandled-value invocation threw - before the sabotage was reverted. + /// + [Fact] + public void EnumProjectionMapsHaveEqualArityAndEveryInternalValueRoundTripsByName() + { + AssertMapIsCompleteAndNamePreserving( + typeof(RuntimeTeleportHookPhase), + typeof(RuntimeInitialCreateTeleportHookPhase), + "MapHookPhase"); + AssertMapIsCompleteAndNamePreserving( + typeof(RuntimeAuthoritativePositionDisposition), + typeof(RuntimeInitialCreatePositionDisposition), + "MapDisposition"); + AssertMapIsCompleteAndNamePreserving( + typeof(RuntimePositionConstrainPhase), + typeof(RuntimeInitialCreatePositionConstrainPhase), + "MapConstrainPhase"); + } + + private static void AssertMapIsCompleteAndNamePreserving( + Type internalEnumType, + Type publicEnumType, + string mapMethodName) + { + MethodInfo? method = typeof(RuntimeInitialCreateContinuationExecutor) + .GetMethod( + mapMethodName, + BindingFlags.NonPublic | BindingFlags.Static); + Assert.True( + method is not null, + $"{nameof(RuntimeInitialCreateContinuationExecutor)} no longer " + + $"declares a private static method named {mapMethodName} - " + + "update this test's reflection lookup to match."); + + Array internalValues = Enum.GetValues(internalEnumType); + Array publicValues = Enum.GetValues(publicEnumType); + // Equal arity: every internal value must have exactly one public + // counterpart and vice versa. A mismatch here is the first sign + // either enum grew without the other (or the mapper) being updated + // to match. + Assert.True( + internalValues.Length == publicValues.Length, + $"{internalEnumType.Name} has {internalValues.Length} values " + + $"but {publicEnumType.Name} has {publicValues.Length} - keep " + + "the internal/public enum pair in lockstep."); + + foreach (object? internalValue in internalValues) + { + // Invoked via reflection deliberately - a value this mapper + // cannot handle now throws ArgumentOutOfRangeException (see + // the mapper's own doc comment), which TargetInvocationException + // propagates through Invoke and fails this test with a clear + // message identifying exactly which enum member is unmapped. + object? mapped = method!.Invoke(null, [internalValue]); + Assert.Equal(internalValue!.ToString(), mapped!.ToString()); + } + } + [Fact] public void ExecutorCompletion_ReceiptIsReadableFromWithinTheSameSynchronousOnPlacementDispatch() {