feat(runtime): public initial-Create completion surface for hosts
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 <noreply@anthropic.com>
This commit is contained in:
parent
a32aba35d1
commit
fe02c4f56d
4 changed files with 588 additions and 9 deletions
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -194,6 +194,99 @@ internal readonly record struct RuntimeInitialCreateExecutionReceipt(
|
|||
ImmutableArray<RuntimeInitialCreateExecutedAction> Trace,
|
||||
int ReplayedDeferredChildCount);
|
||||
|
||||
/// <summary>
|
||||
/// C3-1: public projection of <see cref="RuntimeTeleportHookPhase"/>. 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.
|
||||
/// </summary>
|
||||
public enum RuntimeInitialCreateTeleportHookPhase : byte
|
||||
{
|
||||
None,
|
||||
BeforePositionOperation,
|
||||
AfterPositionOperation,
|
||||
AfterEnterWorld,
|
||||
}
|
||||
|
||||
/// <summary>C3-1: public projection of <see cref="RuntimeAuthoritativePositionDisposition"/>.</summary>
|
||||
public enum RuntimeInitialCreatePositionDisposition : byte
|
||||
{
|
||||
RejectedAuthority,
|
||||
RejectedData,
|
||||
AwaitFreshPosition,
|
||||
NoPositionOperation,
|
||||
Interpolate,
|
||||
SetPosition,
|
||||
SetPositionSimple,
|
||||
}
|
||||
|
||||
/// <summary>C3-1: public projection of <see cref="RuntimePositionConstrainPhase"/>.</summary>
|
||||
public enum RuntimeInitialCreatePositionConstrainPhase : byte
|
||||
{
|
||||
None,
|
||||
BeforePositionOperation,
|
||||
AfterPositionOperation,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <c>ConstrainTo</c> 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 <c>UnparentBeforeRouting</c>
|
||||
/// ("unset_parent") and <c>ApplyPlacementFrameBeforeRouting</c>
|
||||
/// ("SetPlacementFrame") facts are deliberately NOT projected here - the
|
||||
/// executor's own merge (<c>ApplyAcceptedPositionSnapshot</c>'s
|
||||
/// <c>clearParent</c>/<c>installPlacementFrame</c> 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
|
||||
/// (<see cref="ConstrainPhase"/>, <see cref="StopInterpolating"/>,
|
||||
/// <see cref="ZeroVelocity"/>, <see cref="PreserveHeading"/>,
|
||||
/// <see cref="SendPositionImmediately"/>, <see cref="HookPhase"/>) are
|
||||
/// exactly the bindings still DEFERRED to the host at presentation time;
|
||||
/// everything already-applied is intentionally excluded.
|
||||
/// </summary>
|
||||
public readonly record struct RuntimeInitialCreatePositionRouteFact(
|
||||
ulong Sequence,
|
||||
RuntimeInitialCreatePositionDisposition Disposition,
|
||||
RuntimeInitialCreateTeleportHookPhase HookPhase,
|
||||
RuntimeInitialCreatePositionConstrainPhase ConstrainPhase,
|
||||
bool StopInterpolating,
|
||||
bool ZeroVelocity,
|
||||
bool PreserveHeading,
|
||||
bool SendPositionImmediately);
|
||||
|
||||
/// <summary>
|
||||
/// C3-1: the public host consumption shape for one executor drain's
|
||||
/// completion, reached via
|
||||
/// <see cref="RuntimePlacementProjectionChannel.TryGetInitialCreateCompletion"/>
|
||||
/// using the correlated <see cref="RuntimePlacementProjectionKind.ExecutorCompleted"/>
|
||||
/// receipt's own Entity/Sequence identity. Built exactly once, at completion
|
||||
/// time, and cached alongside the internal receipt it projects (see
|
||||
/// <see cref="RuntimeInitialCreateContinuationExecutor.ProjectCompletion"/>) -
|
||||
/// a host retrying <c>TryGetInitialCreateCompletion</c> across multiple polls
|
||||
/// never triggers a second allocation.
|
||||
/// Review addendum (2026-08-02): <see cref="PositionRouteFacts"/>'s ARRAY
|
||||
/// ORDER is the authoritative ordering, not <see cref="RuntimeInitialCreatePositionRouteFact.Sequence"/>
|
||||
/// alone - Position facts drained from the SAME same-incarnation envelope
|
||||
/// (<see cref="RuntimeInitialCreateExecutedAction.Stage"/> distinguishes
|
||||
/// them internally, a field this projection does not carry) share one
|
||||
/// continuation <c>Sequence</c>, so two entries can legitimately have equal
|
||||
/// <c>Sequence</c> values. <see cref="RuntimeInitialCreateContinuationExecutor.ProjectCompletion"/>
|
||||
/// 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 <c>Sequence</c> - is the
|
||||
/// only reliable way to recover drain order from this array.
|
||||
/// </summary>
|
||||
public readonly record struct RuntimeInitialCreatePlacementCompletion(
|
||||
RuntimeEntityKey Entity,
|
||||
uint FullCellId,
|
||||
RuntimeInitialCreateTeleportHookPhase TeleportHookPhase,
|
||||
ImmutableArray<RuntimeInitialCreatePositionRouteFact> PositionRouteFacts,
|
||||
int ReplayedDeferredChildCount);
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <see cref="TryGetCompletionReceipt"/> 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
|
||||
/// <see cref="RuntimeInitialCreatePlacementCompletion"/> shape (see
|
||||
/// <see cref="ProjectCompletion"/>) - stored here, not recomputed per
|
||||
/// read, so a host polling <c>TryGetInitialCreateCompletion</c> across
|
||||
/// retries never allocates a second time for the same completion.
|
||||
/// </summary>
|
||||
private readonly Dictionary<RuntimeEntityKey,
|
||||
(ulong Sequence, RuntimeInitialCreateExecutionReceipt Receipt)>
|
||||
(ulong Sequence, RuntimeInitialCreateExecutionReceipt Receipt,
|
||||
RuntimeInitialCreatePlacementCompletion Public)>
|
||||
_completionReceipts = [];
|
||||
private Func<RuntimeGenerationToken>? _generation;
|
||||
private Func<bool>? _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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// C3-1: the public host consumption surface for
|
||||
/// <see cref="TryGetCompletionReceipt"/> - same exact-sequence
|
||||
/// correlation rule, but returns the cached public projection instead of
|
||||
/// the internal receipt/trace. Reached via
|
||||
/// <see cref="RuntimePlacementProjectionChannel.TryGetInitialCreateCompletion"/>.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <c>None</c>. 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
|
||||
/// <see cref="RuntimeInitialCreateContinuationExecutorTests"/>'s
|
||||
/// reflection-based completeness test, which walks every declared
|
||||
/// <see cref="RuntimeTeleportHookPhase"/> value through this exact
|
||||
/// method and fails if a new internal value is ever added without a
|
||||
/// matching arm here.
|
||||
/// </summary>
|
||||
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."),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Review fix (2026-08-02): see <see cref="MapHookPhase"/>'s remarks -
|
||||
/// same explicit-arms-plus-throwing-catch-all discipline.
|
||||
/// </summary>
|
||||
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."),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Review fix (2026-08-02): see <see cref="MapHookPhase"/>'s remarks -
|
||||
/// same explicit-arms-plus-throwing-catch-all discipline.
|
||||
/// </summary>
|
||||
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."),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// C3-1: projects an internal <see cref="RuntimeInitialCreateExecutionReceipt"/>
|
||||
/// to the public <see cref="RuntimeInitialCreatePlacementCompletion"/>
|
||||
/// shape exactly once, at completion time (see the
|
||||
/// <c>_completionReceipts</c> assignment in <see cref="ExecuteCore"/>).
|
||||
/// 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 <see cref="RuntimeInitialCreatePlacementCompletion.PositionRouteFacts"/> -
|
||||
/// widening this to every trace entry would require making the whole
|
||||
/// internal action-kind vocabulary public, which the pinned contract
|
||||
/// explicitly prefers to avoid.
|
||||
/// </summary>
|
||||
private static RuntimeInitialCreatePlacementCompletion ProjectCompletion(
|
||||
in RuntimeInitialCreateExecutionReceipt receipt)
|
||||
{
|
||||
ImmutableArray<RuntimeInitialCreateExecutedAction> trace = receipt.Trace;
|
||||
int positionCount = 0;
|
||||
for (int i = 0; i < trace.Length; i++)
|
||||
{
|
||||
if (trace[i].Kind == RuntimeInitialCreateExecutedActionKind.Position)
|
||||
positionCount++;
|
||||
}
|
||||
|
||||
ImmutableArray<RuntimeInitialCreatePositionRouteFact> positionFacts;
|
||||
if (positionCount == 0)
|
||||
{
|
||||
positionFacts = ImmutableArray<RuntimeInitialCreatePositionRouteFact>.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
var builder = ImmutableArray.CreateBuilder<RuntimeInitialCreatePositionRouteFact>(
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// F2: reaps exactly one completion-receipt correlation entry, bound as
|
||||
/// <see cref="RuntimeSetPositionState.BindExecutorCompletionAcknowledgement"/>'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:
|
||||
|
|
|
|||
|
|
@ -12,16 +12,20 @@ public sealed class RuntimePlacementProjectionChannel
|
|||
{
|
||||
private readonly RuntimeEntityObjectEventStream _events;
|
||||
private readonly RuntimeSetPositionState _setPosition;
|
||||
private readonly RuntimeInitialCreateContinuationExecutor _initialCreateExecution;
|
||||
private Func<RuntimeGenerationToken> _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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -81,6 +85,32 @@ public sealed class RuntimePlacementProjectionChannel
|
|||
|
||||
public int PendingCount => _setPosition.PendingProjectionCount;
|
||||
|
||||
/// <summary>
|
||||
/// C3-1: the public host consumption shape for an initial-Create
|
||||
/// continuation-executor drain's completion. Reached with the exact
|
||||
/// <see cref="RuntimePlacementProjectionToken"/> carried by a
|
||||
/// <see cref="RuntimePlacementProjectionKind.ExecutorCompleted"/> receipt
|
||||
/// observed through <see cref="Subscribe"/> - 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.
|
||||
/// </summary>
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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<RuntimePlacementProjectionSnapshot>();
|
||||
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<RuntimePlacementProjectionSnapshot>();
|
||||
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<RuntimePlacementProjectionSnapshot>();
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Review fix (2026-08-02): the compiler's exhaustiveness net for the
|
||||
/// three enum-mirror switches (<c>MapHookPhase</c>/<c>MapDisposition</c>/
|
||||
/// <c>MapConstrainPhase</c>) 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
|
||||
/// <c>OperationResetAllFieldsToDefaultTouchesEveryDeclaredField</c>'s
|
||||
/// reflection-based completeness guard for
|
||||
/// <see cref="RuntimeSetPositionState"/>'s pooled Operation fields.
|
||||
/// Sabotage-verified during development: temporarily adding an extra
|
||||
/// member to <c>RuntimeTeleportHookPhase</c> (with no matching arm in
|
||||
/// <c>MapHookPhase</c> or the public
|
||||
/// <see cref="RuntimeInitialCreateTeleportHookPhase"/> enum) failed this
|
||||
/// test exactly as predicted - both the arity assertion and the
|
||||
/// unhandled-value invocation threw - before the sabotage was reverted.
|
||||
/// </summary>
|
||||
[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()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue