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,59 @@
namespace AcDream.App.Tests.Input;
/// <summary>
/// C3c-F2 (2026-08-02): source pin for the production player-mode auto-entry
/// precondition. <c>PlayerModeAutoEntry</c> is a ONE-SHOT that disarms before
/// invoking its callback, and the production callback completes the world
/// reveal, so an attempt made before the Runtime first-entry conductor has
/// committed permanently seals the reveal with the player never in world —
/// the second link of the connected-gate login wedge
/// (logs/connected-world-gate-20260802-130455: one "not committed yet" line,
/// then event=complete with materialized=False and 5,577
/// readiness-after-terminal rejections). The production context therefore has
/// to report the same commit <c>PlayerModeController.TryEnter</c> requires.
/// The guard's own one-shot/latch behavior is covered behaviorally by
/// AcDream.Core.Tests.Input.AutoEnterPlayerModeTests; only the production
/// context's dependency graph (a ~15-dependency PlayerModeController) has no
/// focused harness, hence the source pin.
/// </summary>
public sealed class C3cF2AutoEntryWiringTests
{
[Fact]
public void ProductionAutoEntryRequiresTheRuntimePublishedController()
{
string source = ReadSource("Input", "PlayerModeAutoEntry.cs");
Assert.DoesNotContain(
"public bool IsPlayerControllerReady => true;",
source,
StringComparison.Ordinal);
Assert.Contains(
"IsRuntimePublished: true",
source,
StringComparison.Ordinal);
Assert.Contains(
"record.PhysicsHost is EntityPhysicsHost",
source,
StringComparison.Ordinal);
}
private static string ReadSource(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.App",
Path.Combine(relativePath)));
}
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
}
}

View file

