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