feat(runtime): C3c - production placement cutover: both hosts on the residence conductors (routes 1+8)

Campaign P remaining-physics-divergence, placement cutover slice C3c
(docs/plans/2026-08-02-placement-cutover.md). Both production hosts now
register every initial Create through the residence + continuation-
executor + first-entry-conductor machinery (C0-C3b):

- Graphical (route 1): RegisterEntityWithInitialResidence at Create; the
  shared RuntimeFirstEntryDriveController pumps both conductors from the
  placement-receipt flow; MaterializeProjection and RebucketLiveEntity
  are presentation-only while a residence is ACTIVE (ExecutorCompleted is
  the presentation-binding receipt); post-residence entities take the
  full legacy path including the prepare_to_enter_world clock edges.
  PlayerModeController attaches presentation to the Runtime-published
  controller; its legacy resolve/step-heights/host-construction path is
  deleted; presentation-only rollback (retail has no entry-flow rollback).
- Headless (route 8): OnSpawned registers with residence when a drive
  exists; content-less sessions keep the pre-flip direct registration;
  SynchronizeLocalPlayer/CreateController/ApplySetupStepHeights deleted;
  prepared-collision read failure is a typed AwaitingCollisionSource
  retry; far remotes outside the service window complete celless.
- RuntimeLocalPlayerMovementState.Controller setter sealed internal; all
  controller mutation flows through the publication lifecycle.

Fix slices landed within this cutover, each dual-gated:
- F1: live movement-stat/server-physics application routed through the
  Runtime ownership seam (post-logout ingest crash on the retired
  controller eliminated; RuntimeMovementSkillProjection deleted).
- F2: login activation wedge - collision-admission prefix gate factored
  out of the seal (reentrant-commit RejectedAuthority), rearm generation
  identity corrected, PlayerModeAutoEntry requires the Runtime-published
  controller (world reveal can no longer seal unmaterialized).
- F3: landblock-prefix 0-sentinel replaced by explicit absent-id guards;
  map-corner landblocks (grid row/col 0) fully legal through admission,
  park/rearm/retire, quiescence, and outdoor shadow seeds.
- F5: local-player first-entry ground contact seeded by the shared
  SpawnPlacementSettler (moved App->Core) at FinalizeActivation - the
  retail first-gravity-frame touch (enter_world 0x00516170 carries no
  seed); the legacy unconditional force-seed is overwritten by a real
  floor-found contact; airborne spawns stay airborne; outbound contact
  bit verified end-to-end. Fixes the standing-cast 'You can't do that
  while in the air!' rejections.
- R1 (dual-review round): login constraint leash armed at the committed
  placement (HandleReceivedPosition 0x00453FD0 analog); register rows
  AD-61 (settle-timing compression now covering the local player) and
  AD-42 (repointed off the deleted resolve split) in this commit;
  residence-conversion owner API; wire-landblock guards; drive-pending
  ledger in IsConverged; route attach/detach latch; executor-drain drift
  model documented + source-pinned.

Gates: Runtime 1,003, App 4,039/3 skips, Headless 79, complete solution
10,816/0 failed/4 skips (Release, -m:1); connected lifecycle/reconnect
gate PASS (logs/connected-world-gate-20260802-175401; graceful exits,
world-visible, zero airborne rejections). The nine-stop soak remains red
for the pre-existing 6b28ff99 whole-world collision-clone throughput
regression (attributed with evidence; scheduled as its own slice before
C5). Dual Opus reviews (retail-conformance + adversarial): delta PASS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-02 18:10:33 +02:00
parent 78f1eb1896
commit 529e0e9d88
68 changed files with 5977 additions and 831 deletions

View file

@ -0,0 +1,74 @@
using System.Text.RegularExpressions;
namespace AcDream.App.Tests.World;
/// <summary>
/// C3c-R1 F3 (coordinator resolution, 2026-08-02): the create-authority
/// drift probes in the expectation-item 6/8 tests
/// (LiveEntityHydrationControllerTests + LiveEntityCreateSupersessionRecoveryTests)
/// hand-call <c>record.Canonical.AdvanceCreateAuthority()</c> as an HONEST
/// MODEL of the executor drain's advance — the SOLE remaining production
/// site that advances create authority for an existing incarnation. A
/// nested production OnCreate can no longer produce that drift:
/// post-residence ExistingGeneration registration is description-only
/// (RuntimeEntityObjectLifetime gates the advance on
/// <c>!beginInitialResidence</c>) and <c>ConsumeExecuted</c> removes the
/// completed residence entry at Released, closing the FIFO-adoption path
/// (empirically confirmed: the restored nested-OnCreate probe produced no
/// drift and no CreateSupersessionRecovery). This pin flags the model as
/// STALE if the production site ever moves or loses the advance — the
/// item 6/8 probes must be re-derived from wherever it goes.
/// </summary>
public sealed class C3cR1F3DriftModelSourcePinTests
{
[Fact]
public void HandCalledDriftProbe_StillModelsTheExecutorDrainAdvance()
{
string executor = ReadRuntimeSource(
"Entities",
"RuntimeInitialCreateContinuationExecutor.cs");
// Exactly one production advance, and it lives inside the
// WeenieDescription drain stage the probes model.
Assert.Single(
Regex.Matches(executor, @"_entities\.AdvanceCreateAuthority\(")
.Cast<Match>());
Assert.Matches(
new Regex(
@"private bool ApplyWeenieDescriptionAction[\s\S]{0,6000}?"
+ @"_entities\.AdvanceCreateAuthority\(canonical\);"),
executor);
// The registration-time advance stays gated OFF the residence
// route — the reason a nested production OnCreate cannot reach the
// modeled drift.
string lifetime = ReadRuntimeSource(
"Entities",
"RuntimeEntityObjectLifetime.cs");
Assert.Matches(
new Regex(
@"if \(!beginInitialResidence\)\s*"
+ @"Entities\.AdvanceCreateAuthority\(retained\);"),
lifetime);
}
private static string ReadRuntimeSource(params string[] relativePath)
{
DirectoryInfo? directory = new(AppContext.BaseDirectory);
while (directory is not null)
{
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
{
return File.ReadAllText(Path.Combine(
directory.FullName,
"src",
"AcDream.Runtime",
Path.Combine(relativePath)));
}
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
}
}

View file

@ -144,7 +144,7 @@ public sealed class DeferredLiveEntityRuntimeComponentLifecycleTests
}
private static WorldSession.EntitySpawn CreateSpawn(uint guid) =>
new(
new WorldSession.EntitySpawn(
Guid: guid,
Position: null,
SetupTableId: null,
@ -157,5 +157,5 @@ public sealed class DeferredLiveEntityRuntimeComponentLifecycleTests
ItemType: null,
MotionState: null,
MotionTableId: null,
InstanceSequence: 1);
InstanceSequence: 1).WithConsistentPhysics();
}

View file