@ -12,24 +12,200 @@ namespace AcDream.App.Tests;
/// </summary>
internal static class LiveEntityRuntimeFixture
{
/// <summary>
/// C3c: initial-Create registration now begins the canonical residence
/// lease, whose admission requires a live session generation. Focused
/// App fixtures bind a fixed non-zero token exactly like the Runtime
/// conductor fixtures do.
/// </summary>
private static RuntimeEntityObjectLifetime WithGeneration(
RuntimeEntityObjectLifetime lifetime)
{
lifetime.BindEventContext(
static () => new AcDream.Runtime.RuntimeGenerationToken(1UL),
static () => 1UL);
return lifetime;
}
public static LiveEntityRuntime Create(
GpuWorldState spatial,
ILiveEntityResourceLifecycle resources,
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId)
{
var lifetime = new RuntimeEntityObjectLifetime(firstLocalEntityId);
var lifetime = WithGeneration(
new RuntimeEntityObjectLifetime(firstLocalEntityId));
return new LiveEntityRuntime(spatial, resources, lifetime);
}
/// <summary>
/// C3c: a runtime whose initial-create residences can actually be driven
/// to completion — collision generation committed for
/// <paramref name="landblockId"/>, the production
/// <see cref="AcDream.Runtime.Session.RuntimeFirstEntryDriveController"/>
/// constructed against the lifetime, and an acknowledge-only placement
/// subscription mirroring the host rules (Discard/ExecutorCompleted
/// acknowledged; Place/Withdraw left at the head for the conductors).
/// Tests exercising post-residence legacy update paths register, then
/// call <see cref="DrivenLiveEntityRuntime.Pump"/> to complete the
/// conductors exactly like the composed host's Create-transaction pump.
/// </summary>
internal sealed class DrivenLiveEntityRuntime
{
internal required LiveEntityRuntime Runtime { get; init; }
internal required RuntimeEntityObjectLifetime Lifetime { get; init; }
internal required AcDream.Runtime.Session.RuntimeFirstEntryDriveController
FirstEntry { get; init; }
internal required AcDream.Runtime.Physics
.RuntimePlacementProjectionSubscription Subscription { get; init; }
internal void Pump() => FirstEntry.DriveAll();
}
public static DrivenLiveEntityRuntime CreateDriven(
GpuWorldState spatial,
ILiveEntityResourceLifecycle resources,
uint landblockId = 0x01010000u)
{
var lifetime = WithGeneration(new RuntimeEntityObjectLifetime());
lifetime.Physics.SetPosition.BeginCollisionGeneration(
landblockId & 0xFFFF0000u, 1UL);
lifetime.Physics.Engine.AddLandblock(
landblockId & 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);
lifetime.Physics.SetPosition.CommitCollisionGeneration(
landblockId & 0xFFFF0000u, 1UL, ready: true);
var runtime = new LiveEntityRuntime(spatial, resources, lifetime);
var movement = new AcDream.Runtime.Gameplay
.RuntimeLocalPlayerMovementState();
var identity = new AcDream.Runtime.Gameplay
.RuntimeLocalPlayerIdentityState();
var publication = new AcDream.Runtime.Gameplay
.RuntimeLocalPlayerPhysicsPublicationState(
lifetime.Entities,
lifetime.Physics,
movement,
identity);
movement.AttachPhysicsPublication(publication);
lifetime.LocalPlayerFirstEntry.BindPublication(publication);
var firstEntry = new AcDream.Runtime.Session
.RuntimeFirstEntryDriveController(
lifetime,
new AcDream.Runtime.GameRuntimeClock(),
new SphereCollisionSource(),
static () => AcDream.Runtime.Gameplay
.PlayerMovementConstructionOptions.Fallback,
static _ => new AcDream.Runtime.Gameplay
.RuntimeLocalPlayerPhysicsActivationPreparation(
0.48f,
1.835f,
AcDream.Runtime.Gameplay
.RuntimeLocalPlayerShadowDisposition
.ProvenShapeless));
var subscription = new AcDream.Runtime.Physics
.RuntimePlacementProjectionSubscription(
lifetime.Placements,
static () => new AcDream.Runtime.RuntimeGenerationToken(1UL),
new AckOnlyPlacementSink(runtime));
return new DrivenLiveEntityRuntime
{
Runtime = runtime,
Lifetime = lifetime,
FirstEntry = firstEntry,
Subscription = subscription,
};
}
private sealed class AckOnlyPlacementSink(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);
}
}
private sealed class SphereCollisionSource
: 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 static LiveEntityRuntime Create(
GpuWorldState spatial,
ILiveEntityResourceLifecycle resources,
PhysicsEngine physicsEngine,
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId)
{
var lifetime = new RuntimeEntityObjectLifetime(
var lifetime = WithGeneration(new RuntimeEntityObjectLifetime(
physicsEngine,
firstLocalEntityId);
firstLocalEntityId));
return new LiveEntityRuntime(spatial, resources, lifetime);
}
@ -39,7 +215,8 @@ internal static class LiveEntityRuntimeFixture
Action<LiveEntityRecord> tearDownRuntimeComponents,
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId)
{
var lifetime = new RuntimeEntityObjectLifetime(firstLocalEntityId);
var lifetime = WithGeneration(
new RuntimeEntityObjectLifetime(firstLocalEntityId));
return new LiveEntityRuntime(
spatial,
resources,
@ -53,7 +230,8 @@ internal static class LiveEntityRuntimeFixture
ILiveEntityRuntimeComponentLifecycle runtimeComponentLifecycle,
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId)
{
var lifetime = new RuntimeEntityObjectLifetime(firstLocalEntityId);
var lifetime = WithGeneration(
new RuntimeEntityObjectLifetime(firstLocalEntityId));
return new LiveEntityRuntime(
spatial,
resources,
@ -68,9 +246,9 @@ internal static class LiveEntityRuntimeFixture
PhysicsEngine physicsEngine,
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId)
{
var lifetime = new RuntimeEntityObjectLifetime(
var lifetime = WithGeneration(new RuntimeEntityObjectLifetime(
physicsEngine,
firstLocalEntityId);
firstLocalEntityId));
return new LiveEntityRuntime(
spatial,
resources,

View file

@ -0,0 +1,53 @@
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
namespace AcDream.App.Tests;
/// <summary>
/// C3c: initial-Create registration now freezes the raw create for the
/// canonical residence lease and requires the flattened parser projections
/// to agree with the nested PhysicsDesc block
/// (RuntimeEntityObjectLifetime.HasConsistentCreateIdentityAndParent).
/// Legacy hand-built fixture spawns predate that gate; this helper derives
/// the minimal consistent nested block from the flattened fields.
/// </summary>
internal static class LiveEntitySpawnFixture
{
internal static WorldSession.EntitySpawn WithConsistentPhysics(
this WorldSession.EntitySpawn spawn) => spawn with
{
Physics = new PhysicsSpawnData(
RawState: spawn.PhysicsState ?? 0u,
Position: spawn.Position,
Movement: null,
AnimationFrame: spawn.PlacementId,
SetupTableId: spawn.SetupTableId,
MotionTableId: spawn.MotionTableId,
SoundTableId: null,
PhysicsScriptTableId: null,
Parent: spawn.ParentGuid is { } parentGuid
&& spawn.ParentLocation is { } parentLocation
? new PhysicsAttachment(parentGuid, parentLocation)
: null,
Children: null,
Scale: spawn.ObjScale,
Friction: spawn.Friction,
Elasticity: spawn.Elasticity,
Translucency: null,
Velocity: null,
Acceleration: null,
AngularVelocity: null,
DefaultScriptType: null,
DefaultScriptIntensity: null,
Timestamps: new PhysicsTimestamps(
Position: spawn.PositionSequence,
Movement: spawn.MovementSequence,
State: 0,
Vector: 0,
Teleport: 0,
ServerControlledMove: spawn.ServerControlSequence,
ForcePosition: 0,
ObjDesc: 0,
Instance: spawn.InstanceSequence)),
};
}

View file

@ -0,0 +1,204 @@
using System.Net;
using AcDream.App.Net;
using AcDream.Core.Chat;
using AcDream.Core.Combat;
using AcDream.Core.Items;
using AcDream.Core.Net;
using AcDream.Core.Physics;
using AcDream.Core.Social;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Session;
namespace AcDream.App.Tests.Net;
/// <summary>
/// C3c-F1 (2026-08-02): the movement-stats application seam driven through
/// the REAL inbound chain that crashed the connected lifecycle gate —
/// <c>ClientObjectTable.Ingest</c> → <c>ObjectAdded/ObjectUpdated</c> →
/// <c>LiveSessionEventRouter.RecomputePlayerQualities</c> →
/// <c>OnMovementStatsUpdated</c> → <see cref="LiveMovementStatsApplier"/> →
/// <see cref="RuntimeLocalPlayerMovementState.ApplyCharacterMovementStats"/>.
/// </summary>
public sealed class LiveMovementStatsApplierTests
{
private const uint PlayerGuid = 0x5000000Au;
private sealed class Harness : IDisposable
{
public WorldSession Session { get; }
public LiveSessionEventRouter Router { get; }
public ClientObjectTable Objects { get; } = new();
public RuntimeCharacterState Character { get; } = new();
public RuntimeLocalPlayerMovementState Movement { get; } = new();
public LiveMovementStatsApplier Applier { get; }
public List<string> Log { get; } = [];
public Harness()
{
Session = new WorldSession(new IPEndPoint(IPAddress.Loopback, 9));
// The REAL applier the factory binds (LiveSessionRuntimeFactory
// constructs the same class over the same owner pair) with the
// REAL character-bindings recompute callback shape.
Applier = new LiveMovementStatsApplier(
Movement,
Character.MovementSkills,
Log.Add);
Router = new LiveSessionEventRouter(
Session,
new LiveEntitySessionSink(
_ => { }, _ => { }, _ => { }, _ => { }, _ => { }, _ => { },
_ => { }, _ => { }, _ => { }, _ => { }, _ => { }, _ => { }),
new LiveEnvironmentSessionSink(_ => { }, _ => { }),
new LiveInventorySessionBindings(
Objects,
PlayerGuid: () => PlayerGuid,
OnShortcuts: null,
OnUseDone: null,
ItemMana: new ItemManaState(),
ExternalContainers: new ExternalContainerState()),
new LiveCharacterSessionBindings(
new CombatState(),
Character,
ResolveSkillFormulaBonus: null,
OnSkillsUpdated: (_, _) => Applier.Apply("skills"),
OnConfirmationRequest: null,
OnConfirmationDone: null,
ClientTime: () => 0d,
OnMovementStatsUpdated: () => Applier.Apply("stats")),
new LiveSocialSessionBindings(
new ChatLog(),
new TurbineChatState(),
new FriendsState(),
new SquelchState()));
Router.Attach();
}
/// <summary>
/// Ingests the player's own object row — the exact post-logout
/// inbound-Create edge (<c>ApplyAcceptedSpawn</c> →
/// <c>ClientObjectTable.Ingest</c>) that fired the crashing
/// recompute in logs/connected-world-gate-20260802-122749.
/// </summary>
public void IngestPlayerRow(uint? pwdBitfield = null) =>
Objects.Ingest(new WeenieData(
Guid: PlayerGuid, Name: "+Acdream", Type: ItemType.Creature,
WeenieClassId: 1u, IconId: 0, IconOverlayId: 0,
IconUnderlayId: 0, Effects: 0,
Value: null, StackSize: null, StackSizeMax: null, Burden: null,
ContainerId: null, WielderId: null, ValidLocations: null,
CurrentWieldedLocation: null, Priority: null,
ItemsCapacity: null, ContainersCapacity: null,
Structure: null, MaxStructure: null, Workmanship: null,
PublicWeenieBitfield: pwdBitfield));
public void Dispose()
{
Router.Dispose();
Session.Dispose();
Movement.Dispose();
Character.Dispose();
}
}
private static PlayerMovementController NewDormantRuntimeController()
{
PlayerMovementController controller =
PlayerMovementController.CreatePublicationCandidate(
new PhysicsEngine(),
PlayerMovementConstructionOptions.Fallback);
controller.SealPublicationCandidate();
controller.CommitRuntimeOwnership(new RetailObjectQuantumClock());
return controller;
}
[Fact]
public void PostTeardownIngestRecomputeReportsTypedDropInsteadOfCrashing()
{
using var harness = new Harness();
// The session's authoritative skills were complete before teardown.
harness.Character.MovementSkills.Update(runSkill: 240, jumpSkill: 180);
// Post-teardown transient truth: a retired controller still
// reachable through the displaced recompute callback — the state the
// old direct path crashed on ("A sealed, retired, or discarded
// Runtime movement controller cannot be mutated").
PlayerMovementController controller = NewDormantRuntimeController();
harness.Movement.Controller = controller;
controller.ActivateRuntimePublication();
controller.RetireRuntimePublication();
Assert.Throws<InvalidOperationException>(
() => controller.SetCharacterSkills(1, 1));
// The exact crash edge: a post-logout inbound Create's object-table
// ingest fires the quality recompute through the real router.
harness.IngestPlayerRow();
Assert.Contains(
harness.Log,
line => line.StartsWith(
"player: dropped displaced movement stats",
StringComparison.Ordinal));
Assert.DoesNotContain(
harness.Log,
line => line.StartsWith(
"player: applied server movement",
StringComparison.Ordinal));
}
[Fact]
public void DormantWindowIngestRecomputeAppliesToTheControllerThatGoesLive()
{
using var harness = new Harness();
harness.Character.MovementSkills.Update(runSkill: 240, jumpSkill: 180);
harness.Character.MovementSkills.UpdateStamina(37);
// The committed-but-unactivated first-entry window (activation
// deferred on cell streaming) with inbound ingests still pumping.
PlayerMovementController controller = NewDormantRuntimeController();
harness.Movement.Controller = controller;
Assert.True(controller.IsRuntimeOwnedDormant);
// BF_PLAYER (0x8) + BF_PLAYER_KILLER (0x20) on the player's own row
// exercises the full stat set through the recompute.
harness.IngestPlayerRow(pwdBitfield: 0x28u);
Assert.Contains(
harness.Log,
line => line.StartsWith(
"player: applied server movement stats",
StringComparison.Ordinal));
// The same instance goes live at activation with the values already
// current.
controller.ActivateRuntimePublication();
IWeenieObject weenie = controller.Motion.WeenieObj!;
Assert.True(weenie.InqRunRate(out float runRate));
Assert.True(runRate > 0f);
Assert.Equal(
ObjectInfoState.IsPK,
controller.OwnPvpFlags & ObjectInfoState.IsPK);
}
[Fact]
public void AbsentControllerAndIncompleteSnapshotStaySilent()
{
using var harness = new Harness();
// Incomplete snapshot (no PlayerDescription yet) with no controller:
// byte-identical to the pre-F1 silent skip — no log line at all.
harness.IngestPlayerRow();
Assert.DoesNotContain(
harness.Log,
line => line.StartsWith("player:", StringComparison.Ordinal));
// Complete snapshot but still no controller (pre-first-entry):
// still the silent skip.
harness.Character.MovementSkills.Update(runSkill: 240, jumpSkill: 180);
Assert.Equal(
RuntimeMovementStatsApplication.DroppedNoController,
harness.Applier.Apply("stats"));
Assert.DoesNotContain(
harness.Log,
line => line.StartsWith("player:", StringComparison.Ordinal));
}
}

View file

@ -257,7 +257,12 @@ public sealed class LiveSessionResetPlanTests
spatial,
new FailingOnceResources(),
runtime.EntityObjects);
live.RegisterLiveEntity(Spawn(player, 1, 1, 0x01010001u));
// C3c: this GameRuntime has no session, so its generation is zero and
// residence-based registration (RegisterLiveEntity) correctly refuses
// an initial world Create. The subject here is reset/teardown retry,
// not the create flow — seed the entity through the legacy direct
// Runtime registration, which needs no residence lease.
runtime.EntityObjects.RegisterEntity(Spawn(player, 1, 1, 0x01010001u));
live.MaterializeLiveEntity(
player,
0x01010001u,

View file

@ -0,0 +1,53 @@
using System.Text.RegularExpressions;
namespace AcDream.App.Tests.Physics;
/// <summary>
/// C3c-F1 (2026-08-02): source pins for the App-side halves of the
/// movement-owner application seams whose production graphs have no
/// focused harness (the inbound network-update controller's dependency
/// set is composition-only). The Runtime lifecycle matrices carry the
/// behavioral coverage; these pins keep the App wiring routed through the
/// owner's typed entries instead of the throwing direct mutations that
/// crashed the connected lifecycle gate twice
/// (logs/connected-world-gate-20260802-122749 — SetCharacterSkills;
/// logs/connected-world-gate-20260802-125907 — ApplyPhysicsState).
/// </summary>
public sealed class C3cF1ProductionWiringTests
{
[Fact]
public void LocalPlayerInboundSetState_RoutesThroughTheTypedOwnerEntry()
{
string source = ReadSource(
"Physics",
"LiveEntityNetworkUpdateController.cs");
Assert.Single(
Regex.Matches(source, @"ApplyServerPhysicsState\(")
.Cast<Match>());
Assert.DoesNotContain(
".ApplyPhysicsState(",
source,
StringComparison.Ordinal);
}
private static string ReadSource(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.App",
Path.Combine(relativePath)));
}
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
}
}

View file

@ -7,20 +7,40 @@ public sealed class Issue270ProductionWiringTests
[Fact]
public void MovementStats_UseOneEdgeTrackerAndResetItWithTheSession()
{
string source = ReadSource("Net", "LiveSessionRuntimeFactory.cs");
// C3c-F1 (2026-08-02): the #270 invariant is unchanged — exactly one
// stamina-exhaustion edge tracker, exhaustion dispatched once on the
// edge, tracker reset with the session — but the wiring moved from
// the factory's deleted ApplyMovementStats body into
// LiveMovementStatsApplier, which routes through the Runtime
// movement owner's typed seam instead of touching the controller.
string applier = ReadSource("Net", "LiveMovementStatsApplier.cs");
string factory = ReadSource("Net", "LiveSessionRuntimeFactory.cs");
Assert.Contains(
"_staminaExhaustion.Observe(snapshot.CurrentStamina)",
source,
applier,
StringComparison.Ordinal);
Assert.Single(
Regex.Matches(
source,
@"controller!\.Motion\.ReportExhaustion\(\);")
applier,
@"_movement\.ReportExhaustion\(\);")
.Cast<Match>());
Assert.Contains(
"_staminaExhaustion.Reset();",
source,
applier,
StringComparison.Ordinal);
Assert.Contains(
"_movementStats.Reset();",
factory,
StringComparison.Ordinal);
// The factory keeps zero direct controller mutations: both stat
// callbacks route through the applier's seam.
Assert.Equal(
2,
Regex.Matches(factory, @"_movementStats\.Apply\(").Count);
Assert.DoesNotContain(
"Motion.ReportExhaustion",
factory,
StringComparison.Ordinal);
}
@ -38,8 +58,11 @@ public sealed class Issue270ProductionWiringTests
Assert.Equal(
3,
Regex.Matches(source, @"SeedRemoteSpawnPlacement\(").Count);
// C3c-F5: the settle helper moved to Core (SpawnPlacementSettler) so
// the local player's Runtime first-entry activation shares the same
// tested compressed-first-gravity-frame sweep.
Assert.Contains(
"RemoteSpawnPlacementSettler.TrySettle(",
"SpawnPlacementSettler.TrySettle(",
source,
StringComparison.Ordinal);
}

