acdream/tests/AcDream.App.Tests/LiveEntityRuntimeFixture.cs
Erik 529e0e9d88 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>
2026-08-02 18:10:33 +02:00

258 lines
10 KiB
C#

using AcDream.App.Streaming;
using AcDream.App.World;
using AcDream.Core.Physics;
using AcDream.Runtime.Entities;
namespace AcDream.App.Tests;
/// <summary>
/// Explicit isolated lifetime-group seam for App projection tests. Production
/// composition supplies its session-owned group from GameWindow; focused tests
/// use a fresh group per runtime so identity never leaks between fixtures.
/// </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 = 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 = WithGeneration(new RuntimeEntityObjectLifetime(
physicsEngine,
firstLocalEntityId));
return new LiveEntityRuntime(spatial, resources, lifetime);
}
public static LiveEntityRuntime Create(
GpuWorldState spatial,
ILiveEntityResourceLifecycle resources,
Action<LiveEntityRecord> tearDownRuntimeComponents,
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId)
{
var lifetime = WithGeneration(
new RuntimeEntityObjectLifetime(firstLocalEntityId));
return new LiveEntityRuntime(
spatial,
resources,
tearDownRuntimeComponents,
lifetime);
}
public static LiveEntityRuntime Create(
GpuWorldState spatial,
ILiveEntityResourceLifecycle resources,
ILiveEntityRuntimeComponentLifecycle runtimeComponentLifecycle,
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId)
{
var lifetime = WithGeneration(
new RuntimeEntityObjectLifetime(firstLocalEntityId));
return new LiveEntityRuntime(
spatial,
resources,
runtimeComponentLifecycle,
lifetime);
}
public static LiveEntityRuntime Create(
GpuWorldState spatial,
ILiveEntityResourceLifecycle resources,
ILiveEntityRuntimeComponentLifecycle runtimeComponentLifecycle,
PhysicsEngine physicsEngine,
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId)
{
var lifetime = WithGeneration(new RuntimeEntityObjectLifetime(
physicsEngine,
firstLocalEntityId));
return new LiveEntityRuntime(
spatial,
resources,
runtimeComponentLifecycle,
lifetime);
}
}