@ -370,12 +370,14 @@ public sealed class LiveEntityHydrationControllerTests
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
LiveEntityRecord record = fixture.Record;
WorldEntity entity = record.WorldEntity!;
var body = new PhysicsBody();
Assert.Same(
body,
fixture.Runtime.GetOrCreatePhysicsBody(
record.ServerGuid,
_ => body));
// C3c/C3b: the first-entry conductor constructs the canonical
// physics body at Create (retail ACCObjectMaint::CreateObject /
// set_description). Capture that existing body — the identity the
// pickup/re-enter cycle must preserve — instead of seeding one.
PhysicsBody body = fixture.Runtime.GetOrCreatePhysicsBody(
record.ServerGuid,
static _ => throw new InvalidOperationException(
"The conductor-built canonical body should already exist."));
fixture.Relationships.OnUnparentAction = _ =>
fixture.Runtime.WithdrawLiveEntityProjection(record)
? ChildUnparentDisposition.Completed
@ -962,6 +964,14 @@ public sealed class LiveEntityHydrationControllerTests
// Local-player records ordinarily do not rebucket from streaming
// callbacks; an incomplete initial transaction must still recover.
// C3c: the failed Create transaction unwound before OnCreateCore's
// own drive pump ran, leaving the residence pending with FullCellId
// 0 (streaming callbacks key candidates off the committed cell). In
// production the per-frame first-entry pump completes the conductor
// independently of the failed hydration transaction; model that
// pump here, then let the streaming callback recover the partial
// projection exactly as before.
fixture.FirstEntry.DriveAll();
fixture.Controller.OnLandblockLoaded(Cell);
Assert.Same(partial, record.WorldEntity);
@ -1042,7 +1052,13 @@ public sealed class LiveEntityHydrationControllerTests
Assert.Equal("same generation newer", fixture.Objects.Get(Guid)!.Name);
Assert.True(fixture.Record.InitialHydrationCompleted);
Assert.Single(fixture.Materializer.Calls);
Assert.Equal((ushort)2, fixture.Materializer.PositionSequences[0]);
// C3c: the nested fresher same-generation Create is admitted into
// the outer create's ACTIVE residence FIFO (AD-59) and its facts
// commit at the executor drain, in array order. The single
// materialization therefore runs from the admission-frozen seq-1
// create; the seq-2 facts (including the name asserted above) land
// through the drain and bind via the completion receipt.
Assert.Equal((ushort)1, fixture.Materializer.PositionSequences[0]);
}
[Fact]
@ -1114,10 +1130,16 @@ public sealed class LiveEntityHydrationControllerTests
Assert.Equal((ushort)1, fixture.Record.Generation);
Assert.Equal("same generation newer", fixture.Objects.Get(Guid)!.Name);
Assert.True(fixture.Record.InitialHydrationCompleted);
Assert.Equal((ushort)3, fixture.Materializer.PositionSequences[^1]);
// C3c: the nested fresher same-generation Create is admitted into
// the replacement generation's ACTIVE residence FIFO (AD-59) and its
// facts commit at the executor drain, in array order. The
// materialization therefore runs from the admission-frozen seq-2
// replacement create; the seq-3 facts (including the name asserted
// above) land through the drain and never re-materialize.
Assert.Equal((ushort)2, fixture.Materializer.PositionSequences[^1]);
Assert.DoesNotContain(
(ushort)2,
fixture.Materializer.PositionSequences.Skip(1));
(ushort)3,
fixture.Materializer.PositionSequences);
}
[Fact]
@ -1210,11 +1232,19 @@ public sealed class LiveEntityHydrationControllerTests
Assert.True(record.InitialHydrationCompleted);
Assert.Equal("same generation newer", fixture.Objects.Get(Guid)!.Name);
Assert.Equal([1, 1, 2], fixture.Materializer.PositionSequences);
// C3c: a post-residence same-generation Create is description-only
// at registration — its position churn flows through the
// freshness-gated events tail (which is what makes RecoverProjection
// return false above), and create authority advances only inside the
// residence transaction. No drift means no nested
// CreateSupersessionRecovery re-materialization: the recovery's own
// SpatialRecovery attempt stays the last call at the original
// installed version.
Assert.Equal([1, 1], fixture.Materializer.PositionSequences);
Assert.Equal(
LiveProjectionPurpose.CreateSupersessionRecovery,
LiveProjectionPurpose.SpatialRecovery,
fixture.Materializer.Calls[^1].Purpose);
Assert.Equal(2UL, fixture.Materializer.InstalledCreateIntegrationVersion);
Assert.Equal(1UL, fixture.Materializer.InstalledCreateIntegrationVersion);
}
[Theory]
@ -1236,10 +1266,23 @@ public sealed class LiveEntityHydrationControllerTests
if (refreshed || spawn.PositionSequence != 1)
return;
refreshed = true;
fixture.Controller.OnCreate(Spawn(
Generation: 1,
PositionSequence: 2,
Name: "recovery v2"));
// C3c: a fresher same-generation CreateObject no longer advances
// create authority at registration (its advance lands at the
// residence drain's WeenieDescription stage). Model that exact
// advance directly so the drift-retry machinery under test still
// fires.
// C3c-R1 F3 (coordinator resolution): this hand-call is an
// honest MODEL of the executor drain's advance
// (RuntimeInitialCreateContinuationExecutor
// .ApplyWeenieDescriptionAction — the sole production site,
// source-pinned by C3cR1F3DriftModelSourcePinTests). A nested
// production OnCreate can no longer reach it here:
// post-residence ExistingGeneration registration is
// description-only (RuntimeEntityObjectLifetime gates the
// advance on !beginInitialResidence, :660-665) and
// ConsumeExecuted already removed the completed residence
// entry, closing the FIFO-adoption path.
record.Canonical.AdvanceCreateAuthority();
};
fixture.Materializer.ThrowAfterMaterializePurposeOnce =
LiveProjectionPurpose.CreateSupersessionRecovery;
@ -1270,8 +1313,12 @@ public sealed class LiveEntityHydrationControllerTests
Assert.False(record.CreateProjectionSynchronizationPending);
Assert.Same(retained, record.WorldEntity);
// C3c: the retry retransmit (a post-residence same-generation
// Create) no longer advances create authority itself, so both retry
// paths install the drift probe's version (2), not a
// retransmit-advanced 3.
Assert.Equal(
retryFromLandblock ? 2UL : 3UL,
2UL,
fixture.Materializer.InstalledCreateIntegrationVersion);
Assert.Equal(
LiveProjectionPurpose.CreateSupersessionRecovery,
@ -1357,10 +1404,23 @@ public sealed class LiveEntityHydrationControllerTests
if (refreshed || spawn.PositionSequence != 1)
return;
refreshed = true;
fixture.Controller.OnCreate(Spawn(
Generation: 1,
PositionSequence: 2,
Name: "ready v2"));
// C3c: a fresher same-generation CreateObject no longer advances
// create authority at registration (its advance lands at the
// residence drain's WeenieDescription stage). Model that exact
// advance directly so the drift-retry machinery under test still
// fires.
// C3c-R1 F3 (coordinator resolution): this hand-call is an
// honest MODEL of the executor drain's advance
// (RuntimeInitialCreateContinuationExecutor
// .ApplyWeenieDescriptionAction — the sole production site,
// source-pinned by C3cR1F3DriftModelSourcePinTests). A nested
// production OnCreate can no longer reach it here:
// post-residence ExistingGeneration registration is
// description-only (RuntimeEntityObjectLifetime gates the
// advance on !beginInitialResidence, :660-665) and
// ConsumeExecuted already removed the completed residence
// entry, closing the FIFO-adoption path.
record.Canonical.AdvanceCreateAuthority();
};
fixture.Ready.FailPublishCount = 1;
@ -1486,6 +1546,12 @@ public sealed class LiveEntityHydrationControllerTests
{
const uint parentGuid = 0x70000002u;
using var fixture = new Fixture(originKnown: true);
// C3c: a Create whose parent is not addressable is now queued under
// the parent's GUID (retail QueueBlobForObject) instead of applying
// immediately. Register the parent so the nested parented Create
// routes exactly as before.
fixture.Runtime.RegisterLiveEntity(
Spawn(Generation: 1, PositionSequence: 1) with { Guid = parentGuid });
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
LiveEntityRecord record = fixture.Record;
WorldEntity retained = record.WorldEntity!;
@ -1522,6 +1588,17 @@ public sealed class LiveEntityHydrationControllerTests
record,
positionVersion));
Assert.True(fixture.Runtime.WithdrawLiveEntityProjection(record));
// C3c: mirror the production relationship owner
// (EquippedChildRenderController.TryAttach), which converts a
// residence-managed child's sticky residence to LegacyImmediate
// at the world -> attached kind transition.
if (record.MaterializationResidence is
AcDream.App.World.LiveEntityMaterializationResidence
.AwaitRuntimePlacement)
{
record.MaterializationResidence = AcDream.App.World
.LiveEntityMaterializationResidence.LegacyImmediate;
}
WorldEntity? attached = fixture.Runtime.MaterializeLiveEntity(
Guid,
Cell,
@ -1540,6 +1617,12 @@ public sealed class LiveEntityHydrationControllerTests
fixture.Controller.OnCreate(CelllessSpawn(
PositionSequence: 2,
parentGuid));
// C3c: a post-residence same-generation Create no longer
// advances create authority at registration (the advance lands
// at a residence drain's WeenieDescription stage). Model that
// advance directly so the supersession-recovery machinery under
// test still fires and completes at the attached-ready boundary.
record.Canonical.AdvanceCreateAuthority();
};
Assert.False(fixture.Controller.RecoverProjection(
@ -1575,6 +1658,25 @@ public sealed class LiveEntityHydrationControllerTests
using var fixture = new Fixture(
originKnown: true,
playerGuid: retryFromLandblock ? Guid : 0u);
// C3c: a Create whose parent is not addressable is now queued under
// the parent's GUID (retail QueueBlobForObject) instead of applying
// immediately. Register the parent so the initial parented Create
// routes exactly as before — placed in a DIFFERENT landblock so this
// scaffolding identity is not itself a candidate for the recovered
// landblock's projection sweep (the child's RegisterCount assertions
// count only the child's resources).
WorldSession.EntitySpawn parentSpawn =
Spawn(Generation: 1, PositionSequence: 1) with { Guid = parentGuid };
var parentPosition = new CreateObject.ServerPosition(
0x01020001u, 10f, 10f, 5f, 1f, 0f, 0f, 0f);
fixture.Runtime.RegisterLiveEntity(parentSpawn with
{
Position = parentPosition,
Physics = parentSpawn.Physics!.Value with
{
Position = parentPosition,
},
});
fixture.Ready.FailPublishCount = 2;
fixture.Network.ApplyAction = events =>
{
@ -1659,8 +1761,20 @@ public sealed class LiveEntityHydrationControllerTests
if (!replaced && replacementStage == stage)
{
replaced = true;
fixture.Runtime.RegisterLiveEntity(
Spawn(Generation: 1, PositionSequence: 2));
// C3c: a fresher same-generation CreateObject no longer
// advances create authority at registration — its advance
// lands at the residence drain's WeenieDescription stage.
// Model that exact advance directly so the between-stage
// revalidation guard stays covered.
// C3c-R1 F3: honest MODEL of the executor drain's advance
// (ApplyWeenieDescriptionAction — the sole production site,
// source-pinned by C3cR1F3DriftModelSourcePinTests); a
// nested production OnCreate can no longer reach it —
// post-residence registration is description-only
// (RuntimeEntityObjectLifetime :660-665,
// !beginInitialResidence gate) and ConsumeExecuted already
// removed the completed residence entry.
expected.Canonical.AdvanceCreateAuthority();
}
return true;
}
@ -1768,8 +1882,18 @@ public sealed class LiveEntityHydrationControllerTests
var projection = new RecordingLiveProjectionSink(
_ =>
{
fixture.Runtime.RegisterLiveEntity(
Spawn(Generation: 1, PositionSequence: 2));
// C3c: a fresher same-generation CreateObject's authority
// advance now lands at the residence drain's
// WeenieDescription stage; model that advance directly.
// C3c-R1 F3: honest MODEL of the executor drain's advance
// (ApplyWeenieDescriptionAction — the sole production site,
// source-pinned by C3cR1F3DriftModelSourcePinTests); a
// nested production OnCreate can no longer reach it —
// post-residence registration is description-only
// (RuntimeEntityObjectLifetime :660-665,
// !beginInitialResidence gate) and ConsumeExecuted already
// removed the completed residence entry.
fixture.Record.Canonical.AdvanceCreateAuthority();
return true;
});
var publisher = new LiveEntityReadyPublisher(
@ -1792,10 +1916,19 @@ public sealed class LiveEntityHydrationControllerTests
WorldEntity entity = record.WorldEntity!;
ulong capturedCreateIntegrationVersion = record.CreateIntegrationVersion;
// Models ProjectionPoseReady synchronously accepting a fresher
// same-generation CreateObject before EntityReady is emitted.
fixture.Runtime.RegisterLiveEntity(
Spawn(Generation: 1, PositionSequence: 2));
// Models ProjectionPoseReady synchronously observing a fresher
// same-generation CreateObject's authority advance before
// EntityReady is emitted. C3c: that advance now lands at the
// residence drain's WeenieDescription stage
// (AdvanceCreateAuthority), not at registration; model it directly.
// C3c-R1 F3: honest MODEL of the executor drain's advance
// (ApplyWeenieDescriptionAction — the sole production site,
// source-pinned by C3cR1F3DriftModelSourcePinTests); a nested
// production OnCreate can no longer reach it — post-residence
// registration is description-only (RuntimeEntityObjectLifetime
// :660-665, !beginInitialResidence gate) and ConsumeExecuted
// already removed the completed residence entry.
record.Canonical.AdvanceCreateAuthority();
bool published = false;
Assert.False(EquippedChildRenderController.PublishEntityReadyExact(
@ -1937,6 +2070,55 @@ public sealed class LiveEntityHydrationControllerTests
uint playerGuid = 0u)
{
Resources = resources ?? new RecordingResources();
// C3c: initial-Create registration begins the canonical
// residence, whose admission requires a live generation; the
// fixture also commits the wire landblock's collision generation
// and wires the production first-entry drive pump so each Create
// transaction completes its conductor synchronously, exactly
// like the composed graphical host.
EntityObjects.BindEventContext(
static () => new AcDream.Runtime.RuntimeGenerationToken(1UL),
static () => 1UL);
EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
Cell & 0xFFFF0000u, 1UL);
EntityObjects.Physics.Engine.AddLandblock(
Cell & 0xFFFF0000u,
new AcDream.Core.Physics.TerrainSurface(
new byte[81], new float[256]),
Array.Empty<AcDream.Core.Physics.CellSurface>(),
Array.Empty<AcDream.Core.Physics.PortalPlane>(),
worldOffsetX: 0f,
worldOffsetY: 0f);
EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
Cell & 0xFFFF0000u, 1UL, ready: true);
Movement = new AcDream.Runtime.Gameplay.RuntimeLocalPlayerMovementState();
IdentityState = new AcDream.Runtime.Gameplay.RuntimeLocalPlayerIdentityState();
var publication = new AcDream.Runtime.Gameplay
.RuntimeLocalPlayerPhysicsPublicationState(
EntityObjects.Entities,
EntityObjects.Physics,
Movement,
IdentityState);
Movement.AttachPhysicsPublication(publication);
EntityObjects.LocalPlayerFirstEntry.BindPublication(publication);
IdentityState.ServerGuid = playerGuid;
FirstEntry = new AcDream.Runtime.Session.RuntimeFirstEntryDriveController(
EntityObjects,
new AcDream.Runtime.GameRuntimeClock(),
new HydrationNullCollisionSource(),
() => AcDream.Runtime.Gameplay.PlayerMovementConstructionOptions.Fallback,
static _ => new AcDream.Runtime.Gameplay
.RuntimeLocalPlayerPhysicsActivationPreparation(
0.48f,
1.835f,
AcDream.Runtime.Gameplay
.RuntimeLocalPlayerShadowDisposition.ProvenShapeless));
// C3c: the real per-session placement subscription — without it
// the first entity's unacknowledged ExecutorCompleted receipt
// wedges the one ordered FIFO and every later Create's conductor
// yields AwaitingReceiptAcknowledgement forever. Ack rules mirror
// production: Discard/ExecutorCompleted acknowledge-only;
// Place/Withdraw stay at the head for the conductor machinery.
var spatial = new GpuWorldState();
spatial.AddLandblock(new LoadedLandblock(
0x0101FFFFu,
@ -1947,6 +2129,11 @@ public sealed class LiveEntityHydrationControllerTests
Resources,
Teardown,
EntityObjects);
_placements = new AcDream.Runtime.Physics
.RuntimePlacementProjectionSubscription(
EntityObjects.Placements,
static () => new AcDream.Runtime.RuntimeGenerationToken(1UL),
new FixturePlacementSink(Runtime));
Materializer = new RecordingMaterializer(Runtime, Operations);
Relationships = new RecordingRelationships(Operations);
Ready = new RecordingReadyPublisher(Operations);
@ -1988,7 +2175,90 @@ public sealed class LiveEntityHydrationControllerTests
Timestamps,
identity,
deletion,
Dormant);
Dormant,
firstEntry: FirstEntry);
}
public AcDream.Runtime.Session.RuntimeFirstEntryDriveController FirstEntry { get; }
private readonly AcDream.Runtime.Physics
.RuntimePlacementProjectionSubscription _placements;
private sealed class FixturePlacementSink(LiveEntityRuntime runtime)
: AcDream.Runtime.Physics.IRuntimePlacementProjectionSink
{
public bool TryApply(
in AcDream.Runtime.Physics.RuntimePlacementProjectionSnapshot projection)
{
if (projection.Kind is AcDream.Runtime.Physics
.RuntimePlacementProjectionKind.Discard)
{
return true;
}
if (projection.Kind is AcDream.Runtime.Physics
.RuntimePlacementProjectionKind.ExecutorCompleted)
{
return projection.Token.ExactCellId == 0u
|| runtime.TryApplyInitialCreateCompletionPresentation(
in projection);
}
return !runtime.HasActiveInitialCreateResidence(
projection.Token.Entity)
&& runtime.TryApplyRuntimePlacementProjection(in projection);
}
}
public AcDream.Runtime.Gameplay.RuntimeLocalPlayerMovementState Movement { get; }
public AcDream.Runtime.Gameplay.RuntimeLocalPlayerIdentityState IdentityState { get; }
private sealed class HydrationNullCollisionSource
: AcDream.Content.IPreparedCollisionSource
{
public AcDream.Content.PreparedAssetPresence ProbeCollision(
AcDream.Content.Pak.PakAssetType type,
uint sourceFileId) =>
AcDream.Content.PreparedAssetPresence.Available;
public AcDream.Content.PreparedCollisionReadResult<
AcDream.Core.Physics.FlatSetupCollision> ReadSetupCollision(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
AcDream.Content.PreparedCollisionReadResult<
AcDream.Core.Physics.FlatSetupCollision>.Loaded(
new AcDream.Core.Physics.FlatSetupCollision(
System.Collections.Immutable.ImmutableArray<
AcDream.Core.Physics.FlatCollisionCylinder>.Empty,
[new AcDream.Core.Physics.FlatCollisionSphere(
System.Numerics.Vector3.Zero, 0.48f)],
height: 0f,
radius: 0f,
stepUpHeight: 0.4f,
stepDownHeight: 0.4f));
public AcDream.Content.PreparedCollisionReadResult<
AcDream.Core.Physics.FlatGfxObjCollisionAsset>
ReadGfxObjCollision(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public AcDream.Content.PreparedCollisionReadResult<
AcDream.Core.Physics.FlatCellStructureCollisionAsset>
ReadCellStructureCollision(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public AcDream.Content.PreparedCollisionReadResult<
AcDream.Core.Physics.FlatEnvCellTopology> ReadEnvCellTopology(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public AcDream.Content.PreparedCollisionSourceStats CollisionStats =>
default;
public void Dispose()
{
}
}
public LiveEntityRecord Record
@ -2165,7 +2435,34 @@ public sealed class LiveEntityHydrationControllerTests
},
LiveEntityProjectionKind.World,
initializeProjection: null,
out LiveEntityRecord? expectedRecord);
out LiveEntityRecord? expectedRecord,
// C3c: mirror the production materializer
// (DatLiveEntityProjectionMaterializer.MaterializeProjection),
// which materializes route-1 world creates residence-managed.
// The legacy-immediate default would commit the wire cell
// out-of-band and retire the fresh residence lease before the
// fixture's drive pump ever ran (diagnosed RejectedToken).
AcDream.App.World.LiveEntityMaterializationResidence
.AwaitRuntimePlacement);
// C3c: mirror the production materializer's self-projection
// branch — when the residence-driven placement already committed
// (or a legacy post-residence path committed the cell) before
// this sidecar could exist, its completion receipt is gone, so
// presentation self-projects from the committed canonical state
// through the presentation-only bucket path.
if (entity is not null
&& expectedRecord is not null
&& runtime.IsCurrentCreateIntegration(
expectedCanonical,
expectedCreateIntegrationVersion)
&& expectedCanonical.FullCellId != 0u
&& !runtime.HasActiveInitialCreateResidence(expectedCanonical)
&& !runtime.RebucketLiveEntity(
canonicalSpawn.Guid,
expectedCanonical.FullCellId))
{
return false;
}
if (ThrowAfterMaterializePurposeOnce == purpose)
{
ThrowAfterMaterializePurposeOnce = null;

View file

@ -423,6 +423,11 @@ public sealed class LiveEntityLifecycleStressTests
canAdvanceOwner: ownerId => _effects?.CanAdvanceOwner(ownerId) ?? true);
EntityObjects = new RuntimeEntityObjectLifetime(Engine);
// C3c: initial-Create registration begins the canonical residence,
// whose admission requires a live session generation.
EntityObjects.BindEventContext(
static () => new AcDream.Runtime.RuntimeGenerationToken(1UL),
static () => 1UL);
Runtime = new LiveEntityRuntime(
Spatial,
new DelegateLiveEntityResourceLifecycle(

View file

@ -639,6 +639,9 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
teardown);
private static WorldSession.EntitySpawn Spawn(uint guid, ushort instance) =>
// C3c: residence admission requires the flattened identity fields to
// agree with a nested PhysicsDesc block; a bare logical fixture
// carries the minimal consistent one (instance timestamp only).
new(
Guid: guid,
Position: null,
@ -652,7 +655,37 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
ItemType: null,
MotionState: null,
MotionTableId: null,
InstanceSequence: instance);
InstanceSequence: instance,
Physics: new PhysicsSpawnData(
RawState: 0u,
Position: null,
Movement: null,
AnimationFrame: null,
SetupTableId: null,
MotionTableId: null,
SoundTableId: null,
PhysicsScriptTableId: null,
Parent: null,
Children: null,
Scale: null,
Friction: null,
Elasticity: null,
Translucency: null,
Velocity: null,
Acceleration: null,
AngularVelocity: null,
DefaultScriptType: null,
DefaultScriptIntensity: null,
Timestamps: new PhysicsTimestamps(
Position: 0,
Movement: 0,
State: 0,
Vector: 0,
Teleport: 0,
ServerControlledMove: 0,
ForcePosition: 0,
ObjDesc: 0,
Instance: instance)));
private static AcDream.Core.World.WorldEntity Entity(
uint localId,

View file

@ -733,7 +733,10 @@ public sealed class LiveEntityPresentationControllerTests
{
var position = new CreateObject.ServerPosition(
0x01010001u, 10f, 10f, 5f, 1f, 0f, 0f, 0f);
var timestamps = new PhysicsTimestamps(1, 1, 1, 1, 0, 1, 0, 1, 1);
// C3c: residence admission requires the nested block's Instance
// timestamp to agree with the flattened InstanceSequence.
var timestamps = new PhysicsTimestamps(
1, 1, 1, 1, 0, 1, 0, 1, instanceSequence);
var physics = new PhysicsSpawnData(
RawState: (uint)state,
Position: position,

View file

@ -247,6 +247,9 @@ public sealed class LiveEntityProjectionWithdrawalControllerTests
MotionState: null,
MotionTableId: null,
InstanceSequence: instance);
// C3c: residence admission requires the flattened parser
// projections to agree with a nested PhysicsDesc block.
spawn = spawn.WithConsistentPhysics();
LiveEntityRecord record = Live.RegisterAndMaterializeProjection(
spawn,
id => new WorldEntity

View file

@ -826,13 +826,22 @@ public sealed class LiveEntityRuntimeTests
{
const uint parentGuid = 0x70000020u;
const uint childGuid = 0x70000021u;
var runtime = LiveEntityRuntimeFixture.Create(new GpuWorldState(), new RecordingResources());
// C3c: initial residences defer wire applies into their FIFO until a
// host pump drives the conductors; use the driven fixture and pump
// after each Create so the parent-event tail commits as before.
LiveEntityRuntimeFixture.DrivenLiveEntityRuntime driven =
LiveEntityRuntimeFixture.CreateDriven(
new GpuWorldState(),
new RecordingResources());
LiveEntityRuntime runtime = driven.Runtime;
runtime.RegisterLiveEntity(Spawn(parentGuid, 9, 1, 0x01010001u));
driven.Pump();
runtime.ParentAttachments.Enqueue(new ParentEvent.Parsed(
parentGuid, childGuid, 1, 2, 9, 5));
ResolveParent(runtime, childGuid);
runtime.RegisterLiveEntity(Spawn(childGuid, 3, 4, 0x01010001u));
driven.Pump();
ResolveParent(runtime, childGuid);
Assert.True(runtime.ParentAttachments.TryGetProjection(
@ -1261,6 +1270,143 @@ public sealed class LiveEntityRuntimeTests
Assert.Equal(0.0, clock.PendingSeconds, 8);
}
/// <summary>
/// C3c-R1 review R2: the presentation-only rebucket shortcut is scoped
/// to the ACTIVE initial-create residence (where the public API is
/// suppressed outright — the conductor's completion receipt is the only
/// presentation channel). A RETIRED-residence entity's sticky
/// MaterializationResidence must NOT keep it on the shortcut: the
/// unflipped legacy update routes (network position/state, teleports,
/// streaming reprojection, hydration recovery — all callers of this one
/// public RebucketLiveEntity chokepoint) are the position authority
/// again, so post-residence moves take the FULL legacy branch:
/// CommitRebucket writes the canonical cell and retail's
/// prepare_to_enter_world (0x00511FA0) clock rebase runs on every
/// root-workset membership edge.
/// </summary>
[Fact]
public void PostResidenceRebucket_TakesTheFullLegacyPathIncludingTheClockEdge()
{
const uint guid = 0x7000004Au;
var spatial = new GpuWorldState();
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
spatial.AddLandblock(EmptyLandblock(0x0102FFFFu));
LiveEntityRuntimeFixture.DrivenLiveEntityRuntime driven =
LiveEntityRuntimeFixture.CreateDriven(
spatial,
new RecordingResources());
LiveEntityRuntime runtime = driven.Runtime;
RuntimeEntityRecord canonical =
Assert.IsType<RuntimeEntityRecord>(runtime.RegisterLiveEntity(
Spawn(guid, 1, 1, 0x01010001u)).Canonical);
runtime.MaterializeLiveEntity(
canonical,
0x01010001u,
id => Entity(id, guid),
LiveEntityProjectionKind.World,
initializeProjection: null,
out _,
LiveEntityMaterializationResidence.AwaitRuntimePlacement);
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record));
// ACTIVE residence: the public API stays suppressed (the completion
// receipt is the entity's first world-visible moment) and no legacy
// cell commit can race the conductor's pending placement.
Assert.True(runtime.HasActiveInitialCreateResidence(canonical));
Assert.False(runtime.RebucketLiveEntity(guid, 0x01020001u));
Assert.Equal(0u, canonical.FullCellId);
// C3c-R1 review F5: the tracked-but-undriven entry is visible in
// the entity-object ownership ledger while it awaits its pump.
Assert.Equal(
1,
driven.Lifetime.CaptureOwnership().FirstEntryDrivePendingCount);
driven.Pump();
Assert.False(runtime.HasActiveInitialCreateResidence(canonical));
Assert.Equal(
LiveEntityMaterializationResidence.AwaitRuntimePlacement,
record.MaterializationResidence);
Assert.Equal(0x01010001u, canonical.FullCellId);
// C3c-R1 review F5: the drive's tracked entries fold into the
// entity-object ownership ledger — one pending entry while the
// residence awaited its pump, zero after.
Assert.Equal(
0,
driven.Lifetime.CaptureOwnership().FirstEntryDrivePendingCount);
// RETIRED residence, loaded-to-loaded: full legacy branch commits
// the canonical cell (the presentation-only shortcut never did) and
// preserves the running clock.
RetailObjectQuantumClock clock = record.ObjectClock;
Assert.Equal(0, clock.Advance(0.02).Count);
Assert.True(runtime.RebucketLiveEntity(guid, 0x01020001u));
Assert.Equal(0x01020001u, canonical.FullCellId);
Assert.Same(clock, record.ObjectClock);
Assert.True(clock.IsActive);
Assert.Equal(0.02, clock.PendingSeconds, 8);
// Membership edge into a pending bucket suspends the clock; the
// pending drain's reentry rebases it for enter-world — the retail
// prepare_to_enter_world edge the shortcut skipped.
Assert.True(runtime.RebucketLiveEntity(guid, 0x02020001u));
Assert.Equal(0x02020001u, canonical.FullCellId);
Assert.False(clock.IsActive);
spatial.AddLandblock(EmptyLandblock(0x0202FFFFu));
Assert.True(clock.IsActive);
Assert.Equal(0.0, clock.PendingSeconds, 8);
}
/// <summary>
/// C3c-R1 review F1: converting the sticky residence-managed
/// presentation kind to LegacyImmediate (the equipped-child
/// world→attached transition) is the owner's explicit API — it refuses
/// while the initial-create residence lease is still active, because an
/// attached materialization would otherwise race the conductor's
/// pending placement.
/// </summary>
[Fact]
public void ResidenceConversionToLegacyImmediate_RefusesWhileTheResidenceIsActive()
{
const uint guid = 0x7000004Bu;
var spatial = new GpuWorldState();
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
LiveEntityRuntimeFixture.DrivenLiveEntityRuntime driven =
LiveEntityRuntimeFixture.CreateDriven(
spatial,
new RecordingResources());
LiveEntityRuntime runtime = driven.Runtime;
RuntimeEntityRecord canonical =
Assert.IsType<RuntimeEntityRecord>(runtime.RegisterLiveEntity(
Spawn(guid, 1, 1, 0x01010001u)).Canonical);
runtime.MaterializeLiveEntity(
canonical,
0x01010001u,
id => Entity(id, guid),
LiveEntityProjectionKind.World,
initializeProjection: null,
out _,
LiveEntityMaterializationResidence.AwaitRuntimePlacement);
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record));
Assert.Throws<InvalidOperationException>(() =>
runtime.ConvertMaterializationResidenceToLegacyImmediate(record));
Assert.Equal(
LiveEntityMaterializationResidence.AwaitRuntimePlacement,
record.MaterializationResidence);
driven.Pump();
runtime.ConvertMaterializationResidenceToLegacyImmediate(record);
Assert.Equal(
LiveEntityMaterializationResidence.LegacyImmediate,
record.MaterializationResidence);
// Idempotent once converted (and a no-op for legacy records).
runtime.ConvertMaterializationResidenceToLegacyImmediate(record);
Assert.Equal(
LiveEntityMaterializationResidence.LegacyImmediate,
record.MaterializationResidence);
}
[Fact]
public void InitiallyVisibleStaticObject_RebasesWithoutBecomingActive()
{
@ -1428,10 +1574,27 @@ public sealed class LiveEntityRuntimeTests
{
const uint stateBeforeBindGuid = 0x70000037u;
const uint bindBeforeStateGuid = 0x70000038u;
var runtime = LiveEntityRuntimeFixture.Create(new GpuWorldState(), new RecordingResources());
runtime.RegisterLiveEntity(Spawn(stateBeforeBindGuid, 1, 1, 0x01010001u));
runtime.RegisterLiveEntity(Spawn(bindBeforeStateGuid, 1, 1, 0x01010001u));
// C3c: initial residences defer wire applies into their FIFO until a
// host pump drives the conductors.
LiveEntityRuntimeFixture.DrivenLiveEntityRuntime driven =
LiveEntityRuntimeFixture.CreateDriven(
new GpuWorldState(),
new RecordingResources());
LiveEntityRuntime runtime = driven.Runtime;
RuntimeEntityRecord stateBeforeBindCanonical =
Assert.IsType<RuntimeEntityRecord>(runtime.RegisterLiveEntity(
Spawn(stateBeforeBindGuid, 1, 1, 0x01010001u)).Canonical);
RuntimeEntityRecord bindBeforeStateCanonical =
Assert.IsType<RuntimeEntityRecord>(runtime.RegisterLiveEntity(
Spawn(bindBeforeStateGuid, 1, 1, 0x01010001u)).Canonical);
// C3c/C3b: the first-entry conductor constructs the canonical body
// at Create (never-clobber: a fixture can no longer seed a
// replacement RemoteMotionRuntime over it). "Arrival order" is now
// SetState-before-the-drain (FIFO'd into the pending residence,
// applied against the conductor-built body at Execute) versus
// SetState-after-completion (legacy immediate apply). Both must
// leave the canonical body's state synchronized.
PhysicsStateFlags firstState = PhysicsStateFlags.Hidden
| PhysicsStateFlags.Gravity
| PhysicsStateFlags.ReportCollisions;
@ -1439,18 +1602,30 @@ public sealed class LiveEntityRuntimeTests
new SetState.Parsed(stateBeforeBindGuid, (uint)firstState, 1, 2),
out _));
runtime.MaterializeLiveEntity(
stateBeforeBindGuid,
stateBeforeBindCanonical,
0x01010001u,
id => Entity(id, stateBeforeBindGuid));
var lateBody = new RemoteMotionRuntime();
runtime.SetRemoteMotionRuntime(stateBeforeBindGuid, lateBody);
id => Entity(id, stateBeforeBindGuid),
LiveEntityProjectionKind.World,
initializeProjection: null,
out _,
LiveEntityMaterializationResidence.AwaitRuntimePlacement);
runtime.MaterializeLiveEntity(
bindBeforeStateGuid,
bindBeforeStateCanonical,
0x01010001u,
id => Entity(id, bindBeforeStateGuid));
var earlyBody = new RemoteMotionRuntime();
runtime.SetRemoteMotionRuntime(bindBeforeStateGuid, earlyBody);
id => Entity(id, bindBeforeStateGuid),
LiveEntityProjectionKind.World,
initializeProjection: null,
out _,
LiveEntityMaterializationResidence.AwaitRuntimePlacement);
driven.Pump();
PhysicsBody lateBody = runtime.GetOrCreatePhysicsBody(
stateBeforeBindGuid,
static _ => throw new InvalidOperationException(
"The conductor-built canonical body should already exist."));
PhysicsBody earlyBody = runtime.GetOrCreatePhysicsBody(
bindBeforeStateGuid,
static _ => throw new InvalidOperationException(
"The conductor-built canonical body should already exist."));
PhysicsStateFlags secondState = PhysicsStateFlags.Static
| PhysicsStateFlags.Ethereal
| PhysicsStateFlags.NoDraw;
@ -1460,8 +1635,8 @@ public sealed class LiveEntityRuntimeTests
Assert.Equal((firstState & ~PhysicsStateFlags.ReportCollisions)
| PhysicsStateFlags.IgnoreCollisions,
lateBody.Body.State);
Assert.Equal(secondState, earlyBody.Body.State);
lateBody.State);
Assert.Equal(secondState, earlyBody.State);
}
[Fact]
@ -1573,9 +1748,17 @@ public sealed class LiveEntityRuntimeTests
public void PositionAfterPickup_RequiresTeleportHookEvenWithEqualTeleportStamp()
{
const uint guid = 0x70000043u;
var runtime = LiveEntityRuntimeFixture.Create(new GpuWorldState(), new RecordingResources());
// C3c: initial residences defer wire applies into their FIFO until a
// host pump drives the conductors; use the driven fixture and pump
// after the Create so the pickup/position tail flows as before.
LiveEntityRuntimeFixture.DrivenLiveEntityRuntime driven =
LiveEntityRuntimeFixture.CreateDriven(
new GpuWorldState(),
new RecordingResources());
LiveEntityRuntime runtime = driven.Runtime;
WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u);
runtime.RegisterLiveEntity(spawn);
driven.Pump();
Assert.True(runtime.TryApplyPickup(
new PickupEvent.Parsed(guid, 1, 2),
out _));