View file

@ -221,7 +221,12 @@ public sealed class LiveEntityInboundAuthorityGateTests
out AcceptedPositionNetworkUpdate accepted));
Assert.Same(registration.Canonical, accepted.Canonical);
Assert.Equal((ulong)2, accepted.PositionAuthorityVersion);
// C3c: the accepted Position of an entity whose initial-create
// residence is still pending is admitted into the residence FIFO —
// its merge (and the position-authority advance) commits at the
// executor drain, so the gate's captured authority is the
// admission-time version, not a post-apply bump.
Assert.Equal((ulong)1, accepted.PositionAuthorityVersion);
Assert.Equal(1, publishCount);
Assert.False(runtime.TryGetRecord(Guid, out _));
}

View file

@ -1112,7 +1112,10 @@ public sealed class RemotePhysicsUpdaterTests
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: serverPosition,

View file

@ -1,109 +0,0 @@
using System.Numerics;
using AcDream.App.Physics;
using AcDream.Core.Physics;
namespace AcDream.App.Tests.Physics;
public sealed class RemoteSpawnPlacementSettlerTests
{
private const uint Landblock = 0xA9B40000u;
private const uint Cell = Landblock | 0x0001u;
private const float Radius = 0.48f;
private const float Height = 1.835f;
[Fact]
public void FloorWithinFirstGravityFrame_CommitsGroundContactOnce()
{
PhysicsEngine engine = BuildFlatEngine();
PhysicsBody body = AirborneBody(new Vector3(12f, 12f, 0.25f));
int hitGround = 0;
int leaveGround = 0;
bool settled = RemoteSpawnPlacementSettler.TrySettle(
engine,
body,
body.Position,
Cell,
Radius,
Height,
ObjectInfoState.EdgeSlide,
movingEntityId: 0x70000001u,
() => hitGround++,
() => leaveGround++);
Assert.True(settled);
Assert.True(body.InContact);
Assert.True(body.OnWalkable);
Assert.Equal(1, hitGround);
Assert.Equal(0, leaveGround);
}
[Fact]
public void MissingCell_CanRetryAfterHydrationWithoutReconstructingBody()
{
var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() };
PhysicsBody body = AirborneBody(new Vector3(12f, 12f, 0.25f));
Vector3 originalPosition = body.Position;
Assert.False(TrySettle(engine, body));
Assert.False(body.InContact);
Assert.Equal(originalPosition, body.Position);
AddFlatLandblock(engine);
Assert.True(TrySettle(engine, body));
Assert.True(body.InContact);
Assert.True(body.OnWalkable);
}
[Fact]
public void FloorOutsideSettleDistance_LeavesGenuinelyAirborneBodyUnchanged()
{
PhysicsEngine engine = BuildFlatEngine();
PhysicsBody body = AirborneBody(new Vector3(12f, 12f, 1.25f));
Vector3 originalPosition = body.Position;
Assert.False(TrySettle(engine, body));
Assert.False(body.InContact);
Assert.False(body.OnWalkable);
Assert.Equal(originalPosition, body.Position);
}
private static bool TrySettle(PhysicsEngine engine, PhysicsBody body) =>
RemoteSpawnPlacementSettler.TrySettle(
engine,
body,
body.Position,
Cell,
Radius,
Height,
ObjectInfoState.EdgeSlide,
movingEntityId: 0x70000001u,
static () => { },
static () => { });
private static PhysicsBody AirborneBody(Vector3 position) => new()
{
Position = position,
Orientation = Quaternion.Identity,
State = PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions,
TransientState = TransientStateFlags.None,
};
private static PhysicsEngine BuildFlatEngine()
{
var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() };
AddFlatLandblock(engine);
return engine;
}
private static void AddFlatLandblock(PhysicsEngine engine) =>
engine.AddLandblock(
Landblock,
new TerrainSurface(new byte[81], new float[256]),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
worldOffsetX: 0f,
worldOffsetY: 0f);
}

