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:
parent
78f1eb1896
commit
529e0e9d88
68 changed files with 5977 additions and 831 deletions
|
|
@ -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,
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue