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

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

View file

@ -56,6 +56,29 @@ public class ShadowObjectRegistryTests
Assert.Equal(1, reg.TotalRegistered);
}
[Fact]
public void Register_CornerLandblock_DerivesRealOutdoorSeed()
{
// C3c-F3: landblock (0,0) — id 0x0000FFFF — has prefix 0x00000000.
// DeriveOutdoorSeed's prefix-0 "absent" sentinel used to reject it,
// silently dropping every landblock-baked static in the map-corner
// block. Local (12,12) = cell (0,0) = cellId 0x00000000 | 1.
var reg = new ShadowObjectRegistry();
reg.Register(1u, 0x01000001u, new Vector3(12f, 12f, 50f), Quaternion.Identity, 1f, OffX, OffY, 0x0000FFFFu);
Assert.Equal(1, reg.TotalRegistered);
Assert.Contains(reg.GetObjectsInCell(0x00000001u), e => e.EntityId == 1u);
}
[Fact]
public void Register_AbsentLandblockId_StillKeepsWhenEmpty()
{
// The genuine "no landblock" input (id 0) must keep the
// keep-when-empty behavior (retail pc:283540) the sentinel provided.
var reg = new ShadowObjectRegistry();
reg.Register(1u, 0x01000001u, new Vector3(12f, 12f, 50f), Quaternion.Identity, 1f, OffX, OffY, 0u);
Assert.Equal(0, reg.TotalRegistered);
}
// -----------------------------------------------------------------------
// GetObjectsInCell
// -----------------------------------------------------------------------

View file