View file

@ -0,0 +1,743 @@
using System.Numerics;
using AcDream.App.Input;
using AcDream.App.Physics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Streaming;
using AcDream.App.World;
using AcDream.Core.Plugins;
using AcDream.Core.Items;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.World;
using AcDream.Runtime.Entities;
using AcDream.Runtime.World;
using DatReaderWriter.DBObjs;
namespace AcDream.App.Tests.World;
/// <summary>
/// C3c contract integration tests: the flipped graphical host wiring driven
/// end-to-end — registration through <see cref="LiveEntityHydrationController"/>,
/// the REAL <see cref="RuntimePlacementPresentationSink"/> behind the REAL
/// <see cref="AcDream.Runtime.Physics.RuntimePlacementProjectionSubscription"/>,
/// and the production
/// <see cref="AcDream.Runtime.Session.RuntimeFirstEntryDriveController"/> pump.
/// No conductor is ever hand-called.
/// </summary>
public sealed class RuntimeFirstEntryHostIntegrationTests
{
private const uint Cell = 0x01010001u;
private const uint Guid = 0x70000301u;
[Fact]
public void InitialCreate_ResidenceConductorReceipt_BindsWorldVisibilityExactlyOnce()
{
using var fixture = new HostFixture(playerGuid: 0u);
int residencesBegan = 0;
fixture.EntityObjects.BindInitialResidenceBeginNotification(
_ => residencesBegan++);
bool visibleAtMaterialize = true;
fixture.Materializer.AfterMaterialize = record =>
{
// Clause 1: the sidecar exists but presentation stays suppressed
// until the conductor's completion receipt binds it.
visibleAtMaterialize = record.IsSpatiallyProjected
|| record.IsSpatiallyVisible;
};
fixture.Controller.OnCreate(Spawn(Guid, Cell));
Assert.Equal(1, residencesBegan);
Assert.False(visibleAtMaterialize);
Assert.True(fixture.Runtime.TryGetRecord(Guid, out LiveEntityRecord record));
// The residence was consumed by the conductor inside the Create
// transaction's own pump.
Assert.False(fixture.Runtime.HasActiveInitialCreateResidence(
record.Canonical));
Assert.Equal(Cell, record.Canonical.FullCellId);
Assert.True(record.IsSpatiallyProjected);
Assert.True(record.IsSpatiallyVisible);
Assert.NotNull(record.PhysicsBody);
Assert.Equal((record, true), Assert.Single(fixture.VisibilityEdges));
AcDream.Plugin.Abstractions.WorldEntitySnapshot snapshot =
Assert.Single(fixture.WorldState.Entities);
Assert.Equal(record.WorldEntity!.Position, snapshot.Position);
Assert.Equal(0, fixture.EntityObjects.Placements.PendingCount);
Assert.Equal(0, fixture.FirstEntry.PendingCount);
}
[Fact]
public void DeferredParentCreate_StaysInvisibleUntilParentReplay()
{
const uint parentGuid = 0x70000302u;
const uint childGuid = 0x70000303u;
using var fixture = new HostFixture(playerGuid: 0u);
int residencesBegan = 0;
fixture.EntityObjects.BindInitialResidenceBeginNotification(
_ => residencesBegan++);
fixture.Controller.OnCreate(ParentedSpawn(childGuid, parentGuid));
// Retail queues the raw blob under the parent's GUID; nothing about
// the child may escape — no canonical, no sidecar, no presentation.
Assert.Equal(0, residencesBegan);
Assert.False(fixture.Runtime.TryGetCanonical(childGuid, out _));
Assert.False(fixture.Runtime.TryGetRecord(childGuid, out _));
Assert.True(fixture.Runtime.ParentAttachments.ContainsDeferredCreate(
childGuid,
instanceSequence: 1));
Assert.Empty(fixture.WorldState.Entities);
Assert.Empty(fixture.VisibilityEdges);
fixture.Controller.OnCreate(Spawn(parentGuid, Cell));
// The replayed child's residence was recorded mid-drain; the next
// frame's pump (the per-frame retry phase) drives its conductor.
fixture.FirstEntry.DriveAll();
Assert.Equal(2, residencesBegan);
Assert.False(fixture.Runtime.ParentAttachments.ContainsDeferredCreate(
childGuid,
instanceSequence: 1));
Assert.True(fixture.Runtime.TryGetCanonical(
childGuid,
out RuntimeEntityRecord child));
Assert.False(fixture.Runtime.HasActiveInitialCreateResidence(child));
Assert.Equal(0, fixture.FirstEntry.PendingCount);
// The parented child is celless and presentation-suppressed until its
// own attach/position flow — only the parent is world-visible.
Assert.Equal(0u, child.FullCellId);
Assert.False(fixture.Runtime.TryGetRecord(childGuid, out _));
Assert.True(fixture.Runtime.TryGetRecord(
parentGuid,
out LiveEntityRecord parent));
Assert.True(parent.IsSpatiallyVisible);
Assert.Single(fixture.WorldState.Entities);
}
[Fact]
public void LocalLogin_PresentationAttachFailure_RetriesWithoutRuntimeRollback()
{
using var fixture = new HostFixture(playerGuid: Guid);
// The camera/shadow-analog App attach failure: the first
// world-visibility binding throws AFTER Runtime committed the
// controller/body/placement.
fixture.VisibilityFailuresRemaining = 1;
fixture.Controller.OnCreate(Spawn(Guid, Cell));
// Runtime is NOT rolled back by the App-side presentation failure:
// the published movement controller, canonical body, and committed
// cell all survive; only the completion receipt stays pending for
// the per-frame retry.
AcDream.Runtime.Gameplay.PlayerMovementController controller =
Assert.IsType<AcDream.Runtime.Gameplay.PlayerMovementController>(
fixture.Movement.Controller);
Assert.True(controller.IsRuntimePublished);
Assert.True(fixture.Runtime.TryGetRecord(Guid, out LiveEntityRecord record));
Assert.Equal(Cell, record.Canonical.FullCellId);
Assert.NotNull(record.PhysicsBody);
Assert.False(fixture.Runtime.HasActiveInitialCreateResidence(
record.Canonical));
Assert.Equal(1, fixture.EntityObjects.Placements.PendingCount);
Assert.Equal((record, true), Assert.Single(fixture.VisibilityEdges));
Assert.True(fixture.Subscription.RetryPending());
Assert.Same(controller, fixture.Movement.Controller);
Assert.True(controller.IsRuntimePublished);
Assert.Equal(0, fixture.EntityObjects.Placements.PendingCount);
Assert.True(record.IsSpatiallyProjected);
Assert.True(record.IsSpatiallyVisible);
Assert.Equal(2, fixture.VisibilityEdges.Count);
}
/// <summary>
/// C3c-F5: through the REAL flipped host wiring (hydration ->
/// residence -> conductor -> publication -> dormant activation), a
/// login onto flat ground must complete with retail's
/// first-gravity-frame contact (SmartBox::HandleCreateObject 0x00454C80
/// -> init_player 0x00455010 -> CPhysicsObj::enter_world 0x00516170 +
/// the first simulated frame's touch, compressed via the shared #270
/// settle) — and the outbound motion snapshot must report grounded,
/// the exact bit LocalPlayerOutboundController serializes and ACE's
/// "You can't do that while in the air!" gate reads. Spawn feet at 5
/// over a flat floor at 4.7 (0.3 m inside the settle reach), with the
/// production human bottom-sphere origin so the authored placement
/// stands clear of the floor.
/// </summary>
[Fact]
public void LocalLogin_FlatGround_ReportsGroundedOutboundContactBit()
{
using var fixture = new HostFixture(
playerGuid: Guid,
terrainHeight: 4.7f,
moverSphereOriginZ: 0.475f);
fixture.Controller.OnCreate(Spawn(Guid, Cell));
AcDream.Runtime.Gameplay.PlayerMovementController controller =
Assert.IsType<AcDream.Runtime.Gameplay.PlayerMovementController>(
fixture.Movement.Controller);
Assert.True(controller.IsRuntimePublished);
Assert.True(fixture.Runtime.TryGetRecord(
Guid,
out LiveEntityRecord record));
PhysicsBody body = Assert.IsType<PhysicsBody>(record.PhysicsBody);
Assert.True(body.InWorld);
Assert.True(body.InContact);
Assert.True(body.OnWalkable);
Assert.True(body.ContactPlaneValid);
Assert.InRange(body.Position.Z, 4.65f, 4.76f);
Assert.True(controller.CanSendPositionEvent);
Assert.True(controller.CaptureMovementResult(
mouseLookEvent: false).IsOnGround);
Assert.Equal(0, fixture.FirstEntry.PendingCount);
}
/// <summary>
/// C3c-F5 counterpart through the same real wiring: a login spawn with
/// no floor within the settle's reach stays genuinely airborne — no
/// forced grounding anywhere in the first-entry sequence.
/// </summary>
[Fact]
public void LocalLogin_AirborneSpawn_StaysGenuinelyAirborne()
{
using var fixture = new HostFixture(
playerGuid: Guid,
moverSphereOriginZ: 0.475f);
fixture.Controller.OnCreate(Spawn(Guid, Cell));
AcDream.Runtime.Gameplay.PlayerMovementController controller =
Assert.IsType<AcDream.Runtime.Gameplay.PlayerMovementController>(
fixture.Movement.Controller);
Assert.True(controller.IsRuntimePublished);
Assert.True(fixture.Runtime.TryGetRecord(
Guid,
out LiveEntityRecord record));
PhysicsBody body = Assert.IsType<PhysicsBody>(record.PhysicsBody);
Assert.True(body.InWorld);
Assert.False(body.InContact);
Assert.False(body.OnWalkable);
Assert.False(controller.CanSendPositionEvent);
Assert.False(controller.CaptureMovementResult(
mouseLookEvent: false).IsOnGround);
}
[Fact]
public void GraphicalAndDirectHosts_CommitIdenticalFirstEntryRuntimeFacts()
{
// Graphical host: full flipped wiring.
using var graphical = new HostFixture(playerGuid: 0u);
graphical.Controller.OnCreate(Spawn(Guid, Cell));
Assert.True(graphical.Runtime.TryGetCanonical(
Guid,
out RuntimeEntityRecord graphicalRecord));
// Direct (no-window) host: the same canonical machinery with no
// presentation at all — registration, drive pump, ack-only
// subscription (the headless host shape).
LiveEntityRuntimeFixture.DrivenLiveEntityRuntime direct =
LiveEntityRuntimeFixture.CreateDriven(
new GpuWorldState(),
new NoopResources());
RuntimeEntityRecord directRecord = Assert.IsType<RuntimeEntityRecord>(
direct.Lifetime.RegisterEntityWithInitialResidence(
Spawn(Guid, Cell),
isLocalPlayer: false).Canonical);
Assert.True(direct.Lifetime.ApplyAcceptedSpawn(
directRecord,
directRecord.CreateIntegrationVersion,
directRecord.Snapshot,
replaceGeneration: false));
direct.Pump();
Assert.Equal(
FirstEntryFacts.Capture(graphicalRecord),
FirstEntryFacts.Capture(directRecord));
Assert.False(graphical.Runtime.HasActiveInitialCreateResidence(
graphicalRecord));
Assert.Equal(0, direct.FirstEntry.PendingCount);
Assert.Equal(0, graphical.FirstEntry.PendingCount);
}
private readonly record struct FirstEntryFacts(
uint ServerGuid,
ushort Incarnation,
uint? LocalEntityId,
uint FullCellId,
uint CanonicalLandblockId,
ulong PositionAuthorityVersion,
ulong PlacementCommitVersion,
ulong CreateIntegrationVersion,
ushort SnapshotPositionSequence,
bool HasBody,
Vector3 BodyPosition,
Quaternion BodyOrientation,
PhysicsStateFlags BodyState,
bool BodyInWorld)
{
internal static FirstEntryFacts Capture(RuntimeEntityRecord record) =>
new(
record.ServerGuid,
record.Incarnation,
record.LocalEntityId,
record.FullCellId,
record.CanonicalLandblockId,
record.PositionAuthorityVersion,
record.PlacementCommitVersion,
record.CreateIntegrationVersion,
record.Snapshot.PositionSequence,
record.PhysicsBody is not null,
record.PhysicsBody?.Position ?? default,
record.PhysicsBody?.Orientation ?? default,
record.PhysicsBody?.State ?? default,
record.PhysicsBody?.InWorld ?? false);
}
private static WorldSession.EntitySpawn Spawn(uint guid, uint cell)
{
var position = new CreateObject.ServerPosition(
cell, 10f, 10f, 5f, 1f, 0f, 0f, 0f);
var timestamps = new PhysicsTimestamps(
Position: 1,
Movement: 1,
State: 1,
Vector: 1,
Teleport: 0,
ServerControlledMove: 1,
ForcePosition: 0,
ObjDesc: 1,
Instance: 1);
var physics = new PhysicsSpawnData(
RawState: (uint)PhysicsStateFlags.ReportCollisions,
Position: position,
Movement: null,
AnimationFrame: null,
SetupTableId: 0x02000001u,
MotionTableId: 0x09000001u,
SoundTableId: null,
PhysicsScriptTableId: null,
Parent: null,
Children: null,
Scale: null,
Friction: null,
Elasticity: null,
Translucency: null,
Velocity: null,
Acceleration: null,
AngularVelocity: null,
DefaultScriptType: null,
DefaultScriptIntensity: null,
Timestamps: timestamps);
return new WorldSession.EntitySpawn(
guid,
position,
0x02000001u,
[],
[],
[],
null,
null,
"first entry",
(uint)ItemType.Creature,
null,
0x09000001u,
PhysicsState: (uint)PhysicsStateFlags.ReportCollisions,
InstanceSequence: 1,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1,
Physics: physics);
}
private static WorldSession.EntitySpawn ParentedSpawn(
uint guid,
uint parentGuid)
{
var timestamps = new PhysicsTimestamps(
Position: 1,
Movement: 1,
State: 1,
Vector: 1,
Teleport: 0,
ServerControlledMove: 1,
ForcePosition: 0,
ObjDesc: 1,
Instance: 1);
var physics = new PhysicsSpawnData(
RawState: (uint)PhysicsStateFlags.ReportCollisions,
Position: null,
Movement: null,
AnimationFrame: 1u,
SetupTableId: 0x02000001u,
MotionTableId: 0x09000001u,
SoundTableId: null,
PhysicsScriptTableId: null,
Parent: new PhysicsAttachment(parentGuid, LocationId: 1u),
Children: null,
Scale: null,
Friction: null,
Elasticity: null,
Translucency: null,
Velocity: null,
Acceleration: null,
AngularVelocity: null,
DefaultScriptType: null,
DefaultScriptIntensity: null,
Timestamps: timestamps);
return new WorldSession.EntitySpawn(
guid,
Position: null,
SetupTableId: 0x02000001u,
AnimPartChanges: [],
TextureChanges: [],
SubPalettes: [],
BasePaletteId: null,
ObjScale: null,
Name: "deferred child",
ItemType: (uint)ItemType.Creature,
MotionState: null,
MotionTableId: 0x09000001u,
PhysicsState: (uint)PhysicsStateFlags.ReportCollisions,
InstanceSequence: 1,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1,
ParentGuid: parentGuid,
ParentLocation: 1u,
PlacementId: 1u,
Physics: physics);
}
private sealed class HostFixture : IDisposable
{
internal readonly RuntimeEntityObjectLifetime EntityObjects = new();
internal readonly LiveEntityRuntime Runtime;
internal readonly LiveEntityHydrationController Controller;
internal readonly HostMaterializer Materializer;
internal readonly AcDream.Runtime.Session.RuntimeFirstEntryDriveController
FirstEntry;
internal readonly AcDream.Runtime.Physics
.RuntimePlacementProjectionSubscription Subscription;
internal readonly AcDream.Runtime.Gameplay.RuntimeLocalPlayerMovementState
Movement;
internal readonly WorldGameState WorldState = new();
internal readonly List<(LiveEntityRecord Record, bool Visible)>
VisibilityEdges = [];
internal int VisibilityFailuresRemaining;
internal HostFixture(
uint playerGuid,
float terrainHeight = 0f,
float moverSphereOriginZ = 0f)
{
EntityObjects.BindEventContext(
static () => new AcDream.Runtime.RuntimeGenerationToken(1UL),
static () => 1UL);
EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
Cell & 0xFFFF0000u, 1UL);
EntityObjects.Physics.Engine.AddLandblock(
Cell & 0xFFFF0000u,
new TerrainSurface(
new byte[81],
Enumerable.Repeat(terrainHeight, 256).ToArray()),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
worldOffsetX: 0f,
worldOffsetY: 0f);
EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
Cell & 0xFFFF0000u, 1UL, ready: true);
Movement = new AcDream.Runtime.Gameplay
.RuntimeLocalPlayerMovementState();
var runtimeIdentity = new AcDream.Runtime.Gameplay
.RuntimeLocalPlayerIdentityState();
var publication = new AcDream.Runtime.Gameplay
.RuntimeLocalPlayerPhysicsPublicationState(
EntityObjects.Entities,
EntityObjects.Physics,
Movement,
runtimeIdentity);
Movement.AttachPhysicsPublication(publication);
EntityObjects.LocalPlayerFirstEntry.BindPublication(publication);
runtimeIdentity.ServerGuid = playerGuid;
var spatial = new GpuWorldState();
spatial.AddLandblock(new LoadedLandblock(
(Cell & 0xFFFF0000u) | 0xFFFFu,
new LandBlock(),
Array.Empty<WorldEntity>()));
Runtime = new LiveEntityRuntime(
spatial,
new NoopResources(),
EntityObjects);
FirstEntry = new AcDream.Runtime.Session
.RuntimeFirstEntryDriveController(
EntityObjects,
new AcDream.Runtime.GameRuntimeClock(),
new SphereCollisionSource(moverSphereOriginZ),
static () => AcDream.Runtime.Gameplay
.PlayerMovementConstructionOptions.Fallback,
static _ => new AcDream.Runtime.Gameplay
.RuntimeLocalPlayerPhysicsActivationPreparation(
0.48f,
1.835f,
AcDream.Runtime.Gameplay
.RuntimeLocalPlayerShadowDisposition
.ProvenShapeless));
var sink = new RuntimePlacementPresentationSink(
Runtime,
new RuntimeWorldTransitState(),
WorldState,
new WorldEvents(),
new EntityEffectPoseRegistry(),
new LocalPlayerShadowState(),
() => playerGuid,
_ => { },
[
(record, visible) =>
{
VisibilityEdges.Add((record, visible));
if (VisibilityFailuresRemaining > 0)
{
VisibilityFailuresRemaining--;
throw new InvalidOperationException(
"fixture presentation attach failure");
}
},
]);
Subscription = new AcDream.Runtime.Physics
.RuntimePlacementProjectionSubscription(
EntityObjects.Placements,
static () => new AcDream.Runtime.RuntimeGenerationToken(1UL),
sink);
Materializer = new HostMaterializer(Runtime);
var identity = new LocalPlayerIdentityState
{
ServerGuid = playerGuid,
};
var dormant = new DormantLiveEntityStore();
var teardown = new NoopTeardown();
var deletion = new LiveEntityDeletionController(
Runtime,
EntityObjects,
teardown,
identity,
dormant);
Controller = new LiveEntityHydrationController(
Runtime,
EntityObjects,
new object(),
Materializer,
new NoopRelationships(),
new AcceptingReady(),
new KnownOrigin(),
new NoopNetworkSink(),
new NoopTimestamps(),
identity,
deletion,
dormant,
firstEntry: FirstEntry);
}
public void Dispose()
{
try
{
Runtime.Clear();
}
catch
{
// Failure-path tests assert their own exceptions.
}
}
}
/// <summary>
/// Mirrors the production materializer
/// (DatLiveEntityProjectionMaterializer.MaterializeProjection): route-1
/// world creates materialize residence-managed and self-project only when
/// the committed cell already exists with no active residence.
/// </summary>
private sealed class HostMaterializer(LiveEntityRuntime runtime)
: ILiveEntityProjectionMaterializer
{
internal Action<LiveEntityRecord>? AfterMaterialize { get; set; }
public bool TryMaterialize(
RuntimeEntityRecord expectedCanonical,
WorldSession.EntitySpawn canonicalSpawn,
LiveProjectionPurpose purpose,
ulong expectedCreateIntegrationVersion,
AcDream.App.Rendering.LiveEntityAppearanceUpdateState? appearanceUpdate = null)
{
if (canonicalSpawn.Position is not { } position
|| canonicalSpawn.SetupTableId is null)
{
return false;
}
WorldEntity? entity = runtime.MaterializeLiveEntity(
expectedCanonical,
position.LandblockId,
id => new WorldEntity
{
Id = id,
ServerGuid = canonicalSpawn.Guid,
SourceGfxObjOrSetupId = canonicalSpawn.SetupTableId.Value,
Position = new Vector3(
position.PositionX,
position.PositionY,
position.PositionZ),
Rotation = Quaternion.Identity,
MeshRefs = [],
ParentCellId = position.LandblockId,
},
LiveEntityProjectionKind.World,
initializeProjection: null,
out LiveEntityRecord? record,
LiveEntityMaterializationResidence.AwaitRuntimePlacement);
if (entity is null || record is null)
return false;
if (runtime.IsCurrentCreateIntegration(
expectedCanonical,
expectedCreateIntegrationVersion)
&& expectedCanonical.FullCellId != 0u
&& !runtime.HasActiveInitialCreateResidence(expectedCanonical)
&& !runtime.RebucketLiveEntity(
canonicalSpawn.Guid,
expectedCanonical.FullCellId))
{
return false;
}
AfterMaterialize?.Invoke(record);
return runtime.IsCurrentRecord(record);
}
public void ResetSessionState()
{
}
}
private sealed class NoopResources : ILiveEntityResourceLifecycle
{
public void Register(WorldEntity entity)
{
}
public void Unregister(WorldEntity entity)
{
}
}
private sealed class NoopTeardown : ILiveEntityTeardownCoordinator
{
public void TearDown(LiveEntityRecord record)
{
}
public void ForgetUnknownOwner(uint serverGuid)
{
}
}
private sealed class NoopRelationships : ILiveEntityRelationshipProjection
{
public void OnSpawn(WorldSession.EntitySpawn spawn)
{
}
public void OnParent(ParentEvent.Parsed update)
{
}
public void OnCreateParentAccepted(CreateParentUpdate update)
{
}
public ChildUnparentDisposition OnChildBecameUnparented(uint childGuid) =>
ChildUnparentDisposition.Completed;
public bool TryApplyAttachedAppearance(
LiveEntityRecord record,
ulong objDescAuthorityVersion) => false;
}
private sealed class AcceptingReady : ILiveEntityReadyPublisher
{
public bool Publish(LiveEntityReadyCandidate candidate) => true;
}
private sealed class KnownOrigin : ILiveEntityWorldOriginCoordinator
{
public bool IsKnown => true;
public LiveEntityOriginInitialization TryInitialize(
WorldSession.EntitySpawn spawn) => new(true, []);
}
private sealed class NoopNetworkSink : ILiveEntityNetworkUpdateSink
{
public void ApplySameGeneration(SameGenerationCreateObjectEvents events)
{
}
}
private sealed class NoopTimestamps : IAcceptedLocalPhysicsTimestampPublisher
{
public void Publish(uint serverGuid, AcceptedPhysicsTimestamps timestamps)
{
}
}
private sealed class SphereCollisionSource(float sphereOriginZ = 0f)
: AcDream.Content.IPreparedCollisionSource
{
public AcDream.Content.PreparedAssetPresence ProbeCollision(
AcDream.Content.Pak.PakAssetType type,
uint sourceFileId) =>
AcDream.Content.PreparedAssetPresence.Available;
public AcDream.Content.PreparedCollisionReadResult<
FlatSetupCollision> ReadSetupCollision(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
AcDream.Content.PreparedCollisionReadResult<FlatSetupCollision>
.Loaded(new FlatSetupCollision(
System.Collections.Immutable.ImmutableArray<
FlatCollisionCylinder>.Empty,
[new FlatCollisionSphere(
new Vector3(0f, 0f, sphereOriginZ),
0.48f)],
height: 0f,
radius: 0f,
stepUpHeight: 0.4f,
stepDownHeight: 0.4f));
public AcDream.Content.PreparedCollisionReadResult<
FlatGfxObjCollisionAsset> ReadGfxObjCollision(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public AcDream.Content.PreparedCollisionReadResult<
FlatCellStructureCollisionAsset> ReadCellStructureCollision(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public AcDream.Content.PreparedCollisionReadResult<
FlatEnvCellTopology> ReadEnvCellTopology(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public AcDream.Content.PreparedCollisionSourceStats CollisionStats =>
default;
public void Dispose()
{
}
}
}

View file

@ -757,6 +757,16 @@ public sealed class RuntimePlacementPresentationSinkTests
internal LiveEntityRecord Materialize(WorldSession.EntitySpawn spawn)
{
LiveEntityRecord record = Runtime.RegisterAndMaterializeProjection(spawn);
// C3c fixture normalization: RegisterAndMaterializeProjection's
// legacy-immediate rebucket commits the wire cell out-of-band of
// the fresh initial-create residence, leaving that residence
// stale. Converge it deterministically HERE (the query performs
// the lazy retirement, releasing the residence's never-driven
// initial SetPosition operation) so ownership snapshots captured
// by tests reflect the settled post-registration state instead of
// shifting inside the sink's own first residence-gate query.
Assert.False(Runtime.HasActiveInitialCreateResidence(
record.Canonical));
Assert.True(record.ResourcesRegistered);
WorldEntity entity = record.WorldEntity!;
var snapshot = new AcDream.Plugin.Abstractions.WorldEntitySnapshot(

View file

@ -740,16 +740,22 @@ public sealed class UpdateFrameOrchestratorTests
"AcDream.App",
"Input",
"PlayerModeController.cs"));
// C3c (clause 4 — sealed-setter lifecycle routing): the movement
// controller, physics body, host, and committed placement are
// Runtime-owned, published by the first-entry conductor's
// publication transaction. Player-mode entry attaches presentation
// only: it gates on the Runtime-published controller, then wires
// camera -> shadow -> host slot -> mode flag. The old pinned
// markers (PreparePositionForCommit / InstallOrRebind /
// CommitPreparedPosition / `_controllerSlot.Controller =`) were
// exactly the App-side controller construction+commit this flip
// deleted.
AssertAppearsInOrder(
playerModeSource,
"controller.PreparePositionForCommit(",
"controller.IsRuntimePublished",
"_camera.EnterChaseMode(legacyCamera, retailCamera);",
"EntityPhysicsHostComposition.SelectStableHostWithoutRebind(",
"_shadow.SyncPose(",
"EntityPhysicsHostComposition.InstallOrRebind(",
"playerEntity.SetPosition(initial.Position);",
"controller.CommitPreparedPosition();",
"_controllerSlot.Controller = controller;",
"_hostSlot.Host = playerHost;",
"_mode.IsPlayerMode = true;");
Assert.Contains("_shadow.Restore(playerEntity, priorShadow);", playerModeSource,
StringComparison.Ordinal);