View file

@ -466,6 +466,7 @@ public sealed class EquippedChildProjectionWithdrawalTests
ChildInstanceSequence: 1,
ChildPositionSequence: 1);
fixture.CompleteFirstEntry();
Assert.True(fixture.Live.TryApplyCreateParent(update, out _));
fixture.Controller.OnCreateParentAccepted(update);
@ -509,13 +510,20 @@ public sealed class EquippedChildProjectionWithdrawalTests
LiveEntityRecord parent = fixture.Spawn(0x70000270u, generation: 1);
WorldSession.EntitySpawn childSpawn = ControllerFixture.SpawnData(
0x70000271u,
generation: 1) with
generation: 1);
childSpawn = childSpawn with
{
Position = null,
ParentGuid = parent.ServerGuid,
ParentLocation = 0,
PlacementId = null,
PositionSequence = 0,
Physics = childSpawn.Physics!.Value with
{
Position = null,
Parent = new PhysicsAttachment(parent.ServerGuid, 0u),
AnimationFrame = null,
},
};
fixture.Live.RegisterLiveEntity(childSpawn);
fixture.Controller.OnSpawn(childSpawn);
@ -558,13 +566,20 @@ public sealed class EquippedChildProjectionWithdrawalTests
LiveEntityRecord parent = fixture.Spawn(0x70000272u, generation: 1);
WorldSession.EntitySpawn childSpawn = ControllerFixture.SpawnData(
0x70000273u,
generation: 1) with
generation: 1);
childSpawn = childSpawn with
{
Position = null,
ParentGuid = parent.ServerGuid,
ParentLocation = 0,
PlacementId = 0,
PositionSequence = 0,
Physics = childSpawn.Physics!.Value with
{
Position = null,
Parent = new PhysicsAttachment(parent.ServerGuid, 0u),
AnimationFrame = 0,
},
};
fixture.Live.RegisterLiveEntity(childSpawn);
fixture.Controller.OnSpawn(childSpawn);
@ -577,13 +592,20 @@ public sealed class EquippedChildProjectionWithdrawalTests
Assert.Null(entity.PaletteOverride);
WorldSession.EntitySpawn grandchildSpawn = ControllerFixture.SpawnData(
0x70000274u,
generation: 1) with
generation: 1);
grandchildSpawn = grandchildSpawn with
{
Position = null,
ParentGuid = child.ServerGuid,
ParentLocation = 0,
PlacementId = 0,
PositionSequence = 0,
Physics = grandchildSpawn.Physics!.Value with
{
Position = null,
Parent = new PhysicsAttachment(child.ServerGuid, 0u),
AnimationFrame = 0,
},
};
fixture.Live.RegisterLiveEntity(grandchildSpawn);
fixture.Controller.OnSpawn(grandchildSpawn);
@ -734,6 +756,8 @@ public sealed class EquippedChildProjectionWithdrawalTests
Position = null,
PositionSequence = 1,
PlacementId = 0,
ParentGuid = parent.ServerGuid,
ParentLocation = 0,
Physics = physics,
};
@ -1192,6 +1216,50 @@ public sealed class EquippedChildProjectionWithdrawalTests
private const uint Cell = 0x01010001u;
private readonly DeferredLiveEntityRuntimeComponentLifecycle _lifecycle = new();
private readonly Setup _setup;
private readonly AcDream.Runtime.Session.RuntimeFirstEntryDriveController _firstEntry;
private sealed class NullCollisionSource
: 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>.Missing;
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()
{
}
}
internal ControllerFixture(
Func<LiveEntityRecord, ulong, ulong, ExactProjectionWithdrawalOutcome>
@ -1201,10 +1269,24 @@ public sealed class EquippedChildProjectionWithdrawalTests
(Cell & 0xFFFF0000u) | 0xFFFFu,
new LandBlock(),
Array.Empty<WorldEntity>()));
Live = LiveEntityRuntimeFixture.Create(
EntityObjects = new RuntimeEntityObjectLifetime();
EntityObjects.BindEventContext(
static () => new AcDream.Runtime.RuntimeGenerationToken(1UL),
static () => 1UL);
_firstEntry = new AcDream.Runtime.Session.RuntimeFirstEntryDriveController(
EntityObjects,
new AcDream.Runtime.GameRuntimeClock(),
new NullCollisionSource(),
() => AcDream.Runtime.Gameplay.PlayerMovementConstructionOptions.Fallback,
static _ => new AcDream.Runtime.Gameplay.RuntimeLocalPlayerPhysicsActivationPreparation(
0.48f,
1.835f,
AcDream.Runtime.Gameplay.RuntimeLocalPlayerShadowDisposition.ProvenShapeless));
Live = new LiveEntityRuntime(
Spatial,
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }),
_lifecycle);
_lifecycle,
EntityObjects);
_setup = new Setup
{
HoldingLocations =
@ -1231,6 +1313,21 @@ public sealed class EquippedChildProjectionWithdrawalTests
}
internal GpuWorldState Spatial { get; } = new();
internal RuntimeEntityObjectLifetime EntityObjects { get; }
/// <summary>
/// C3c: completes fresh residences through the first-entry
/// conductors (a celless route performs no SetPosition, so its drain
/// completes synchronously), releasing the lease so legacy direct
/// CreateParent/Parent application stays valid post-flip. The
/// controller is constructed lazily but binds no notification of its
/// own tracking value until first use, so it sweeps active leases
/// directly instead.
/// </summary>
internal void CompleteFirstEntry()
{
_firstEntry.DriveAll();
}
internal ClientObjectTable Objects { get; } = new();
internal EntityEffectPoseRegistry Poses { get; } = new();
internal LiveEntityRuntime Live { get; }
@ -1254,7 +1351,13 @@ public sealed class EquippedChildProjectionWithdrawalTests
{
WorldSession.EntitySpawn spawn = SpawnData(guid, generation);
if (!hasPosition)
spawn = spawn with { Position = null };
{
spawn = spawn with
{
Position = null,
Physics = spawn.Physics!.Value with { Position = null },
};
}
if (!hasSetup)
spawn = spawn with { SetupTableId = null };
return Assert.IsType<RuntimeEntityRecord>(
@ -1288,21 +1391,59 @@ public sealed class EquippedChildProjectionWithdrawalTests
return record;
}
internal static WorldSession.EntitySpawn SpawnData(uint guid, ushort generation) => new(
guid,
new CreateObject.ServerPosition(
Cell, 0f, 0f, 0f, 1f, 0f, 0f, 0f),
0x02000001u,
Array.Empty<CreateObject.AnimPartChange>(),
Array.Empty<CreateObject.TextureChange>(),
Array.Empty<CreateObject.SubPaletteSwap>(),
BasePaletteId: null,
ObjScale: null,
Name: "attached fixture",
ItemType: null,
MotionState: null,
MotionTableId: null,
InstanceSequence: generation);
internal static WorldSession.EntitySpawn SpawnData(uint guid, ushort generation)
{
// C3c: residence admission freezes the raw create and requires
// the flattened parser projections to agree with the nested
// PhysicsDesc block (HasConsistentCreateIdentityAndParent).
var position = new CreateObject.ServerPosition(
Cell, 0f, 0f, 0f, 1f, 0f, 0f, 0f);
var physics = new PhysicsSpawnData(
RawState: 0u,
Position: position,
Movement: null,
AnimationFrame: null,
SetupTableId: 0x02000001u,
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: generation));
return new WorldSession.EntitySpawn(
guid,
position,
0x02000001u,
Array.Empty<CreateObject.AnimPartChange>(),
Array.Empty<CreateObject.TextureChange>(),
Array.Empty<CreateObject.SubPaletteSwap>(),
BasePaletteId: null,
ObjScale: null,
Name: "attached fixture",
ItemType: null,
MotionState: null,
MotionTableId: null,
InstanceSequence: generation,
Physics: physics);
}
internal void InstallAttached(
LiveEntityRecord parent,

View file

@ -33,7 +33,7 @@ public sealed class LiveAppearanceAnimationTests
ItemType: null,
MotionState: null,
MotionTableId: null,
InstanceSequence: 1);
InstanceSequence: 1).WithConsistentPhysics();
LiveEntityRecord record = runtime.RegisterAndMaterializeProjection(
spawn,
id => Entity(id, 0x01000001u, guid));
@ -175,7 +175,7 @@ public sealed class LiveAppearanceAnimationTests
Assert.Equal(1, registry.RetainedRegistrationCount);
}
private static WorldSession.EntitySpawn Spawn(uint guid, uint cell, ushort instance) => new(
private static WorldSession.EntitySpawn Spawn(uint guid, uint cell, ushort instance) => new WorldSession.EntitySpawn(
guid,
new CreateObject.ServerPosition(cell, 1f, 1f, 1f, 1f, 0f, 0f, 0f),
0x02000010u,
@ -188,7 +188,7 @@ public sealed class LiveAppearanceAnimationTests
ItemType: null,
MotionState: null,
MotionTableId: null,
InstanceSequence: instance);
InstanceSequence: instance).WithConsistentPhysics();
private static WorldEntity Entity(
uint id,

View file

@ -74,7 +74,22 @@ public sealed class LiveEntityCreateSupersessionRecoveryTests
publishAppearance: _ =>
{
operations.Add("appearance");
runtime.RegisterLiveEntity(Spawn() with { Name = "fresher" });
// C3c: a fresher same-generation CreateObject no longer
// advances create authority at registration — while a
// residence is active it is FIFO-staged and its authority
// advance lands at the executor drain's WeenieDescription
// stage (AdvanceCreateAuthority). 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.
record.Canonical.AdvanceCreateAuthority();
return true;
},
publishCurrentSnapshot: _ => operations.Add("current"),
@ -315,7 +330,7 @@ public sealed class LiveEntityCreateSupersessionRecoveryTests
return record;
}
private static WorldSession.EntitySpawn Spawn() => new(
private static WorldSession.EntitySpawn Spawn() => new WorldSession.EntitySpawn(
Guid,
new CreateObject.ServerPosition(
Cell,
@ -336,7 +351,7 @@ public sealed class LiveEntityCreateSupersessionRecoveryTests
ItemType: null,
MotionState: null,
MotionTableId: null,
InstanceSequence: 1);
InstanceSequence: 1).WithConsistentPhysics();
private static LiveEntityAnimationState AnimationState(
WorldEntity entity,

View file

@ -232,7 +232,7 @@ public sealed class LiveEntityLightControllerTests
InstanceSequence: 1,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1);
PositionSequence: 1).WithConsistentPhysics();
}
}