@ -1,10 +1,15 @@
using System.Numerics;
using AcDream.App.Physics;
using AcDream.Core.Physics;
namespace AcDream.App.Tests.Physics;
namespace AcDream.Core.Tests.Physics;
public sealed class RemoteSpawnPlacementSettlerTests
/// <summary>
/// C3c-F5: moved from tests/AcDream.App.Tests/Physics/
/// RemoteSpawnPlacementSettlerTests.cs when the #270 settler moved to Core
/// (App -> <see cref="SpawnPlacementSettler"/>) so the local player's
/// Runtime first-entry activation can share it. Test bodies unchanged.
/// </summary>
public sealed class SpawnPlacementSettlerTests
{
private const uint Landblock = 0xA9B40000u;
private const uint Cell = Landblock | 0x0001u;
@ -19,7 +24,7 @@ public sealed class RemoteSpawnPlacementSettlerTests
int hitGround = 0;
int leaveGround = 0;
bool settled = RemoteSpawnPlacementSettler.TrySettle(
bool settled = SpawnPlacementSettler.TrySettle(
engine,
body,
body.Position,
@ -71,7 +76,7 @@ public sealed class RemoteSpawnPlacementSettlerTests
}
private static bool TrySettle(PhysicsEngine engine, PhysicsBody body) =>
RemoteSpawnPlacementSettler.TrySettle(
SpawnPlacementSettler.TrySettle(
engine,
body,
body.Position,

View file

@ -238,11 +238,25 @@ public sealed class HeadlessSessionHostTests
new HeadlessDiagnosticWriter(TextWriter.Null),
operations);
GameRuntime runtime = host.Runtime;
// C3c: conductor-driven flow — a live generation admits the initial
// residence, the flat landblock's collision generation commits, and
// the world projection's pump drives the local first-entry conductor
// to completion (the deleted SynchronizeLocalPlayer hand-copy's
// replacement).
Assert.Equal(
RuntimeSessionStartStatus.Connected,
host.Start().Status);
const uint player = 0x50000002u;
runtime.PlayerIdentity.ServerGuid = player;
runtime.EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
0xA9B40000u, 1UL);
AddFlatLandblock(runtime.EntityObjects.Physics.Engine);
runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
0xA9B40000u, 1UL, ready: true);
AcDream.Runtime.Session.RuntimeFirstEntryDriveController firstEntry =
CreateFirstEntryDrive(runtime);
RuntimeEntityRecord record = runtime.EntityObjects
.RegisterEntity(Spawn(player))
.RegisterEntityWithInitialResidence(Spawn(player), isLocalPlayer: true)
.Canonical!;
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
record,
@ -252,7 +266,8 @@ public sealed class HeadlessSessionHostTests
var collision = new FixtureCollisionNeighborhood();
var projection = new HeadlessSessionWorldProjection(
runtime,
collision);
collision,
firstEntry);
projection.ProjectSpawn(record, isLocalPlayer: true);
PlayerMovementController controller =
@ -310,11 +325,25 @@ public sealed class HeadlessSessionHostTests
new HeadlessDiagnosticWriter(TextWriter.Null),
operations);
GameRuntime runtime = host.Runtime;
// C3c: conductor-driven flow — a live generation admits the initial
// residence, the flat landblock's collision generation commits, and
// the world projection's pump drives the local first-entry conductor
// to completion (the deleted SynchronizeLocalPlayer hand-copy's
// replacement).
Assert.Equal(
RuntimeSessionStartStatus.Connected,
host.Start().Status);
const uint player = 0x50000003u;
runtime.PlayerIdentity.ServerGuid = player;
runtime.EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
0xA9B40000u, 1UL);
AddFlatLandblock(runtime.EntityObjects.Physics.Engine);
runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
0xA9B40000u, 1UL, ready: true);
AcDream.Runtime.Session.RuntimeFirstEntryDriveController firstEntry =
CreateFirstEntryDrive(runtime);
RuntimeEntityRecord record = runtime.EntityObjects
.RegisterEntity(Spawn(player))
.RegisterEntityWithInitialResidence(Spawn(player), isLocalPlayer: true)
.Canonical!;
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
record,
@ -324,7 +353,8 @@ public sealed class HeadlessSessionHostTests
var collision = new FixtureCollisionNeighborhood();
var projection = new HeadlessSessionWorldProjection(
runtime,
collision);
collision,
firstEntry);
projection.ProjectSpawn(record, isLocalPlayer: true);
PlayerMovementController controller =
Assert.IsType<PlayerMovementController>(
@ -376,6 +406,104 @@ public sealed class HeadlessSessionHostTests
Assert.Equal(2, collision.CenterCount);
}
/// <summary>
/// C3c-R1 review F7: a remote Create whose landblock lies outside the
/// bounded collision neighborhood's service window can never see its
/// deferred placement's collision-generation wake — left alone it would
/// pin its residence (and the drive's pending entry) for the whole
/// session. The host converts it to the celless completion route before
/// the pump: the residence completes with FullCell 0, the accepted wire
/// frame stays on the canonical snapshot (the exact pre-flip
/// accepted-frame behavior for far remotes), and every ledger converges.
/// </summary>
[Fact]
public void FarRemoteCreateCompletesCelllessWithoutPinningItsResidence()
{
var operations = new FixtureSessionOperations();
using var credential = new HeadlessCredentialSecret(
"fixture",
"password");
using var host = new HeadlessSessionHost(
Descriptor(),
credential,
new HeadlessDiagnosticWriter(TextWriter.Null),
operations);
GameRuntime runtime = host.Runtime;
Assert.Equal(
RuntimeSessionStartStatus.Connected,
host.Start().Status);
const uint player = 0x5000000Bu;
runtime.PlayerIdentity.ServerGuid = player;
runtime.EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
0xA9B40000u, 1UL);
AddFlatLandblock(runtime.EntityObjects.Physics.Engine);
runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
0xA9B40000u, 1UL, ready: true);
AcDream.Runtime.Session.RuntimeFirstEntryDriveController firstEntry =
CreateFirstEntryDrive(runtime);
var collision = new FixtureCollisionNeighborhood();
var projection = new HeadlessSessionWorldProjection(
runtime,
collision,
firstEntry);
RuntimeEntityRecord playerRecord = runtime.EntityObjects
.RegisterEntityWithInitialResidence(Spawn(player), isLocalPlayer: true)
.Canonical!;
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
playerRecord,
playerRecord.CreateIntegrationVersion,
playerRecord.Snapshot,
replaceGeneration: false));
projection.ProjectSpawn(playerRecord, isLocalPlayer: true);
const uint farRemote = 0x70000010u;
const uint farCell = 0x00010001u;
Assert.False(collision.IsWithinServiceWindow(farCell));
RuntimeEntityRecord remote = runtime.EntityObjects
.RegisterEntityWithInitialResidence(
Spawn(farRemote, farCell),
isLocalPlayer: false)
.Canonical!;
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
remote,
remote.CreateIntegrationVersion,
remote.Snapshot,
replaceGeneration: false));
projection.ProjectSpawn(remote, isLocalPlayer: false);
// Celless completion: no pinned residence, FullCell stays 0, the
// accepted wire frame survives on the canonical snapshot.
Assert.False(runtime.EntityObjects.TryGetInitialCreateResidence(
remote,
out _));
Assert.Equal(0u, remote.FullCellId);
Assert.Equal(
farCell,
remote.Snapshot.Position!.Value.LandblockId);
RuntimeEntityObjectOwnershipSnapshot ownership =
runtime.EntityObjects.CaptureOwnership();
Assert.Equal(0, ownership.InitialCreateResidenceLeaseCount);
Assert.Equal(0, ownership.FirstEntryDrivePendingCount);
Assert.Equal(0, firstEntry.PendingCount);
// Draining the placement FIFO the way the host subscription would
// converges the completion-receipt ledger too.
while (runtime.EntityObjects.Physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot head))
{
if (!runtime.EntityObjects.Physics.SetPosition
.AcknowledgeProjection(head.Token))
{
break;
}
}
Assert.Equal(
0,
runtime.EntityObjects.CaptureOwnership()
.PendingCompletionReceiptCount);
}
[Fact]
public void PlacementReceiptValidationDoesNotRegainMovementOrPhysicsAuthority()
{
@ -389,11 +517,25 @@ public sealed class HeadlessSessionHostTests
new HeadlessDiagnosticWriter(TextWriter.Null),
operations);
GameRuntime runtime = host.Runtime;
// C3c: conductor-driven flow — a live generation admits the initial
// residence, the flat landblock's collision generation commits, and
// the world projection's pump drives the local first-entry conductor
// to completion (the deleted SynchronizeLocalPlayer hand-copy's
// replacement).
Assert.Equal(
RuntimeSessionStartStatus.Connected,
host.Start().Status);
const uint player = 0x50000004u;
runtime.PlayerIdentity.ServerGuid = player;
runtime.EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
0xA9B40000u, 1UL);
AddFlatLandblock(runtime.EntityObjects.Physics.Engine);
runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
0xA9B40000u, 1UL, ready: true);
AcDream.Runtime.Session.RuntimeFirstEntryDriveController firstEntry =
CreateFirstEntryDrive(runtime);
RuntimeEntityRecord record = runtime.EntityObjects
.RegisterEntity(Spawn(player))
.RegisterEntityWithInitialResidence(Spawn(player), isLocalPlayer: true)
.Canonical!;
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
record,
@ -402,7 +544,8 @@ public sealed class HeadlessSessionHostTests
replaceGeneration: false));
var directProjection = new HeadlessSessionWorldProjection(
runtime,
new FixtureCollisionNeighborhood());
new FixtureCollisionNeighborhood(),
firstEntry);
directProjection.ProjectSpawn(record, isLocalPlayer: true);
PlayerMovementController controller = Assert.IsType<
PlayerMovementController>(runtime.MovementOwner.Controller);
@ -1010,6 +1153,197 @@ public sealed class HeadlessSessionHostTests
private sealed class FixtureCollisionPublicationException : Exception;
[Fact]
public void MissingPreparedCollisionYieldsTypedRetryAndCompletesWhenAvailable()
{
// C3c contract: the prepared-collision read failure is the
// conductor's typed AwaitingCollisionSource retry — no
// InvalidDataException (or any exception) escapes the host wiring,
// the entity stays a tracked, re-drivable first entry, and the same
// sequence completes once the source can serve the Setup.
var operations = new FixtureSessionOperations();
using var credential = new HeadlessCredentialSecret(
"fixture",
"password");
using var host = new HeadlessSessionHost(
Descriptor(),
credential,
new HeadlessDiagnosticWriter(TextWriter.Null),
operations);
GameRuntime runtime = host.Runtime;
Assert.Equal(
RuntimeSessionStartStatus.Connected,
host.Start().Status);
const uint player = 0x50000021u;
runtime.PlayerIdentity.ServerGuid = player;
runtime.EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
0xA9B40000u, 1UL);
AddFlatLandblock(runtime.EntityObjects.Physics.Engine);
runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
0xA9B40000u, 1UL, ready: true);
var source = new FlakySetupCollisionSource();
var firstEntry = new AcDream.Runtime.Session
.RuntimeFirstEntryDriveController(
runtime.EntityObjects,
runtime.Clock,
source,
() => PlayerMovementConstructionOptions.From(
runtime.CharacterOwner.MovementSkills.Snapshot),
static _ => new RuntimeLocalPlayerPhysicsActivationPreparation(
Radius: 0.48f,
Height: 1.835f,
RuntimeLocalPlayerShadowDisposition.ProvenShapeless));
RuntimeEntityRecord record = runtime.EntityObjects
.RegisterEntityWithInitialResidence(Spawn(player), isLocalPlayer: true)
.Canonical!;
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
record,
record.CreateIntegrationVersion,
record.Snapshot,
replaceGeneration: false));
var collision = new FixtureCollisionNeighborhood();
var projection = new HeadlessSessionWorldProjection(
runtime,
collision,
firstEntry);
projection.ProjectSpawn(record, isLocalPlayer: true);
Assert.True(source.SetupReadAttempts >= 1);
Assert.Null(runtime.MovementOwner.Controller);
Assert.Equal(1, firstEntry.PendingCount);
Assert.Equal(0u, record.FullCellId);
source.Available = true;
// The session tick's retry pump.
firstEntry.DriveAll();
Assert.IsType<PlayerMovementController>(
runtime.MovementOwner.Controller);
Assert.Equal(0, firstEntry.PendingCount);
Assert.Equal(0xA9B40000u, record.FullCellId & 0xFFFF0000u);
Assert.NotEqual(0u, record.FullCellId);
}
private sealed class FlakySetupCollisionSource
: AcDream.Content.IPreparedCollisionSource
{
internal bool Available { get; set; }
internal int SetupReadAttempts { get; private set; }
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)
{
SetupReadAttempts++;
if (!Available)
{
return AcDream.Content.PreparedCollisionReadResult<
FlatSetupCollision>.Missing;
}
return AcDream.Content.PreparedCollisionReadResult<
FlatSetupCollision>.Loaded(new FlatSetupCollision(
System.Collections.Immutable.ImmutableArray<
FlatCollisionCylinder>.Empty,
[new FlatCollisionSphere(Vector3.Zero, 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()
{
}
}
private static AcDream.Runtime.Session.RuntimeFirstEntryDriveController
CreateFirstEntryDrive(GameRuntime runtime) => new(
runtime.EntityObjects,
runtime.Clock,
new LoadedSetupCollisionSource(),
() => PlayerMovementConstructionOptions.From(
runtime.CharacterOwner.MovementSkills.Snapshot),
static _ => new RuntimeLocalPlayerPhysicsActivationPreparation(
Radius: 0.48f,
Height: 1.835f,
RuntimeLocalPlayerShadowDisposition.ProvenShapeless));
private sealed class LoadedSetupCollisionSource
: 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(Vector3.Zero, 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()
{
}
}
private static void AddFlatLandblock(PhysicsEngine engine)
{
var heights = new byte[81];
@ -1026,10 +1360,12 @@ public sealed class HeadlessSessionHostTests
worldOffsetY: 0f);
}
private static WorldSession.EntitySpawn Spawn(uint guid)
private static WorldSession.EntitySpawn Spawn(
uint guid,
uint cellId = 0xA9B40001u)
{
var position = new CreateObject.ServerPosition(
0xA9B40001u,
cellId,
96f,
97f,
50f,
@ -1217,6 +1553,22 @@ public sealed class HeadlessSessionHostTests
public bool IsReady(uint fullCellId) =>
fullCellId == LastCell;
// C3c-R1 review F7: the fixture window mirrors production's 3x3
// membership around the last requested center; no center yet means
// "within" (never convert before the first CenterOn).
public bool IsWithinServiceWindow(uint fullCellId)
{
if (LastCell == 0u)
return true;
int dx = Math.Abs(
(int)((fullCellId >> 24) & 0xFFu)
- (int)((LastCell >> 24) & 0xFFu));
int dy = Math.Abs(
(int)((fullCellId >> 16) & 0xFFu)
- (int)((LastCell >> 16) & 0xFFu));
return dx <= 1 && dy <= 1;
}
}
private sealed class FixtureEventRoute(

View file

@ -104,18 +104,13 @@ public sealed class RuntimeLocalPlayerFirstEntryStateTests
Assert.Equal(RuntimeLocalPlayerFirstEntryStatus.AwaitingActivation, second);
Assert.Equal(1, fixture.Publication.CaptureOwnership().PendingActivationCount);
const ulong generation = 1UL;
fixture.Lifetime.Physics.SetPosition.BeginCollisionGeneration(
Cell & 0xFFFF0000u, generation);
fixture.Lifetime.Physics.Engine.AddLandblock(
Cell & 0xFFFF0000u,
new TerrainSurface(new byte[81], new float[256]),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
worldOffsetX: 0f,
worldOffsetY: 0f);
fixture.Lifetime.Physics.SetPosition.CommitCollisionGeneration(
Cell & 0xFFFF0000u, generation, ready: true);
// C3c-F2: the wake goes through the SAME owner production uses (the
// collision admission ledger). Driving the SetPosition seam directly
// leaves that ledger empty — a state production can never be in, and
// the reason this very test did not catch the login activation wedge.
CommitProductionCollisionGeneration(
fixture.Lifetime.Physics,
Cell & 0xFFFF0000u);
Assert.Equal(RuntimeLocalPlayerFirstEntryStatus.Completed,
fixture.Advance(out RuntimeInitialCreateExecutionReceipt receipt));
@ -603,18 +598,9 @@ public sealed class RuntimeLocalPlayerFirstEntryStateTests
// AwaitingActivation retry test does.
Assert.Equal(RuntimeLocalPlayerFirstEntryStatus.AwaitingActivation,
fixture.Advance(out _));
const ulong generation = 1UL;
fixture.Lifetime.Physics.SetPosition.BeginCollisionGeneration(
Cell & 0xFFFF0000u, generation);
fixture.Lifetime.Physics.Engine.AddLandblock(
Cell & 0xFFFF0000u,
new TerrainSurface(new byte[81], new float[256]),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
worldOffsetX: 0f,
worldOffsetY: 0f);
fixture.Lifetime.Physics.SetPosition.CommitCollisionGeneration(
Cell & 0xFFFF0000u, generation, ready: true);
CommitProductionCollisionGeneration(
fixture.Lifetime.Physics,
Cell & 0xFFFF0000u);
RuntimeLocalPlayerFirstEntryStatus status = fixture.Advance(
out RuntimeInitialCreateExecutionReceipt receipt);
@ -631,6 +617,63 @@ public sealed class RuntimeLocalPlayerFirstEntryStateTests
// Helpers
// ---------------------------------------------------------------
/// <summary>
/// C3c-F2: publishes one landblock collision generation through the exact
/// production owner chain — BeginCollisionAdmission -> prepare -> stage ->
/// CommitCollisionGeneration — so a parked local-player activation wakes
/// the way it does live, with RuntimePhysicsState's admission ledger
/// populated and then retired.
/// </summary>
private static void CommitProductionCollisionGeneration(
RuntimePhysicsState physics,
uint landblockId)
{
RuntimeCollisionAdmission admission =
physics.BeginCollisionAdmission(landblockId);
using PreparedLandblockCollisionGeneration prepared =
physics.PrepareCollisionGeneration(admission);
physics.StageCollisionAssets(
admission,
prepared,
new RuntimeLandblockCollisionAssets(
landblockId,
new TerrainSurface(new byte[81], new float[256]),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
0f,
0f,
0u));
for (int poll = 0; poll < 10_000; poll++)
{
while (physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot projection))
{
Assert.True(physics.SetPosition.AcknowledgeProjection(
projection.Token));
}
while (!physics.AdvanceCollisionRetainedOwnerCapture(
admission,
prepared).Completed)
{
}
foreach (uint ownerId in prepared.RetainedOwnerIds)
physics.RefreshCollisionRetainedOwner(admission, prepared, ownerId);
RuntimeCollisionSealStep seal;
do
{
seal = physics.AdvanceCollisionGenerationSeal(admission, prepared);
}
while (!seal.Completed && !seal.Restarted);
if (!seal.Completed)
continue;
if (physics.CommitCollisionGeneration(admission, prepared).Completed)
return;
}
throw new InvalidOperationException(
"Collision generation did not complete its Runtime mutation transaction.");
}
private static (RuntimeEntityRecord Record, RuntimePlacementProjectionToken Token)
BeginPendingOrdinaryPlacement(Fixture fixture, uint guid)
{

View file

@ -244,4 +244,261 @@ public sealed class RuntimeLocalPlayerMovementStateTests
Assert.Equal(0L, allocated);
}
// ── C3c-F1 (2026-08-02): movement-stats application seam ──────────────
//
// The typed seam is the ONLY route by which server stat recomputes
// reach the controller; the publication lifecycle decides whether the
// write lands (live/dormant) or is a typed displaced drop (terminal).
private static RuntimeMovementSkillState CompleteSkillState()
{
var skills = new RuntimeMovementSkillState();
skills.Update(runSkill: 240, jumpSkill: 180);
skills.UpdateBurden(0.5f);
skills.UpdateStamina(37);
// BF_PLAYER (0x8) + BF_PLAYER_KILLER (0x20) → ObjectInfoState.IsPK.
skills.UpdateOwnPwdBitfield(0x28u);
skills.UpdatePlayerKillerStatus(1, 123.5f);
return skills;
}
private static PlayerMovementController NewDormantRuntimeController()
{
PlayerMovementController controller =
PlayerMovementController.CreatePublicationCandidate(
new PhysicsEngine(),
PlayerMovementConstructionOptions.Fallback);
controller.SealPublicationCandidate();
controller.CommitRuntimeOwnership(new RetailObjectQuantumClock());
return controller;
}
private static (float RunRate, float JumpVz, bool CanJump, int JumpCost,
ObjectInfoState PvpFlags) CaptureStatObservables(
PlayerMovementController controller)
{
IWeenieObject weenie = controller.Motion.WeenieObj!;
Assert.True(weenie.InqRunRate(out float runRate));
Assert.True(weenie.InqJumpVelocity(1.0f, out float jumpVz));
bool canJump = weenie.CanJump(1.0f);
Assert.True(((PlayerWeenie)weenie).JumpStaminaCost(1.0f, out int cost));
return (runRate, jumpVz, canJump, cost, controller.OwnPvpFlags);
}
[Fact]
public void ApplyCharacterMovementStats_LiveApplicationIsByteIdenticalToTheRetiredDirectPath()
{
RuntimeMovementSkillState skills = CompleteSkillState();
RuntimeMovementSkillSnapshot snapshot = skills.Snapshot;
// Old direct path (the deleted RuntimeMovementSkillProjection.ApplyTo
// body), applied through the public gated setters.
var direct = new PlayerMovementController(new PhysicsEngine());
direct.SetCharacterSkills(snapshot.RunSkill, snapshot.JumpSkill);
direct.SetCharacterBurden(snapshot.Burden);
direct.SetCharacterStamina(snapshot.CurrentStamina);
direct.OwnPvpFlags = EntityCollisionFlagsExt
.FromPwdBitfield(snapshot.OwnPwdBitfield)
.ToMoverState();
direct.SetCharacterPkStatus(
snapshot.PlayerKillerStatus,
snapshot.LastPkAttackTimestamp);
var routed = new PlayerMovementController(new PhysicsEngine());
using var movement = new RuntimeLocalPlayerMovementState
{
Controller = routed,
};
Assert.Equal(
RuntimeMovementStatsApplication.AppliedLive,
movement.ApplyCharacterMovementStats(skills));
Assert.Equal(
CaptureStatObservables(direct),
CaptureStatObservables(routed));
Assert.Equal(ObjectInfoState.IsPK, routed.OwnPvpFlags);
}
[Fact]
public void ApplyCharacterMovementStats_DormantWindowWriteLandsOnTheControllerThatGoesLive()
{
RuntimeMovementSkillState skills = CompleteSkillState();
PlayerMovementController controller = NewDormantRuntimeController();
using var movement = new RuntimeLocalPlayerMovementState
{
Controller = controller,
};
// The committed-but-unactivated first-entry window: the dormant
// controller is installed in the movement owner while activation
// waits on cell streaming, and inbound recomputes keep pumping.
Assert.True(controller.IsRuntimeOwnedDormant);
Assert.Throws<InvalidOperationException>(
() => controller.SetCharacterSkills(1, 1));
Assert.Equal(
RuntimeMovementStatsApplication.AppliedDormant,
movement.ApplyCharacterMovementStats(skills));
// Activation flips the SAME instance live — the applied values are
// already current when movement starts.
controller.ActivateRuntimePublication();
Assert.True(controller.IsRuntimePublished);
(float runRate, _, _, _, ObjectInfoState pvp) =
CaptureStatObservables(controller);
Assert.Equal(
PlayerWeenie.GetRunRate(0.5f, 240),
runRate,
precision: 5);
Assert.Equal(ObjectInfoState.IsPK, pvp);
}
[Fact]
public void ApplyCharacterMovementStats_TerminalControllerReportsTypedDisplacedDrop()
{
RuntimeMovementSkillState skills = CompleteSkillState();
PlayerMovementController controller = NewDormantRuntimeController();
using var movement = new RuntimeLocalPlayerMovementState
{
Controller = controller,
};
controller.ActivateRuntimePublication();
var before = CaptureStatObservables(controller);
// The Motion surface is gated once terminal; capture the weenie
// reference while the controller is still published.
IWeenieObject weenie = controller.Motion.WeenieObj!;
// Post-teardown transient truth: the controller retired while still
// reachable through a displaced recompute callback. The public
// setter still throws (the seal working); the seam reports the
// typed drop instead and mutates nothing.
controller.RetireRuntimePublication();
Assert.Throws<InvalidOperationException>(
() => controller.SetCharacterSkills(1, 1));
Assert.Equal(
RuntimeMovementStatsApplication.DroppedDisplacedController,
movement.ApplyCharacterMovementStats(skills));
Assert.True(weenie.InqRunRate(out float runRate));
Assert.Equal(before.RunRate, runRate);
Assert.Equal(before.PvpFlags, controller.OwnPvpFlags);
}
[Fact]
public void ApplyCharacterMovementStats_SealedCandidateIsATypedDisplacedDrop()
{
// Defensive lifecycle-matrix row: a sealed candidate is frozen for
// the Prepare→Commit validation handshake and never installed into
// the movement owner in production (Prepare requires the owner
// empty; Commit installs it already-dormant within the same
// synchronous conductor step), so only a direct controller-level
// call can observe this row.
PlayerMovementController controller =
PlayerMovementController.CreatePublicationCandidate(
new PhysicsEngine(),
PlayerMovementConstructionOptions.Fallback);
controller.SealPublicationCandidate();
Assert.Equal(
RuntimeMovementStatsApplication.DroppedDisplacedController,
controller.ApplyCharacterMovementStats(
CompleteSkillState().Snapshot));
}
[Fact]
public void ApplyCharacterMovementStats_AbsentControllerAndIncompleteSnapshotDropSilently()
{
var skills = new RuntimeMovementSkillState();
using var movement = new RuntimeLocalPlayerMovementState();
Assert.Equal(
RuntimeMovementStatsApplication.DroppedNoController,
movement.ApplyCharacterMovementStats(skills));
movement.Controller = new PlayerMovementController(new PhysicsEngine());
Assert.Equal(
RuntimeMovementStatsApplication.DroppedIncompleteSnapshot,
movement.ApplyCharacterMovementStats(skills));
}
[Fact]
public void ApplyCharacterMovementStats_DisposedOwnerToleratesTheDisplacedCallback()
{
var movement = new RuntimeLocalPlayerMovementState
{
Controller = new PlayerMovementController(new PhysicsEngine()),
};
movement.Dispose();
// The displaced-callback-rejection pattern: a recompute landing
// after terminal disposal observes a typed drop, never a fault.
Assert.Equal(
RuntimeMovementStatsApplication.DroppedNoController,
movement.ApplyCharacterMovementStats(CompleteSkillState()));
Assert.False(movement.ReportExhaustion());
}
[Fact]
public void ReportExhaustion_DispatchesOnlyThroughALiveController()
{
using var movement = new RuntimeLocalPlayerMovementState();
Assert.False(movement.ReportExhaustion());
PlayerMovementController controller = NewDormantRuntimeController();
movement.Controller = controller;
Assert.False(movement.ReportExhaustion());
controller.ActivateRuntimePublication();
Assert.True(movement.ReportExhaustion());
controller.RetireRuntimePublication();
Assert.False(movement.ReportExhaustion());
}
[Fact]
public void ApplyServerPhysicsState_DormantDropsForActivationAndLiveAppliesExactly()
{
// The second connected-gate crash chain
// (logs/connected-world-gate-20260802-125907): an inbound local-
// player SetState pushed ApplyPhysicsState at the dormant
// first-entry controller. The typed entry decides by lifecycle.
PlayerMovementController controller =
PlayerMovementController.CreatePublicationCandidate(
new PhysicsEngine(),
PlayerMovementConstructionOptions.Fallback);
PhysicsBody body = controller.PhysicsBody;
PhysicsStateFlags initial = body.State;
PhysicsStateFlags pushed =
PhysicsStateFlags.Gravity
| PhysicsStateFlags.ReportCollisions
| PhysicsStateFlags.Ethereal;
Assert.NotEqual(initial, pushed);
controller.SealPublicationCandidate();
controller.CommitRuntimeOwnership(new RetailObjectQuantumClock());
// Dormant: the activation transaction owns the dormant body's
// physics state; the push is dropped and the body is untouched.
Assert.Equal(
RuntimeServerPhysicsStateApplication.DroppedDormantActivationOwned,
controller.ApplyServerPhysicsState(pushed));
Assert.Equal(initial, body.State);
// Published: byte-identical to the direct ApplyPhysicsState body.
controller.ActivateRuntimePublication();
Assert.Equal(
RuntimeServerPhysicsStateApplication.AppliedLive,
controller.ApplyServerPhysicsState(pushed));
Assert.Equal(pushed, body.State);
// Terminal: a displaced push, typed instead of a fault, mutating
// nothing.
controller.RetireRuntimePublication();
Assert.Throws<InvalidOperationException>(
() => controller.ApplyPhysicsState(initial));
Assert.Equal(
RuntimeServerPhysicsStateApplication.DroppedDisplacedController,
controller.ApplyServerPhysicsState(initial));
Assert.Equal(pushed, body.State);
}
}

View file

@ -198,6 +198,175 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
Assert.True(body.InWorld);
}
/// <summary>
/// C3c-F5: retail seeds the local player's ground contact from the first
/// gravity frame after <c>enter_world</c> (SmartBox::HandleCreateObject
/// 0x00454C80 → init_player 0x00455010 → CPhysicsObj::enter_world
/// 0x00516170; SetPosition records no touch), which the activation
/// compresses through the shared #270 settle sweep. Spawn (1,2,3) with a
/// flat floor at 2.7 — feet 0.3 m above it, inside the 0.5 m settle
/// reach — and the production human bottom-sphere origin (0.475) so the
/// authored placement itself stands clear of the floor.
/// </summary>
[Fact]
public void CommitActivationOnFlatGroundSeedsRetailFirstGravityFrameContact()
{
using var fixture = new Fixture(
residentWorld: true,
terrainHeight: 2.7f,
moverSphereOriginZ: 0.475f);
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var evaluation));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
fixture.Owner.CommitActivation(evaluation, out _));
PhysicsBody body = Assert.IsType<PhysicsBody>(fixture.Record.PhysicsBody);
Assert.True(body.InWorld);
Assert.True(body.InContact);
Assert.True(body.OnWalkable);
Assert.True(body.ContactPlaneValid);
Assert.True(body.ContactPlane.Normal.Z > 0.9f);
// The settle snapped the feet onto the floor, exactly like retail's
// first gravity frame.
Assert.InRange(body.Position.Z, 2.65f, 2.76f);
PlayerMovementController controller =
Assert.IsType<PlayerMovementController>(
fixture.Movement.Controller);
// The exact outbound predicate LocalPlayerOutboundController
// serializes as the wire contact bit (InContact && OnWalkable).
Assert.True(controller.CanSendPositionEvent);
Assert.True(controller.CaptureMovementResult(
mouseLookEvent: false).IsOnGround);
}
/// <summary>
/// C3c-F5 counterpart: a spawn with no floor within the settle's reach
/// (flat terrain at 0, feet at 3) must stay genuinely airborne — the
/// seed only ever commits contact the sweep actually found, exactly
/// like retail's fall after enter_world.
/// </summary>
[Fact]
public void CommitActivationOverVoidLeavesFirstEntryGenuinelyAirborne()
{
using var fixture = new Fixture(
residentWorld: true,
moverSphereOriginZ: 0.475f);
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var evaluation));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
fixture.Owner.CommitActivation(evaluation, out _));
PhysicsBody body = Assert.IsType<PhysicsBody>(fixture.Record.PhysicsBody);
Assert.True(body.InWorld);
Assert.False(body.InContact);
Assert.False(body.OnWalkable);
Assert.False(body.ContactPlaneValid);
Assert.Equal(3f, body.Position.Z);
PlayerMovementController controller =
Assert.IsType<PlayerMovementController>(
fixture.Movement.Controller);
Assert.False(controller.CanSendPositionEvent);
Assert.False(controller.CaptureMovementResult(
mouseLookEvent: false).IsOnGround);
}
/// <summary>
/// C3c-R1 review R1: the login constraint leash. Retail arms the leash
/// at every accepted-position event (SmartBox::HandleReceivedPosition
/// 0x00453FD0); the flip deleted the App-side login arm
/// (CommitPreparedPosition), so the dormant activation's final commit
/// must arm it — anchored at the committed placement (already
/// floor-snapped by the faithful placement transaction), before the
/// compressed first-gravity-frame settle runs, exactly like retail arms
/// at the received position and only then simulates the first frame.
/// </summary>
[Fact]
public void CommitActivationArmsTheLoginConstraintLeashAtTheCommittedPlacement()
{
using var fixture = new Fixture(
residentWorld: true,
terrainHeight: 2.7f,
moverSphereOriginZ: 0.475f);
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var evaluation));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
fixture.Owner.CommitActivation(evaluation, out _));
PlayerMovementController controller =
Assert.IsType<PlayerMovementController>(
fixture.Movement.Controller);
ConstraintManager? constraint =
controller.PositionManager?.Constraint;
Assert.NotNull(constraint);
Assert.True(constraint!.IsConstrained);
// The anchor cell is the placement's committed containing cell.
Assert.Equal(
controller.PhysicsBody.CellPosition.ObjCellId,
constraint.ConstraintPos.ObjCellId);
// Anchored at the committed (floor-snapped) placement pose.
Assert.InRange(
constraint.ConstraintPos.Frame.Origin.Z, 2.65f, 2.76f);
Assert.InRange(
controller.PhysicsBody.Position.Z, 2.65f, 2.76f);
Assert.Equal(
ConstraintDistance.GetStartConstraintDistance(
constraint.ConstraintPos.ObjCellId),
constraint.ConstraintDistanceStart);
Assert.Equal(
ConstraintDistance.GetMaxConstraintDistance(
constraint.ConstraintPos.ObjCellId),
constraint.ConstraintDistanceMax);
}
/// <summary>
/// C3c-R1 review R1: the arm is exactly-once — the successful final
/// commit nulls the activation envelope, so a stale CommitActivation
/// retry is RejectedAuthority and can never re-arm the leash.
/// </summary>
[Fact]
public void CommitActivationNeverRearmsTheLeashOnAStaleRetry()
{
using var fixture = new Fixture(
residentWorld: true,
terrainHeight: 2.7f,
moverSphereOriginZ: 0.475f);
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var evaluation));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
fixture.Owner.CommitActivation(evaluation, out _));
PlayerMovementController controller =
Assert.IsType<PlayerMovementController>(
fixture.Movement.Controller);
Assert.True(
controller.PositionManager!.Constraint!.IsConstrained);
controller.PositionManager.UnConstrain();
Assert.Equal(RuntimeDormantSetPositionCommitStatus.RejectedAuthority,
fixture.Owner.CommitActivation(evaluation, out _));
Assert.False(
controller.PositionManager.Constraint.IsConstrained);
}
[Fact]
public void DeferredActivationEvaluationLeavesExactOwnedGraphDormantAndRetryable()
{
@ -356,10 +525,117 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
Assert.Equal(1, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
const ulong generation = 1UL;
fixture.Lifetime.Physics.SetPosition.BeginCollisionGeneration(
Cell & 0xFFFF0000u,
generation);
// C3c-F2: the wake is driven through the SAME owner production uses
// (RuntimePhysicsState's collision admission), not through the raw
// SetPosition seam. Driving SetPosition directly leaves the admission
// ledger empty, which is a state production can never be in and which
// hid the login rearm wedge this test is supposed to cover.
CommitProductionCollisionGeneration(fixture, Cell & 0xFFFF0000u);
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var ready));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
fixture.Owner.CommitActivation(ready, out var projection));
Assert.True(projection.IsValid);
Assert.True(fixture.Record.PhysicsBody!.InWorld);
Assert.Equal(0, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
}
[Fact]
public void DeferredCommitRearmsAfterProductionAdmissionCommitsItsGeneration()
{
// C3c-F2 regression (the connected-gate login wedge). The sibling
// test above wakes the parked lease by calling BeginCollisionGeneration
// / CommitCollisionGeneration DIRECTLY on the SetPosition state, which
// leaves RuntimePhysicsState's admission ledger empty. PRODUCTION
// always wakes through BeginCollisionAdmission -> stage ->
// CommitCollisionGeneration, and that path retires the admission the
// instant the generation commits — after which
// ExpectedCollisionGeneration names the NEXT, never-begun generation.
// The rearm must key off the generation the collision world now HOLDS.
using var fixture = new Fixture();
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.DeferredCell,
fixture.Owner.EvaluateActivation(token, out var deferred));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.DeferredCell,
fixture.Owner.CommitActivation(deferred, out var noProjection));
Assert.False(noProjection.IsValid);
Assert.False(fixture.Record.PhysicsBody!.InWorld);
Assert.False(fixture.Movement.Controller!.IsRuntimePublished);
Assert.Equal(1, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
RuntimeCollisionAdmission admission = fixture.Lifetime.Physics
.BeginCollisionAdmission(Cell & 0xFFFF0000u);
// While the destination admission is in flight the lease must simply
// keep waiting — never a rejection, never an early rearm.
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.DeferredCell,
fixture.Owner.EvaluateActivation(token, out var waiting));
Assert.False(waiting.IsValid);
using PreparedLandblockCollisionGeneration prepared = fixture
.Lifetime.Physics.PrepareCollisionGeneration(admission);
fixture.Lifetime.Physics.StageCollisionAssets(
admission,
prepared,
CollisionAssets(Cell & 0xFFFF0000u));
Assert.True(CommitPrepared(
fixture.Lifetime.Physics,
admission,
prepared).Committed);
// The exact divergence this test pins: the committed authority is the
// generation the lease parked on; "expected" has already moved past it.
Assert.Equal(
admission.Generation,
fixture.Lifetime.Physics.CollisionGenerationAuthority(Cell));
Assert.NotEqual(
admission.Generation,
fixture.Lifetime.Physics.ExpectedCollisionGeneration(Cell));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var ready));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
fixture.Owner.CommitActivation(ready, out var projection));
Assert.True(projection.IsValid);
Assert.True(fixture.Record.PhysicsBody!.InWorld);
Assert.NotNull(fixture.Record.PhysicsHost);
Assert.True(fixture.Movement.Controller!.IsRuntimePublished);
Assert.Equal(0, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
}
[Fact]
public void DeferredCommitStaysParkedWhileTheCommittingAdmissionIsStillRegistered()
{
// C3c-F2, the link the live probe caught: the collision-generation
// commit marks the parked lease ready and only THEN retires its
// admission (RuntimePhysicsState.cs:2503 vs :2552-2558). Production
// reenters the first-entry pump inside that window. Rearming there
// moves the lease out of AwaitingCell and its very next evaluation
// fails the seal on the still-registered admission — after which
// EvaluateActivation can no longer answer DeferredCell and reports
// RejectedAuthority, which is TERMINAL for the conductor. The lease
// must stay parked and retryable instead.
using var fixture = new Fixture();
Assert.Equal(RuntimeLocalPlayerPhysicsPublicationStatus.Committed,
fixture.Owner.Commit(
fixture.Prepare(),
out RuntimeLocalPlayerPhysicsActivationToken token));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.DeferredCell,
fixture.Owner.EvaluateActivation(token, out var deferred));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.DeferredCell,
fixture.Owner.CommitActivation(deferred, out _));
RuntimeCollisionAdmission admission = fixture.Lifetime.Physics
.BeginCollisionAdmission(Cell & 0xFFFF0000u);
// Reconstruct the exact production instant: the landblock is in the
// engine and the parked lease has been marked ready by the SAME call
// RuntimePhysicsState.cs:2503 makes, while the admission that is
// committing has not yet been retired (:2552-2558).
fixture.Lifetime.Physics.Engine.AddLandblock(
Cell & 0xFFFF0000u,
new TerrainSurface(new byte[81], new float[256]),
@ -369,8 +645,39 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
worldOffsetY: 0f);
fixture.Lifetime.Physics.SetPosition.CommitCollisionGeneration(
Cell & 0xFFFF0000u,
generation,
admission.Generation,
ready: true);
Assert.Equal(
admission.Generation,
fixture.Lifetime.Physics.CollisionGenerationAuthority(Cell));
Assert.Equal(
admission.Generation,
fixture.Lifetime.Physics.ExpectedCollisionGeneration(Cell));
Assert.False(fixture.Lifetime.Physics
.IsCollisionEvaluationPrefixAdmissible(Cell));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.DeferredCell,
fixture.Owner.EvaluateActivation(token, out var stillWaiting));
Assert.False(stillWaiting.IsValid);
Assert.Equal(1, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
Assert.False(fixture.Record.PhysicsBody!.InWorld);
Assert.False(fixture.Movement.Controller!.IsRuntimePublished);
using (PreparedLandblockCollisionGeneration prepared = fixture
.Lifetime.Physics.PrepareCollisionGeneration(admission))
{
fixture.Lifetime.Physics.StageCollisionAssets(
admission,
prepared,
CollisionAssets(Cell & 0xFFFF0000u));
Assert.True(CommitPrepared(
fixture.Lifetime.Physics,
admission,
prepared).Committed);
}
Assert.True(fixture.Lifetime.Physics
.IsCollisionEvaluationPrefixAdmissible(Cell));
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var ready));
@ -378,6 +685,7 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
fixture.Owner.CommitActivation(ready, out var projection));
Assert.True(projection.IsValid);
Assert.True(fixture.Record.PhysicsBody!.InWorld);
Assert.True(fixture.Movement.Controller!.IsRuntimePublished);
Assert.Equal(0, fixture.Owner.CaptureOwnership()
.PendingActivationCount);
}
@ -562,15 +870,9 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
Assert.Equal(0, fixture.Lifetime.Physics.Engine.ShadowObjects
.PendingSetPositionDispatchCount);
const ulong generation = 1UL;
fixture.Lifetime.Physics.SetPosition.BeginCollisionGeneration(
Cell & 0xFFFF0000u, generation);
fixture.Lifetime.Physics.Engine.AddLandblock(
Cell & 0xFFFF0000u,
new TerrainSurface(new byte[81], new float[256]),
Array.Empty<CellSurface>(), Array.Empty<PortalPlane>(), 0f, 0f);
fixture.Lifetime.Physics.SetPosition.CommitCollisionGeneration(
Cell & 0xFFFF0000u, generation, ready: true);
// C3c-F2: production wake path (collision admission), see
// CommitProductionCollisionGeneration.
CommitProductionCollisionGeneration(fixture, Cell & 0xFFFF0000u);
Assert.Equal(RuntimeLocalPlayerPhysicsActivationStatus.Evaluated,
fixture.Owner.EvaluateActivation(token, out var ready));
Assert.Equal(RuntimeDormantSetPositionCommitStatus.Committed,
@ -2192,6 +2494,11 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
private sealed class Fixture : IDisposable
{
private bool _lifetimeDisposed;
// C3c-F5: default 0 keeps every pre-existing test's mover shape
// byte-identical; the first-entry settle tests pass the production
// human Setup's bottom-sphere origin (0.475) so an authored
// placement can stand clear of a floor the settle then reaches.
private readonly float _moverSphereOriginZ;
internal Fixture(
bool preparePlacement = true,
@ -2199,11 +2506,13 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
RuntimeLocalPlayerShadowDisposition shadowDisposition =
RuntimeLocalPlayerShadowDisposition.ProvenShapeless,
float terrainHeight = 0f,
float moverSphereOriginZ = 0f,
Vector3? initialVelocity = null,
Vector3? initialOmega = null,
float? initialFriction = null,
float? initialElasticity = null)
{
_moverSphereOriginZ = moverSphereOriginZ;
if (residentWorld)
{
var engine = new PhysicsEngine
@ -2269,7 +2578,9 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
Assert.True(Placement.IsValid);
var setup = new FlatSetupCollision(
ImmutableArray<FlatCollisionCylinder>.Empty,
[new FlatCollisionSphere(Vector3.Zero, 0.48f)],
[new FlatCollisionSphere(
new Vector3(0f, 0f, _moverSphereOriginZ),
0.48f)],
height: 0f,
radius: 0f,
stepUpHeight: 0.4f,
@ -2537,6 +2848,31 @@ public sealed class RuntimeLocalPlayerPhysicsPublicationStateTests
"Collision generation did not complete its Runtime mutation transaction.");
}
/// <summary>
/// C3c-F2: publishes one landblock collision generation through the exact
/// production owner chain — BeginCollisionAdmission -> prepare -> stage ->
/// CommitCollisionGeneration. Waking a parked activation any other way
/// (calling the SetPosition seam directly) leaves RuntimePhysicsState's
/// admission ledger empty, a state production can never reach.
/// </summary>
private static void CommitProductionCollisionGeneration(
Fixture fixture,
uint landblockId)
{
RuntimeCollisionAdmission admission = fixture.Lifetime.Physics
.BeginCollisionAdmission(landblockId);
using PreparedLandblockCollisionGeneration prepared = fixture
.Lifetime.Physics.PrepareCollisionGeneration(admission);
fixture.Lifetime.Physics.StageCollisionAssets(
admission,
prepared,
CollisionAssets(landblockId));
Assert.True(CommitPrepared(
fixture.Lifetime.Physics,
admission,
prepared).Committed);
}
private static RuntimeLandblockCollisionAssets CollisionAssets(
uint landblockId) => new(
landblockId,

View file

@ -0,0 +1,356 @@
using System.Numerics;
using AcDream.Core.Physics;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Physics;
namespace AcDream.Runtime.Tests.Physics;
/// <summary>
/// C3c-F3: landblock (0,0) — id 0x0000FFFF, Dereth's south-west map corner —
/// has the legitimate collision prefix 0x00000000. The prefix-0 "absent"
/// sentinel used to make every collision publication, quiescence, park, wake,
/// and retirement against that landblock throw
/// <see cref="ArgumentOutOfRangeException"/> from
/// <c>RuntimeSetPositionState.BeginCollisionPrefixQuiescence</c> (the
/// connected-gate crash at teleport destination (9,4), whose far streaming
/// radius reaches the corner: logs/connected-world-gate-20260802-135444).
/// These tests drive the exact production owner chain against the corner id
/// and, for quiescence semantics, assert step-for-step parity with a
/// nonzero-prefix landblock.
/// </summary>
public sealed partial class RuntimeCollisionPrefixQuiescenceTests
{
private const uint CornerLandblock = 0x0000FFFFu;
private const uint CornerPrefix = 0x00000000u;
private const uint CornerCell = 0x00000001u;
private const uint CornerCell2 = 0x00000002u;
private const uint CornerIndoorCell = 0x00000100u;
private const uint NeighborLandblock = 0x0001FFFFu;
[Fact]
public void CornerLandblockCollisionGenerationCommitsThroughTheProductionAdmissionChain()
{
// Empty engine — production streaming publishes the corner landblock
// from nothing, exactly like LandblockPhysicsPublisher.AdvanceCompleteOne.
using var fixture = new Fixture(
bindGeneration: true,
engine: new PhysicsEngine { DataCache = new PhysicsDataCache() });
RuntimePhysicsState physics = fixture.Lifetime.Physics;
RuntimeCollisionAdmission admission =
physics.BeginCollisionAdmission(CornerLandblock);
Assert.Equal(CornerLandblock, admission.LandblockId);
using PreparedLandblockCollisionGeneration prepared =
PrepareSealedMutation(physics, admission, CornerLandblock);
// Pre-fix this first commit threw ArgumentOutOfRangeException
// ("landblockId") from BeginCollisionPrefixQuiescence's prefix == 0
// sentinel guard.
RuntimeCollisionGenerationCommit commit =
CommitToCompletion(physics, admission, prepared);
Assert.True(commit.Completed);
Assert.True(physics.Engine.IsLandblockTerrainResident(CornerLandblock));
// The neighbouring landblock (0,1) — prefix 0x00010000 — publishes
// identically through the same chain.
RuntimeCollisionAdmission neighborAdmission =
physics.BeginCollisionAdmission(NeighborLandblock);
using PreparedLandblockCollisionGeneration neighborPrepared =
PrepareSealedMutation(physics, neighborAdmission, NeighborLandblock);
RuntimeCollisionGenerationCommit neighborCommit =
CommitToCompletion(physics, neighborAdmission, neighborPrepared);
Assert.True(neighborCommit.Completed);
Assert.True(
physics.Engine.IsLandblockTerrainResident(NeighborLandblock));
RuntimePhysicsOwnershipSnapshot ownership = physics.CaptureOwnership();
Assert.Equal(0, ownership.CollisionPrefixMutationCount);
Assert.Equal(0, ownership.CollisionPrefixQuiescenceCount);
Assert.Equal(0, ownership.PendingCollisionPrefixProjectionCount);
Assert.Equal(0, ownership.CollisionAdmissionCount);
}
[Fact]
public void CornerResidentParksAndRestoresAcrossAnActivationReplacement()
{
// Mirror of ActivationWaitsForExactWithdrawAndPlaceReceipts against
// the corner landblock: publish/park (ParkDeferred's quiescence
// override carries prefix 0x00000000), wake, and restore.
using var fixture = new Fixture(
bindGeneration: true,
engine: CornerEngine());
RuntimeEntityRecord record = fixture.Add(
0x700031F1u,
1,
CornerCell,
new Vector3(11f, 12f, 0f));
RuntimeSetPositionOutcome seeded = fixture.Place(
record,
CornerCell,
new Vector3(11.5f, 12f, 0f));
// SetPosition against a corner cell commits (host acknowledgement of
// the Place projection is the ordinary pending suffix, identical to
// any nonzero-prefix landblock).
Assert.Equal(
RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
seeded.Status);
Assert.True(fixture.Lifetime.Physics.SetPosition
.AcknowledgeProjection(seeded.Projection));
RuntimePhysicsState physics = fixture.Lifetime.Physics;
RuntimeCollisionAdmission admission =
physics.BeginCollisionAdmission(CornerLandblock);
using PreparedLandblockCollisionGeneration prepared =
PrepareSealedMutation(physics, admission, CornerLandblock);
RuntimeCollisionGenerationCommit first =
physics.CommitCollisionGeneration(admission, prepared);
Assert.False(first.EngineCommitted);
Assert.False(first.Completed);
Assert.False(physics.IsSpatialRoot(record));
Assert.True(physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot withdrawn));
Assert.Equal(RuntimePlacementProjectionKind.Withdraw, withdrawn.Kind);
Assert.True(physics.SetPosition.AcknowledgeProjection(withdrawn.Token));
_ = SealMutation(physics, admission, prepared);
RuntimeCollisionGenerationCommit transferred =
physics.CommitCollisionGeneration(admission, prepared);
Assert.True(transferred.EngineCommitted);
Assert.False(transferred.Completed);
Assert.True(physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot restored));
Assert.Equal(RuntimePlacementProjectionKind.Place, restored.Kind);
Assert.True(physics.SetPosition.AcknowledgeProjection(restored.Token));
RuntimeCollisionGenerationCommit completed =
physics.CommitCollisionGeneration(admission, prepared);
Assert.True(completed.Completed);
Assert.True(physics.IsSpatialRoot(record));
Assert.Equal(CornerCell, record.FullCellId);
Assert.Equal(0, physics.CaptureOwnership().CollisionPrefixMutationCount);
Assert.Equal(
0,
physics.CaptureOwnership().CollisionPrefixQuiescenceCount);
}
[Fact]
public void CornerPrefixQuiescenceHoldsAndReleasesExactlyLikeANonzeroPrefix()
{
// Contract test 2: a parked deferral against the prefix-0 landblock
// holds and releases quiescence exactly like a nonzero-prefix
// landblock. The identical script runs against both and every step's
// observable outcome must match.
using var corner = new Fixture(
bindGeneration: true,
engine: CornerEngine());
using var control = new Fixture(bindGeneration: true);
// The corner run addresses landblock (0,0) by its canonical id
// 0x0000FFFF — the raw input 0x00000000 stays reserved for "absent"
// (see AbsentLandblockIdStillCannotBeginQuiescence).
List<string> cornerLog = RunHeldPlacementQuiescenceCycle(
corner,
CornerLandblock,
sourceCell: CornerCell,
targetCell: CornerCell2,
guid: 0x700031F2u);
List<string> controlLog = RunHeldPlacementQuiescenceCycle(
control,
PrefixP,
sourceCell: CellP,
targetCell: PrefixP | 0x0002u,
guid: 0x700031F3u);
Assert.Equal(controlLog, cornerLog);
}
[Fact]
public void CornerLandblockDemotesAndWithdrawsThroughRetirementMutations()
{
using (var demoteFixture = new Fixture(
bindGeneration: true,
engine: CornerEngine()))
{
RuntimeEntityRecord outdoor = demoteFixture.Add(
0x700031F4u,
1,
CornerCell,
new Vector3(12f, 41f, 0f));
RuntimeEntityRecord indoor = demoteFixture.Add(
0x700031F5u,
1,
CornerIndoorCell,
new Vector3(13f, 41f, 0f));
RuntimeCollisionMutationResult first = demoteFixture.Lifetime
.Physics.DemoteCollisionToTerrain(CornerLandblock);
Assert.False(first.Completed);
Assert.True(demoteFixture.Lifetime.Physics.IsSpatialRoot(outdoor));
Assert.False(demoteFixture.Lifetime.Physics.IsSpatialRoot(indoor));
Assert.True(demoteFixture.Lifetime.Physics.SetPosition
.TryPeekProjection(
out RuntimePlacementProjectionSnapshot withdrawal));
Assert.Equal(indoor.Key, withdrawal.Token.Entity);
Assert.True(demoteFixture.Lifetime.Physics.SetPosition
.AcknowledgeProjection(withdrawal.Token));
RuntimeCollisionMutationResult completed = demoteFixture.Lifetime
.Physics.DemoteCollisionToTerrain(CornerLandblock);
Assert.True(completed.Completed);
Assert.True(completed.Ready);
}
using var withdrawFixture = new Fixture(
bindGeneration: true,
engine: CornerEngine());
RuntimeEntityRecord record = withdrawFixture.Add(
0x700031F6u,
1,
CornerCell,
new Vector3(14f, 42f, 0f));
RuntimeSetPositionOutcome seeded = withdrawFixture.Place(
record,
CornerCell,
new Vector3(14.5f, 42f, 0f));
Assert.True(withdrawFixture.Lifetime.Physics.SetPosition
.AcknowledgeProjection(seeded.Projection));
RuntimePhysicsState physics = withdrawFixture.Lifetime.Physics;
RuntimeCollisionMutationResult pending =
physics.WithdrawCollision(CornerLandblock);
Assert.False(pending.Completed);
Assert.True(physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot removed));
Assert.True(physics.SetPosition.AcknowledgeProjection(removed.Token));
RuntimeCollisionMutationResult withdrawn =
physics.WithdrawCollision(CornerLandblock);
Assert.True(withdrawn.Completed);
Assert.False(withdrawn.Ready);
Assert.False(physics.IsSpatialRoot(record));
}
[Fact]
public void AbsentLandblockIdStillCannotBeginQuiescence()
{
// The prefix-0 sentinel accidentally rejected the corner landblock;
// the genuine "no landblock at all" input (id 0) must keep throwing.
using var fixture = new Fixture(
bindGeneration: true,
engine: CornerEngine());
Assert.Throws<ArgumentOutOfRangeException>(
() => fixture.Begin(0u, 2UL, includeOutdoorCells: true));
Assert.Throws<ArgumentOutOfRangeException>(
() => fixture.Lifetime.Physics.BeginCollisionAdmission(0u));
RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(
CornerLandblock,
2UL,
includeOutdoorCells: true);
Assert.True(token.IsValid);
Assert.Equal(CornerPrefix, token.LandblockPrefix);
Assert.True(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence(
token));
}
/// <summary>
/// One held-placement quiescence cycle (the
/// SourceToOutsidePlacementIsHeldThenRestoredBeforeBarrierOpens shape,
/// single-prefix variant), with every observable step outcome recorded so
/// two runs can be compared for exact parity.
/// </summary>
private static List<string> RunHeldPlacementQuiescenceCycle(
Fixture fixture,
uint landblockId,
uint sourceCell,
uint targetCell,
uint guid)
{
var log = new List<string>();
RuntimeEntityRecord record = fixture.Add(
guid,
1,
sourceCell,
new Vector3(10f, 22f, 0f));
RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(
landblockId,
2UL,
includeOutdoorCells: true);
log.Add($"tokenValid={token.IsValid}");
RuntimeSetPositionOutcome held = fixture.Place(
record,
targetCell,
new Vector3(14f, 22f, 0f),
currentCell: sourceCell);
log.Add($"place={held.Status}");
log.Add($"placeGeneration={held.Projection.CollisionGeneration}");
log.Add($"placeCellLow={held.ExactCellId & 0xFFFFu:X4}");
log.Add(
$"root={fixture.Lifetime.Physics.IsSpatialRoot(record)}");
log.Add($"ackWithdraw={fixture.Lifetime.Physics.SetPosition
.AcknowledgeProjection(held.Projection)}");
log.Add($"acquire1={fixture.TryAcquire(token, out _)}");
log.Add($"acquire2={fixture.TryAcquire(token, out _)}");
log.Add($"cancelRestorePending={fixture.Lifetime.Physics
.CancelCollisionPrefixQuiescence(
token,
successorGeneration: 1UL,
successorReady: true)}");
bool peeked = fixture.Lifetime.Physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot restored);
log.Add($"restorePeeked={peeked}");
log.Add($"restoreKind={restored.Kind}");
log.Add($"restoreCellLow={restored.Token.ExactCellId & 0xFFFFu:X4}");
log.Add($"ackRestore={fixture.Lifetime.Physics.SetPosition
.AcknowledgeProjection(restored.Token)}");
log.Add($"cancelCompleted={fixture.Lifetime.Physics
.CancelCollisionPrefixQuiescence(
token,
successorGeneration: 1UL,
successorReady: true)}");
log.Add($"finalCellLow={record.FullCellId & 0xFFFFu:X4}");
log.Add(
$"finalRoot={fixture.Lifetime.Physics.IsSpatialRoot(record)}");
RuntimePhysicsOwnershipSnapshot ownership =
fixture.Lifetime.Physics.CaptureOwnership();
log.Add($"quiescences={ownership.CollisionPrefixQuiescenceCount}");
log.Add(
$"pendingProjections={ownership.PendingCollisionPrefixProjectionCount}");
return log;
}
private static RuntimeCollisionGenerationCommit CommitToCompletion(
RuntimePhysicsState physics,
RuntimeCollisionAdmission admission,
PreparedLandblockCollisionGeneration prepared)
{
for (int poll = 0; poll < 10_000; poll++)
{
RuntimeCollisionGenerationCommit commit =
physics.CommitCollisionGeneration(admission, prepared);
if (commit.Completed)
return commit;
while (physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot projection))
{
Assert.True(physics.SetPosition.AcknowledgeProjection(
projection.Token));
}
if (!commit.EngineCommitted)
_ = SealMutation(physics, admission, prepared);
}
throw new InvalidOperationException(
"Collision generation did not complete its mutation transaction.");
}
private static PhysicsEngine CornerEngine()
{
var engine = new PhysicsEngine
{
DataCache = new PhysicsDataCache(),
};
AddFlatLandblock(engine, CornerPrefix);
return engine;
}
}