View file

@ -278,6 +278,15 @@ public sealed class CurrentGameRuntimeAdapterTests
[Fact]
public void DirectAndGraphicalHosts_ProduceIdenticalEntityObjectTrace()
{
// C3c (clause 2 — undriven-residence semantics): an initial world
// Create now begins the canonical initial-create residence, whose
// admission requires a live session generation. Both hosts here
// deliberately carry the zero generation of a session-less runtime,
// so BOTH refuse the Create transactionally with the identical
// structural error and publish no entity-object trace at all. The
// full driven-flow direct-vs-graphical parity (residence ->
// conductor -> receipts) is covered by the C3c first-entry
// integration tests.
WorldSession.EntitySpawn spawn =
Spawn(Harness.TargetGuid, instance: 7, cell: 0x12340001u);
var direct = new RuntimeEntityObjectLifetime();
@ -286,148 +295,28 @@ public sealed class CurrentGameRuntimeAdapterTests
using IDisposable directSubscription =
direct.Events.Subscribe(directTrace);
RuntimeEntityRegistrationResult directRegistration =
direct.RegisterEntity(spawn);
RuntimeEntityRecord directCanonical =
Assert.IsType<RuntimeEntityRecord>(directRegistration.Canonical);
direct.Objects.AddOrUpdate(Object(spawn));
direct.Objects.MoveItem(
spawn.Guid,
Harness.PlayerGuid,
newSlot: 3);
var objDesc = new ObjDescEvent.Parsed(
spawn.Guid,
new CreateObject.ModelData(
0x04000001u,
Array.Empty<CreateObject.SubPaletteSwap>(),
Array.Empty<CreateObject.TextureChange>(),
Array.Empty<CreateObject.AnimPartChange>()),
spawn.InstanceSequence,
ObjDescSequence: 2);
Assert.True(direct.TryApplyObjDesc(
objDesc,
acknowledgeProjection: null,
out _));
var motion = new WorldSession.EntityMotionUpdate(
spawn.Guid,
new CreateObject.ServerMotionState(0x3D, 0x11),
spawn.InstanceSequence,
MovementSequence: 2,
ServerControlSequence: 2,
IsAutonomous: false);
Assert.True(direct.TryApplyMotion(
motion,
retainPayload: true,
acknowledgeProjection: null,
out _,
out _));
var vector = new VectorUpdate.Parsed(
spawn.Guid,
new Vector3(1f, 2f, 3f),
new Vector3(0f, 0f, 0.25f),
spawn.InstanceSequence,
VectorSequence: 2);
Assert.True(direct.TryApplyVector(
vector,
acknowledgeProjection: null,
out _));
var state = new SetState.Parsed(
spawn.Guid,
(uint)(PhysicsStateFlags.ReportCollisions
| PhysicsStateFlags.Hidden),
spawn.InstanceSequence,
StateSequence: 2);
Assert.True(direct.TryApplyState(
state,
acknowledgeProjection: null,
out _,
out _));
Assert.True(direct.CommitChildNoDraw(
directCanonical,
noDraw: true));
Assert.True(direct.CommitChildNoDraw(
directCanonical,
noDraw: false));
WorldSession.EntityPositionUpdate position =
Position(spawn.Guid, spawn.InstanceSequence);
Assert.True(direct.TryApplyPosition(
position,
isLocalPlayer: false,
forcePositionRotation: null,
currentLocalVelocity: null,
projectionRequiresTeleportHook: false,
acknowledgeProjection: null,
out _,
out _,
out _));
Assert.True(direct.CommitRebucket(
directCanonical,
0x12360001u,
0x1236FFFFu));
Assert.True(direct.TryApplyPickup(
new PickupEvent.Parsed(
spawn.Guid,
spawn.InstanceSequence,
PositionSequence: 3),
acknowledgeProjection: null,
out _));
Assert.True(direct.TryAcceptDelete(
new DeleteObject.Parsed(spawn.Guid, spawn.InstanceSequence),
isLocalPlayer: false,
removeRetainedObject: true,
out RuntimeEntityDeleteAcceptance directDelete));
direct.CompleteAcceptedDelete(directDelete);
Assert.Null(direct.RetireCanonicalOnly(directCanonical));
InvalidOperationException directRefusal =
Assert.Throws<InvalidOperationException>(() =>
direct.RegisterEntityWithInitialResidence(
spawn,
isLocalPlayer: false));
using var graphical = new Harness();
var graphicalTrace = new EntityObjectTrace();
using IDisposable graphicalSubscription =
graphical.EntityObjects.Events.Subscribe(graphicalTrace);
_ = graphical.Entities.RegisterAndMaterializeProjection(spawn);
graphical.Objects.AddOrUpdate(Object(spawn));
graphical.Objects.MoveItem(
spawn.Guid,
Harness.PlayerGuid,
newSlot: 3);
Assert.True(graphical.Entities.TryApplyObjDesc(
objDesc,
out _));
Assert.True(graphical.Entities.TryApplyMotion(
motion,
retainPayload: true,
out _,
out _));
Assert.True(graphical.Entities.TryApplyVector(vector, out _));
Assert.True(graphical.Entities.TryApplyState(state, out _, out _));
Assert.True(graphical.Entities.SetAttachedChildNoDraw(
spawn.Guid,
noDraw: true));
Assert.True(graphical.Entities.SetAttachedChildNoDraw(
spawn.Guid,
noDraw: false));
Assert.True(graphical.Entities.TryApplyPosition(
position,
isLocalPlayer: false,
forcePositionRotation: null,
currentLocalVelocity: null,
out _,
out _,
out _));
Assert.True(graphical.Entities.RebucketLiveEntity(
spawn.Guid,
0x12360001u));
Assert.True(graphical.Entities.TryApplyPickup(
new PickupEvent.Parsed(
spawn.Guid,
spawn.InstanceSequence,
PositionSequence: 3),
out _));
Assert.True(graphical.Entities.UnregisterLiveEntity(
new DeleteObject.Parsed(spawn.Guid, spawn.InstanceSequence),
isLocalPlayer: false,
removeRetainedObject: true));
InvalidOperationException graphicalRefusal =
Assert.Throws<InvalidOperationException>(() =>
graphical.Entities.RegisterAndMaterializeProjection(spawn));
Assert.Contains(
"cannot acquire a structurally valid initial residence lease",
directRefusal.Message,
StringComparison.Ordinal);
Assert.Equal(directRefusal.Message, graphicalRefusal.Message);
Assert.Equal(directTrace.Entries, graphicalTrace.Entries);
Assert.Empty(directTrace.Entries);
Assert.Equal(0, direct.Entities.Count);
Assert.Equal(0, direct.Objects.ObjectCount);
Assert.Equal(0, graphical.EntityObjects.Entities.Count);
@ -438,6 +327,11 @@ public sealed class CurrentGameRuntimeAdapterTests
public void GraphicalObserverFailure_DoesNotStarveLaterObserverOrOwner()
{
using var harness = new Harness();
// C3c: registration begins the canonical initial-create residence,
// whose admission requires a live session generation.
Assert.Equal(
RuntimeSessionStartStatus.Connected,
harness.Runtime.Session.Start(harness.Runtime.Generation).Status);
var throwing = new ThrowingEntityObserver();
var recording = new RuntimeTraceRecorder();
IDisposable first = harness.Runtime.Subscribe(throwing);

View file

@ -599,7 +599,7 @@ public sealed class LocalPlayerTeleportControllerTests
new Vector3(x, y, z),
Quaternion.Identity));
private static WorldSession.EntitySpawn Spawn(uint guid, uint cell) => new(
private static WorldSession.EntitySpawn Spawn(uint guid, uint cell) => new WorldSession.EntitySpawn(
Guid: guid,
Position: new CreateObject.ServerPosition(
cell,
@ -619,7 +619,7 @@ public sealed class LocalPlayerTeleportControllerTests
Name: "player",
ItemType: null,
MotionState: null,
MotionTableId: null);
MotionTableId: null).WithConsistentPhysics();
private sealed class NullResources : ILiveEntityResourceLifecycle
{

View file

@ -544,7 +544,7 @@ public sealed class StreamingFrameControllerTests
InstanceSequence: 1,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1);
PositionSequence: 1).WithConsistentPhysics();
}
private static IReadOnlyList<LandblockStreamResult> Drain(

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);