View file

@ -17,18 +17,33 @@ public sealed class RuntimeLiveEntitySessionControllerTests
[Fact]
public void DirectSinkOwnsCanonicalCreateUpdateDeleteWithoutProjection()
{
using GameRuntime runtime = CreateRuntime();
// C3c: the direct sink's Create now enters the canonical initial
// residence; this test drives the remote first-entry conductor to
// completion (the direct-host pump's job) before the follow-up
// Position flows the ordinary post-residence path.
using StartedRuntime started = StartRuntime();
GameRuntime runtime = started.Runtime;
CommitLandblockCollision(runtime, 0x01010000u);
RuntimeFirstEntryDriveController drive = CreateDrive(runtime);
using var session = new WorldSession(
new IPEndPoint(IPAddress.Loopback, 9000),
new FixtureTransport());
// C3c-R1 review R3: the residence route requires a drive-backed
// world projection — a content-less (projection-less) controller
// keeps the pre-flip legacy registration instead (see
// ContentLessDirectSink_KeepsPreFlipLegacyRegistration).
var controller = new RuntimeLiveEntitySessionController(
runtime,
session);
session,
worldProjection: new FixtureWorldProjection());
LiveEntitySessionSink sink = controller.CreateSink();
WorldSession.EntitySpawn spawn =
Spawn(0x70000001u, incarnation: 1);
sink.Spawned(spawn);
drive.DriveAll();
Assert.Equal(0, drive.PendingCount);
DrainPlacementFifo(runtime);
sink.PositionUpdated(new WorldSession.EntityPositionUpdate(
spawn.Guid,
spawn.Position!.Value with
@ -62,10 +77,79 @@ public sealed class RuntimeLiveEntitySessionControllerTests
runtime.EntityObjects.Entities.PendingTeardownCount);
}
/// <summary>
/// C3c-R1 review R3: a CONTENT-LESS headless host (validated-legal
/// configuration: HeadlessConfigurationLoader accepts a null
/// process.content) builds no world projection and no first-entry
/// drive. Its Creates must keep the exact pre-flip legacy registration
/// — the accepted frame commits directly (FullCellId derives from the
/// wire cell at registration), no residence lease ever opens, and the
/// entity/residence/drive ledgers stay at zero — because a residence
/// with no drive to pump it would park every Create (and every packet
/// FIFO'd behind its pending residence) forever.
/// </summary>
[Fact]
public void ContentLessDirectSink_KeepsPreFlipLegacyRegistration()
{
using StartedRuntime started = StartRuntime();
GameRuntime runtime = started.Runtime;
using var session = new WorldSession(
new IPEndPoint(IPAddress.Loopback, 9000),
new FixtureTransport());
var controller = new RuntimeLiveEntitySessionController(
runtime,
session);
LiveEntitySessionSink sink = controller.CreateSink();
WorldSession.EntitySpawn spawn =
Spawn(0x70000003u, incarnation: 1);
sink.Spawned(spawn);
Assert.True(
runtime.EntityObjects.Entities.TryGetActive(
spawn.Guid,
out RuntimeEntityRecord canonical));
Assert.Equal(
spawn.Position!.Value.LandblockId,
canonical.FullCellId);
RuntimeEntityObjectOwnershipSnapshot ownership =
runtime.EntityObjects.CaptureOwnership();
Assert.Equal(0, ownership.InitialCreateResidenceLeaseCount);
Assert.Equal(0, ownership.FirstEntryDrivePendingCount);
Assert.Equal(1, runtime.Entities.Count);
Assert.Equal(1, runtime.Inventory.ObjectCount);
// Position packets flow the ordinary immediate path — nothing is
// FIFO'd behind a pending residence.
sink.PositionUpdated(new WorldSession.EntityPositionUpdate(
spawn.Guid,
spawn.Position!.Value with
{
PositionX = 20f,
},
Velocity: null,
PlacementId: null,
IsGrounded: true,
InstanceSequence: 1,
PositionSequence: 2,
TeleportSequence: 0,
ForcePositionSequence: 0));
Assert.Equal(
20f,
canonical.Snapshot.Position!.Value.PositionX);
sink.Deleted(new DeleteObject.Parsed(spawn.Guid, 1));
Assert.Equal(0, runtime.Entities.Count);
Assert.Equal(
0,
runtime.EntityObjects.Entities.PendingTeardownCount);
}
[Fact]
public void DirectSinkCompletesExactPortalAndSendsLoginComplete()
{
using GameRuntime runtime = CreateRuntime();
using StartedRuntime started = StartRuntime();
GameRuntime runtime = started.Runtime;
const uint playerGuid = 0x50000001u;
runtime.PlayerIdentity.ServerGuid = playerGuid;
using var session = new WorldSession(
@ -106,7 +190,8 @@ public sealed class RuntimeLiveEntitySessionControllerTests
[Fact]
public void DirectSinkProjectsAcceptedLocalWorldStateThroughOneHostSeam()
{
using GameRuntime runtime = CreateRuntime();
using StartedRuntime started = StartRuntime();
GameRuntime runtime = started.Runtime;
const uint playerGuid = 0x50000002u;
runtime.PlayerIdentity.ServerGuid = playerGuid;
using var session = new WorldSession(
@ -154,16 +239,268 @@ public sealed class RuntimeLiveEntitySessionControllerTests
Assert.True(runtime.Portal.Snapshot.Completed);
}
private static GameRuntime CreateRuntime()
/// <summary>
/// C3c-R1 review F6: the drive controller outlives its session routes,
/// so "session reset precedes a new route" is an asserted latch, not a
/// silent assumption — a second route cannot attach before the prior
/// route detached, and a route that never owned the drive cannot clear
/// the live route's tracked entries.
/// </summary>
[Fact]
public void FirstEntryDriveServesOneRouteAtATimeAndScopesClearToTheOwner()
{
using StartedRuntime started = StartRuntime();
GameRuntime runtime = started.Runtime;
RuntimeFirstEntryDriveController drive = CreateDrive(runtime);
var routeA = new object();
var routeB = new object();
drive.AttachRoute(routeA);
// Re-attaching the same owner is a no-op; a SECOND route asserts.
drive.AttachRoute(routeA);
Assert.Throws<InvalidOperationException>(
() => drive.AttachRoute(routeB));
_ = runtime.EntityObjects.RegisterEntityWithInitialResidence(
Spawn(0x70000004u, incarnation: 1),
isLocalPlayer: false);
Assert.Equal(1, drive.PendingCount);
// A non-owner detach (never-attached / displaced route rollback)
// must not clear the live route's entries.
drive.DetachRoute(routeB);
Assert.Equal(1, drive.PendingCount);
drive.DetachRoute(routeA);
Assert.Equal(0, drive.PendingCount);
// After the owner detached, a replacement route may attach.
drive.AttachRoute(routeB);
drive.DetachRoute(routeB);
}
/// <summary>
/// C3c: initial-residence admission requires a live session generation
/// (RuntimeInitialCreateResidenceState.CanAcceptCreate), so these direct
/// sink tests start one through the same fixture-session shape
/// DirectGameRuntimeCommandAdapterTests uses.
/// </summary>
private sealed class StartedRuntime : IDisposable
{
internal required GameRuntime Runtime { get; init; }
internal required LiveSessionHost Live { get; init; }
public void Dispose()
{
_ = Live.Stop(Runtime.Generation);
Runtime.Dispose();
}
}
private static StartedRuntime StartRuntime()
{
var operations = new FixtureGameplayOperations();
var sessionOperations = new FixtureSessionOperations();
var runtime = new GameRuntime(new GameRuntimeDependencies(
operations,
operations,
operations,
operations));
operations,
SessionOperations: sessionOperations));
operations.Bind(runtime);
return runtime;
var resetHost = new FixtureResetHost();
var options = new LiveSessionConnectOptions(
true,
"127.0.0.1",
9000,
"account",
"password");
var live = new LiveSessionHost(
runtime.Session,
new LiveSessionHostBindings(
new LiveSessionRoutingFactories(
_ => new FixtureEventRoute(),
_ => new FixtureCommandRoute()),
generation => runtime.ResetGeneration(generation, resetHost),
new LiveSessionSelectionBindings(
id => runtime.PlayerIdentity.ServerGuid = id,
_ => { },
runtime.CommunicationOwner.Chat.SetLocalPlayerGuid,
_ => { },
_ => { },
runtime.ActionOwner.Combat.Clear),
new LiveSessionEnteredWorldBindings(
_ => { },
() => { },
() => { },
_ => { },
() => { }),
(_, _, _) => { },
() => { }),
options);
LiveSessionStartResult startResult = live.Start(options);
Assert.Equal(LiveSessionStartStatus.Connected, startResult.Status);
Assert.NotEqual(0UL, runtime.Generation.Value);
return new StartedRuntime { Runtime = runtime, Live = live };
}
private static void CommitLandblockCollision(
GameRuntime runtime,
uint landblockId)
{
runtime.EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
landblockId, 1UL);
runtime.EntityObjects.Physics.Engine.AddLandblock(
landblockId,
new TerrainSurface(new byte[81], new float[256]),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
worldOffsetX: 0f,
worldOffsetY: 0f);
runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
landblockId, 1UL, ready: true);
}
private static RuntimeFirstEntryDriveController CreateDrive(
GameRuntime runtime) =>
new(
runtime.EntityObjects,
runtime.Clock,
new UnusedCollisionSource(),
() => PlayerMovementConstructionOptions.Fallback,
static _ => new RuntimeLocalPlayerPhysicsActivationPreparation(
Radius: 0.48f,
Height: 1.835f,
RuntimeLocalPlayerShadowDisposition.ProvenShapeless));
/// <summary>
/// Drains/acknowledges every still-pending placement receipt (the
/// ExecutorCompleted correlation is reaped by its acknowledgement) the
/// way a host subscription would.
/// </summary>
private static void DrainPlacementFifo(GameRuntime runtime)
{
while (runtime.EntityObjects.Physics.SetPosition.TryPeekProjection(
out AcDream.Runtime.Physics.RuntimePlacementProjectionSnapshot head))
{
if (!runtime.EntityObjects.Physics.SetPosition
.AcknowledgeProjection(head.Token))
{
break;
}
}
}
private sealed class UnusedCollisionSource
: 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()
{
}
}
private sealed class FixtureSessionOperations : ILiveSessionOperations
{
public IPEndPoint ResolveEndpoint(string host, int port) =>
new(IPAddress.Loopback, port);
public WorldSession CreateSession(IPEndPoint endpoint) =>
new(endpoint, new FixtureTransport());
public void Connect(WorldSession session, string user, string password)
{
}
public CharacterList.Parsed GetCharacters(WorldSession session) =>
new(
0u,
[new CharacterList.Character(0x50000001u, "Direct", 0u)],
[],
11,
"account",
true,
true);
public void EnterWorld(WorldSession session, int activeCharacterIndex)
{
}
public void Tick(WorldSession session)
{
}
public void DisposeSession(WorldSession session) =>
session.Dispose();
}
private sealed class FixtureEventRoute : ILiveSessionEventRouting
{
public void Attach()
{
}
public void Dispose()
{
}
}
private sealed class FixtureCommandRoute : ILiveSessionCommandRouting
{
public void Activate()
{
}
public void Dispose()
{
}
}
private sealed class FixtureResetHost : IRuntimeGenerationResetHost
{
public void RetireEntityProjection(RuntimeEntityRecord entity)
{
}
public void DrainEntityProjectionBoundary()
{
}
public void CompleteEntityProjectionRetirement()
{
}
}
private static WorldSession.EntitySpawn Spawn(
@ -194,7 +531,7 @@ public sealed class RuntimeLiveEntitySessionControllerTests
Position: position,
Movement: null,
AnimationFrame: null,
SetupTableId: 0x02000001u,
SetupTableId: null,
MotionTableId: null,
SoundTableId: null,
PhysicsScriptTableId: null,
@ -213,7 +550,7 @@ public sealed class RuntimeLiveEntitySessionControllerTests
return new WorldSession.EntitySpawn(
guid,
position,
0x02000001u,
null,
[],
[],
[],