using System.Buffers.Binary; using System.Collections.Immutable; using System.Net; using System.Numerics; using System.Reflection; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Headless.Configuration; using AcDream.Headless.Credentials; using AcDream.Headless.Diagnostics; using AcDream.Headless.Hosting; using AcDream.Headless.Platform; using AcDream.Runtime; using AcDream.Runtime.Entities; using AcDream.Runtime.Gameplay; using AcDream.Runtime.Physics; using AcDream.Runtime.Session; using AcDream.Runtime.World; namespace AcDream.Headless.Tests; public sealed class HeadlessSessionHostTests { [Fact] public void SingleSessionStartsReconnectsAndConvergesWithoutPresentation() { var operations = new FixtureSessionOperations(); using var diagnosticsOutput = new StringWriter(); using var credential = new HeadlessCredentialSecret( "fixture", "password"); var host = new HeadlessSessionHost( Descriptor(), credential, new HeadlessDiagnosticWriter(diagnosticsOutput), operations); RuntimeSessionStartResult first = host.Start(); ulong firstGeneration = host.Runtime.Generation.Value; host.Tick(0.015d); RuntimeSessionStartResult second = host.Reconnect(); Assert.Equal(RuntimeSessionStartStatus.Connected, first.Status); Assert.Equal(RuntimeSessionStartStatus.Connected, second.Status); Assert.Equal(0x50000002u, first.CharacterId); Assert.Equal("Headless", host.ActiveCharacterName); Assert.True(host.Runtime.Session.IsInWorld); Assert.True(host.Runtime.Generation.Value > firstGeneration); Assert.Equal(2, operations.CreatedSessionCount); Assert.Equal(1, operations.DisposedSessionCount); host.Dispose(); Assert.Equal(2, operations.DisposedSessionCount); Assert.True(host.Runtime.CaptureOwnership().IsConverged); Assert.True(credential.IsDisposed); string diagnostics = diagnosticsOutput.ToString(); Assert.Contains("\"state\":\"start-result\"", diagnostics); Assert.DoesNotContain("password", diagnostics); Assert.DoesNotContain("AcDream.App", diagnostics); } [Fact] public async Task ProcessHostRunsUntilCancellationAndReturnsStableExitCode() { var configuration = new HeadlessConfiguration { Version = 1, Sessions = [ Descriptor( HeadlessCredentialProviderKind.StandardInput, "stdin-bot"), ], }; HeadlessPathSet paths = HeadlessPathSet.Resolve( new HeadlessPathOverrides()); using var diagnostics = new StringWriter(); var operations = new FixtureSessionOperations(); using var host = new HeadlessProcessHost( configuration, paths, new System.IO.StringReader( "process-password" + Environment.NewLine), diagnostics, operations); using var cancellation = new CancellationTokenSource(); cancellation.Cancel(); HeadlessExitCode result = await host.RunAsync(cancellation.Token); Assert.Equal(HeadlessExitCode.Success, result); Assert.True(host.Session.Runtime.Session.IsInWorld); Assert.DoesNotContain( "process-password", diagnostics.ToString()); } [Fact] public async Task DirectCredentialsOverrideSingleConfiguredSession() { var configuration = new HeadlessConfiguration { Version = 1, Sessions = [Descriptor()], }; HeadlessPathSet paths = HeadlessPathSet.Resolve( new HeadlessPathOverrides()); using var diagnostics = new StringWriter(); var operations = new FixtureSessionOperations(); using var host = new HeadlessProcessHost( configuration, paths, TextReader.Null, diagnostics, operations, directCredentials: new HeadlessDirectCredentials( "direct-account", "direct-secret")); using var cancellation = new CancellationTokenSource(); cancellation.Cancel(); HeadlessExitCode result = await host.RunAsync(cancellation.Token); Assert.Equal(HeadlessExitCode.Success, result); Assert.Equal("direct-account", operations.LastUser); Assert.Equal("direct-secret", operations.LastPassword); Assert.DoesNotContain( "direct-secret", diagnostics.ToString()); } [Fact] public void DirectFrameUsesSharedRetailOrderAndMovementCadence() { var operations = new FixtureSessionOperations(); using var credential = new HeadlessCredentialSecret( "fixture", "password"); using var host = new HeadlessSessionHost( Descriptor(), credential, new HeadlessDiagnosticWriter(TextWriter.Null), operations); Assert.Equal( RuntimeSessionStartStatus.Connected, host.Start().Status); HydrateGroundedPlayer(host.Runtime); var sent = new List<(byte[] Body, double Time)>(); var trace = new RuntimeTraceRecorder(); using IDisposable subscription = host.Runtime.Subscribe(trace); operations.Sessions[^1].GameActionCapture = body => sent.Add(( body, host.Runtime.Clock.SimulationTimeSeconds)); RuntimeCommandResult intent = host.Commands.Movement.SetIntent( host.Runtime.Generation, new MovementInput(Forward: true, Run: true)); host.Tick(0.015d); Assert.True(intent.Accepted); Assert.Contains( trace.Entries, static entry => entry.Kind == RuntimeTraceKind.Movement); Assert.Equal( [ MoveToState.MoveToStateAction, AutonomousPosition.AutonomousPositionAction, ], sent.Select(static entry => ActionOpcode(entry.Body)).ToArray()); sent.Clear(); for (int index = 0; index < 70; index++) host.Tick(0.015d); (byte[] Body, double Time)[] positions = sent .Where(static entry => ActionOpcode(entry.Body) == AutonomousPosition.AutonomousPositionAction) .ToArray(); Assert.InRange(positions.Length, 1, 2); Assert.True(positions[^1].Time >= 1d); if (positions.Length == 2) { Assert.True( positions[1].Time - positions[0].Time >= 0.99d); } Assert.DoesNotContain( sent, entry => ActionOpcode(entry.Body) == MoveToState.MoveToStateAction); } [Fact] public void TeardownRetriesOnlyTheUnfinishedSuffix() { var operations = new FixtureSessionOperations(); var writer = new FailOnceTextWriter(); using var credential = new HeadlessCredentialSecret( "fixture", "password"); var host = new HeadlessSessionHost( Descriptor(), credential, new HeadlessDiagnosticWriter(writer), operations); Assert.Equal( RuntimeSessionStartStatus.Connected, host.Start().Status); writer.FailNextWrite = true; Assert.Throws(host.Dispose); Assert.Equal(1, operations.DisposedSessionCount); host.Dispose(); Assert.Equal(1, operations.DisposedSessionCount); Assert.True(host.Runtime.CaptureOwnership().IsConverged); } /// /// B5(a) review fix: /// proved the underlying re-offer MECHANISM works, but hand-constructed /// directly and called /// route.RetryPending() itself — it never touches /// 's own /// _eventRoute?.RetryPending() call. This test drives Tick /// itself (via the placementSinkOverride test seam added for this /// fix, mirroring the existing policyOverride parameter) so a /// regression that deletes or reorders that exact line would fail HERE, /// not just in the lower-level subscription test. /// [Fact] public void TickRetriesAPreviouslyDeclinedPlacementThroughTheRealEventRoute() { const uint remote = 0x70004301u; const uint landblock = 0xA9B40000u; const uint cell = landblock | 0x0001u; const float height = 6f; var operations = new FixtureSessionOperations(); using var credential = new HeadlessCredentialSecret( "fixture", "password"); var sink = new DecliningThenAcceptingPlacementSink(); using var host = new HeadlessSessionHost( Descriptor(), credential, new HeadlessDiagnosticWriter(TextWriter.Null), operations, placementSinkOverride: sink); GameRuntime runtime = host.Runtime; Assert.Equal( RuntimeSessionStartStatus.Connected, host.Start().Status); runtime.EntityObjects.Physics.ObserveLocalWorldFrame( cell, teleportAdvanced: false); runtime.EntityObjects.Physics.SetPosition.BeginCollisionGeneration( landblock, 1UL); AddFlatLandblock(runtime.EntityObjects.Physics.Engine); runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration( landblock, 1UL, ready: true); RuntimeEntityRecord record = runtime.EntityObjects .RegisterEntity(Spawn(remote, cell)) .Canonical!; runtime.EntityObjects.Entities.SetFinalPhysicsState( record, PhysicsStateFlags.Gravity); runtime.EntityObjects.Entities.SetFullCell( record, cell, landblock); var body = new PhysicsBody { Position = new Vector3(10f, 10f, height), Orientation = Quaternion.Identity, LastUpdateTime = 1d, State = PhysicsStateFlags.Gravity, TransientState = TransientStateFlags.Active, }; body.SnapToCell(cell, body.Position, body.Position); runtime.EntityObjects.Entities.SetPhysicsBody(record, body); record.ObjectClock.Activate(); runtime.EntityObjects.Physics.AcknowledgeSpatialProjection( record, spatial: true); RuntimeEntityPlacementToken token = runtime.EntityObjects.Physics .SetPosition.TryBeginExclusiveAuthoredPlacement( record, record.PositionAuthorityVersion, RuntimeSetPositionOperationKind.RemoteAuthoritative); Assert.True(token.IsValid); RuntimeSetPositionMoverPreparationStatus status = runtime.EntityObjects .Physics.SetPosition.TryPrepareAndSubmitAuthoredPlacement( record, token, RuntimeSetPositionOperationKind.RemoteAuthoritative, PhysicsSetPositionFlags.Teleport | PhysicsSetPositionFlags.Slide, new LoadedSetupCollisionSource(), gameTime: runtime.Clock.SimulationTimeSeconds, out RuntimeSetPositionOutcome outcome, resolveWorldOffsetFromRuntimeFrame: true); Assert.Equal(RuntimeSetPositionMoverPreparationStatus.Prepared, status); Assert.Equal( RuntimeSetPositionStatus.CommittedHostAcknowledgementPending, outcome.Status); // The production HeadlessSessionEventRoute's subscription attached // during host.Start() already observed this Place synchronously — // the fake sink is still declining, so it must remain unacknowledged. Assert.Equal(1, sink.CallCount); Assert.True( runtime.EntityObjects.Physics.SetPosition.TryPeekProjection( out _)); // The sink starts accepting (mirrors a landblock finishing streaming // in) — driving ONE real host tick is what must re-offer the head, // through Tick's own wiring, not a hand-built route. sink.Accept = true; host.Tick(0.015d); Assert.Equal(2, sink.CallCount); Assert.False( runtime.EntityObjects.Physics.SetPosition.TryPeekProjection( out _)); } private sealed class DecliningThenAcceptingPlacementSink : IRuntimePlacementProjectionSink { internal int CallCount { get; private set; } internal bool Accept { get; set; } public bool TryApply(in RuntimePlacementProjectionSnapshot projection) { CallCount++; return Accept; } } [Fact] public void WorldProjectionHydratesCanonicalMovementAndTeleportState() { 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; // 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 .RegisterEntityWithInitialResidence(Spawn(player), isLocalPlayer: true) .Canonical!; Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn( record, record.CreateIntegrationVersion, record.Snapshot, replaceGeneration: false)); var collision = new FixtureCollisionNeighborhood(); // R5/A7 review fix (2026-08-05): the drive controller is now wired // (was omitted in the first pass, leaving the canonical portal arm // a no-op by construction here and dual-host parity with zero // coverage). Mirrors production composition // (HeadlessSessionHost.cs's own construction order): the SAME // RuntimeAcceptedPositionDriveController drives both hosts through // the identical TryExecuteAcceptedPortalArrival entry point. RuntimeAcceptedPositionDriveController acceptedPositionDrive = CreateAcceptedPositionDrive(runtime); var projection = new HeadlessSessionWorldProjection( runtime, collision, firstEntry, acceptedPositionDrive); projection.ProjectSpawn(record, isLocalPlayer: true); PlayerMovementController controller = Assert.IsType( runtime.MovementOwner.Controller); controller.SeedPlacementForTest( new Vector3(48f, 49f, 50f), 0xA9B40001u, new Vector3(48f, 49f, 50f)); projection.ProjectPosition( record, isLocalPlayer: true, PositionTimestampDisposition.Apply); Assert.Same(controller, runtime.MovementOwner.Controller); Assert.Equal(record.LocalEntityId, controller.LocalEntityId); Assert.Equal(new Vector3(48f, 49f, 50f), controller.Position); Assert.Equal( 0xA9B40000u, controller.CellId & 0xFFFF0000u); Assert.True((controller.CellId & 0xFFFFu) < 0x0100u); projection.BeginTeleport(); Assert.Equal(PlayerState.PortalSpace, controller.State); // R5/A7: the destination cell 0xA9B40001 is in the SAME landblock // (0xA9B40000) whose collision generation the test already // committed above, so the canonical portal arm resolves // Committed synchronously - no DeferredCell park needed to exercise // the real headless placement path. A1's headless retry loop // (RuntimeLiveEntitySessionController.PumpPortalCompletion) is // covered separately by // HeadlessPortalDeferredCellCommitsOnPumpAfterCollisionGenerationWake // below. RuntimeDestinationReadiness readiness = projection.PrepareDestination( revealGeneration: 7, new RuntimeTeleportDestination( player, InstanceSequence: 1, PositionSequence: 2, TeleportSequence: 1, ForcePositionSequence: 0, new Position( 0xA9B40001u, new Vector3(96f, 97f, 50f), Quaternion.Identity)), // R5/A7: a real, valid token - `default` was fine for the // old no-op arm but RuntimePortalPlacementAuthority.IsValid // now genuinely gates TryExecuteAcceptedPortalArrival on it. new RuntimeWorldHostProjectionToken(7, 0xA9B40001u)); Assert.True(readiness.IsCollisionReady); Assert.False(readiness.IsUnhydratable); Assert.Equal(PlayerState.InWorld, controller.State); Assert.Equal(2, collision.CenterCount); Assert.Equal(0xA9B40001u, collision.LastCell); // A4/dual-host parity: the canonical placement actually committed - // the body moved to the destination Position, not just the // collision-neighborhood bookkeeping that CenterCount/LastCell // alone would have proven even with the earlier no-op arm. Z // settles 0.005 above the wire value (the foot sphere's bottom // sits at origin + 0.475 - 0.48, LoadedSetupCollisionSource's own // doc comment, ISSUES.md #285) - X/Y are exact, Z is asserted // within that settle tolerance. Assert.Equal(96f, controller.Position.X); Assert.Equal(97f, controller.Position.Y); Assert.Equal(50f, controller.Position.Z, 0.01f); // The resolved outdoor sub-cell index is derived from X/Y within // the landblock (not the wire placeholder 0xA9B40001), same as the // FIRST ProjectPosition assertion above (":406-409") only checks // landblock+indoor-vs-outdoor, not the exact sub-cell. Assert.Equal(0xA9B40000u, controller.CellId & 0xFFFF0000u); Assert.True((controller.CellId & 0xFFFFu) < 0x0100u); } /// /// A1/R5/A7 review fix (2026-08-05): headless has no per-frame anim /// sequencer the way the graphical host does, so its OWN equivalent of /// A1's "hold until committed" mechanism is /// HeadlessSessionWorldProjection.PrepareDestination's /// _awaitingPortalWake polling. This proves it end to end: a /// destination in a landblock whose collision generation is NOT yet /// committed parks (IsCollisionReady: false, body unmoved, no /// throw — DeferredCell is a normal headless outcome per /// PrepareDestination's own doc), and once the destination /// landblock's collision generation commits, the SAME park resolves on /// a later attempt WITHOUT a second concurrent Begin (Runtime's own /// Begin would refuse that with Contention if this class re-attempted /// blindly instead of polling PendingCount). /// [Fact] public void HeadlessPortalPrepareDestinationParksThenCommitsOnCollisionGenerationWake() { 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 = 0x50000009u; const uint destinationLandblock = 0xAAB40000u; 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 .RegisterEntityWithInitialResidence(Spawn(player), isLocalPlayer: true) .Canonical!; Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn( record, record.CreateIntegrationVersion, record.Snapshot, replaceGeneration: false)); var collision = new FixtureCollisionNeighborhood(); RuntimeAcceptedPositionDriveController acceptedPositionDrive = CreateAcceptedPositionDrive(runtime); var projection = new HeadlessSessionWorldProjection( runtime, collision, firstEntry, acceptedPositionDrive); projection.ProjectSpawn(record, isLocalPlayer: true); PlayerMovementController controller = Assert.IsType( runtime.MovementOwner.Controller); controller.SeedPlacementForTest( new Vector3(48f, 49f, 50f), 0xA9B40001u, new Vector3(48f, 49f, 50f)); projection.ProjectPosition( record, isLocalPlayer: true, PositionTimestampDisposition.Apply); projection.BeginTeleport(); var destination = new RuntimeTeleportDestination( player, InstanceSequence: 1, PositionSequence: 2, TeleportSequence: 1, ForcePositionSequence: 0, new Position( destinationLandblock | 0x0001u, new Vector3(10f, 10f, 50f), Quaternion.Identity)); var projectionToken = new RuntimeWorldHostProjectionToken( 7, destinationLandblock | 0x0001u); // Begin's own portal-vs-latest-cell gate (D-T5) requires the // destination's landblock to match the record's LATEST MERGED // Position, not just the transit's retained destination - mirror // what the real inbound Position handler already does before // TryCompletePortal ever runs (LiveEntityNetworkUpdateController's // App-side equivalent). Assert.True(runtime.EntityObjects.TryApplyPosition( new WorldSession.EntityPositionUpdate( player, new CreateObject.ServerPosition( destination.Position.ObjCellId, destination.Position.Frame.Origin.X, destination.Position.Frame.Origin.Y, destination.Position.Frame.Origin.Z, destination.Position.Frame.Orientation.W, destination.Position.Frame.Orientation.X, destination.Position.Frame.Orientation.Y, destination.Position.Frame.Orientation.Z), Velocity: null, PlacementId: null, IsGrounded: true, InstanceSequence: 1, PositionSequence: 3, TeleportSequence: destination.TeleportSequence, ForcePositionSequence: 0), isLocalPlayer: true, forcePositionRotation: Quaternion.Identity, currentLocalVelocity: Vector3.Zero, acknowledgeProjection: null, out _, out _, out _)); // First attempt: destinationLandblock's collision generation was // never begun/committed, so the canonical arm parks DeferredCell. // Must NOT throw (a park is normal, not an error). The dormant // stage (RuntimeSetPositionState's SubmitPreparedPlacementCore // deferred-commit path) already stages the body's Position/CellId // at the destination while it waits (StageDormantCellFrame, // body.InWorld=false) - the reader-visible Position moving early is // that mechanism, not evidence the placement committed; only // PlayerState/IsCollisionReady distinguish "staged" from // "committed" here. RuntimeDestinationReadiness parked = projection.PrepareDestination( revealGeneration: 7, destination, projectionToken); Assert.False(parked.IsCollisionReady); Assert.Equal(PlayerState.PortalSpace, controller.State); // A SECOND attempt while still parked must not double-Begin - // Runtime's own Begin would refuse a genuine second attempt with // Contention, but PrepareDestination's _awaitingPortalWake polls // PendingCount instead of re-attempting, so this must also report // not-ready without throwing. RuntimeDestinationReadiness stillParked = projection.PrepareDestination( revealGeneration: 7, destination, projectionToken); Assert.False(stillParked.IsCollisionReady); // Commit the destination landblock's collision generation and pump // the drive's wake (mirrors HeadlessSessionHost.Tick's own // PumpFirstEntry -> _acceptedPositionDrive.Advance() ordering). runtime.EntityObjects.Physics.SetPosition.BeginCollisionGeneration( destinationLandblock, 1UL); var heights = new byte[81]; Array.Fill(heights, (byte)50); var heightTable = new float[256]; for (int index = 0; index < heightTable.Length; index++) heightTable[index] = index; runtime.EntityObjects.Physics.Engine.AddLandblock( destinationLandblock, new TerrainSurface(heights, heightTable), [], [], worldOffsetX: 0f, worldOffsetY: 0f); runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration( destinationLandblock, 1UL, ready: true); // RuntimeAcceptedPositionDriveControllerTests.CommitLandblockCollision's // exact proven-working shape: the wake path's // resolveWorldOffsetFromRuntimeFrame requires the destination // landblock's world-frame offset to already be resolvable. runtime.EntityObjects.Physics.ObserveLocalWorldFrame( destinationLandblock | 0x0001u, teleportAdvanced: false); // Drain the placement projection FIFO AFTER committing (exact order // from RuntimeAcceptedPositionDriveControllerTests.DrainPlacementFifo's // call site: commit collision -> drain FIFO -> Advance) - the // deferred park's Withdraw notification is published as part of the // collision-generation commit, not before it. while (runtime.EntityObjects.Physics.SetPosition.TryPeekProjection( out RuntimePlacementProjectionSnapshot head)) { if (!runtime.EntityObjects.Physics.SetPosition .AcknowledgeProjection(head.Token)) { break; } } acceptedPositionDrive.Advance(); RuntimeDestinationReadiness committed = projection.PrepareDestination( revealGeneration: 7, destination, projectionToken); Assert.True(committed.IsCollisionReady); Assert.Equal(PlayerState.InWorld, controller.State); Assert.Equal(10f, controller.Position.X); Assert.Equal(10f, controller.Position.Y); Assert.Equal(destinationLandblock, controller.CellId & 0xFFFF0000u); } /// /// B1 review fix (2026-08-05): headless's required test #2 — the same /// unsound-commit-inference defect /// /// proves the HAPPY path for, exercised on the FORGOTTEN path instead. A /// DeferredCell park killed by an ordinary, unrelated accepted Position /// merge (exactly the ACE 5-10 Hz broadcast RuntimeSetPositionState's /// own doc names as the expected way a far-destination park resolves /// without committing) must leave PrepareDestination reporting /// NOT ready, unchanged, and the /// body never moved to the destination - before the fix, /// PendingCount hitting 0 made PrepareDestination infer /// "committed" and run the full readiness/materialize/LoginComplete /// sequence against an unmoved body. /// [Fact] public void HeadlessPortalPrepareDestinationForgottenByOrdinaryMergeDoesNotLatchAsCommitted() { 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 = 0x50000009u; const uint destinationLandblock = 0xAAB40000u; 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 .RegisterEntityWithInitialResidence(Spawn(player), isLocalPlayer: true) .Canonical!; Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn( record, record.CreateIntegrationVersion, record.Snapshot, replaceGeneration: false)); var collision = new FixtureCollisionNeighborhood(); RuntimeAcceptedPositionDriveController acceptedPositionDrive = CreateAcceptedPositionDrive(runtime); var projection = new HeadlessSessionWorldProjection( runtime, collision, firstEntry, acceptedPositionDrive); projection.ProjectSpawn(record, isLocalPlayer: true); PlayerMovementController controller = Assert.IsType( runtime.MovementOwner.Controller); controller.SeedPlacementForTest( new Vector3(48f, 49f, 50f), 0xA9B40001u, new Vector3(48f, 49f, 50f)); projection.ProjectPosition( record, isLocalPlayer: true, PositionTimestampDisposition.Apply); projection.BeginTeleport(); var destination = new RuntimeTeleportDestination( player, InstanceSequence: 1, PositionSequence: 2, TeleportSequence: 1, ForcePositionSequence: 0, new Position( destinationLandblock | 0x0001u, new Vector3(10f, 10f, 50f), Quaternion.Identity)); var projectionToken = new RuntimeWorldHostProjectionToken( 7, destinationLandblock | 0x0001u); Assert.True(runtime.EntityObjects.TryApplyPosition( new WorldSession.EntityPositionUpdate( player, new CreateObject.ServerPosition( destination.Position.ObjCellId, destination.Position.Frame.Origin.X, destination.Position.Frame.Origin.Y, destination.Position.Frame.Origin.Z, destination.Position.Frame.Orientation.W, destination.Position.Frame.Orientation.X, destination.Position.Frame.Orientation.Y, destination.Position.Frame.Orientation.Z), Velocity: null, PlacementId: null, IsGrounded: true, InstanceSequence: 1, PositionSequence: 3, TeleportSequence: destination.TeleportSequence, ForcePositionSequence: 0), isLocalPlayer: true, forcePositionRotation: Quaternion.Identity, currentLocalVelocity: Vector3.Zero, acknowledgeProjection: null, out _, out _, out _)); // First attempt: destinationLandblock's collision generation was // never begun/committed, so the canonical arm parks DeferredCell. RuntimeDestinationReadiness parked = projection.PrepareDestination( revealGeneration: 7, destination, projectionToken); Assert.False(parked.IsCollisionReady); Assert.Equal(PlayerState.PortalSpace, controller.State); Assert.Equal(1, acceptedPositionDrive.PendingCount); // An ordinary, UNRELATED accepted Position for the same entity - no // new teleport, just a normal broadcast at the SAME already-accepted // teleport sequence - Forgets the parked operation the same way // ACE's 5-10 Hz cadence would (RuntimeSetPositionState.Forget, called // from TryApplyPosition for every accepted, non-Rejected Position). Assert.True(runtime.EntityObjects.TryApplyPosition( new WorldSession.EntityPositionUpdate( player, new CreateObject.ServerPosition( 0x20210001u, 48f, 49f, 50f, 1f, 0f, 0f, 0f), Velocity: null, PlacementId: null, IsGrounded: true, InstanceSequence: 1, PositionSequence: 4, TeleportSequence: destination.TeleportSequence, ForcePositionSequence: 0), isLocalPlayer: true, forcePositionRotation: Quaternion.Identity, currentLocalVelocity: Vector3.Zero, acknowledgeProjection: null, out _, out _, out _)); // Forget (inside TryApplyPosition) cancels the underlying // RuntimeSetPositionState operation immediately, but the drive's OWN // _pending cache only notices on its next Advance() pump - the real // host does this every HeadlessSessionHost.Tick via PumpFirstEntry; // the test drives it explicitly, same as the App-level equivalent. acceptedPositionDrive.Advance(); Assert.Equal(0, acceptedPositionDrive.PendingCount); // Drive well past where the pre-fix inference would have latched // "committed" on the very next PrepareDestination call and then // marched to the full readiness/materialize/LoginComplete sequence. for (int i = 0; i < 10; i++) { RuntimeDestinationReadiness stillNotReady = projection.PrepareDestination( revealGeneration: 7, destination, projectionToken); Assert.False(stillNotReady.IsCollisionReady); } Assert.Equal(PlayerState.PortalSpace, controller.State); } [Fact] public void WorldProjectionIgnoresNormalEchoButBlipsForcePosition() { 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; // 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 .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); PlayerMovementController controller = Assert.IsType( runtime.MovementOwner.Controller); controller.SeedPlacementForTest( new Vector3(48f, 49f, 50f), 0xA9B40001u, new Vector3(48f, 49f, 50f)); projection.ProjectPosition( record, isLocalPlayer: true, PositionTimestampDisposition.Apply); Assert.Equal(new Vector3(48f, 49f, 50f), controller.Position); var force = new WorldSession.EntityPositionUpdate( player, record.Snapshot.Position!.Value with { PositionX = 72f, PositionY = 73f, }, Velocity: null, PlacementId: null, IsGrounded: true, InstanceSequence: 1, PositionSequence: 2, TeleportSequence: 0, ForcePositionSequence: 1); Assert.True(runtime.EntityObjects.TryApplyPosition( force, isLocalPlayer: true, forcePositionRotation: Quaternion.Identity, currentLocalVelocity: controller.BodyVelocity, acknowledgeProjection: null, out PositionTimestampDisposition disposition, out _, out AcceptedPhysicsTimestamps timestamps)); Assert.Equal( PositionTimestampDisposition.ForcePosition, disposition); // C4 route 2 (2026-08-03): a ForcePosition on the local player no // longer routes through HeadlessSessionWorldProjection.ProjectPosition // at all — RuntimeLiveEntitySessionController.OnPositionUpdated // dispatches it directly to RuntimeAcceptedPositionDriveController // instead (the deleted BlipLocalPlayer's replacement), but R2 review // fix (2026-08-03): it re-centers the collision neighborhood on the // destination FIRST — the deleted BlipLocalPlayer's own CenterOn // side effect, restored via CenterOnAcceptedForcePosition, because a // DeferredCell park this neighborhood's window can never publish is // a dead end, not a real park // (RuntimeAcceptedPositionDriveController.Advance's R1 doc comment). projection.CenterOnAcceptedForcePosition(record); RuntimeAcceptedPositionDriveController acceptedPositionDrive = CreateAcceptedPositionDrive(runtime); RuntimeAcceptedPositionExecutionStatus forceStatus = acceptedPositionDrive.TryExecuteAcceptedLocalPosition( record, force, disposition, timestamps, timestamps.PreviousTeleport); Assert.Equal( RuntimeAcceptedPositionExecutionStatus.Committed, forceStatus); // R7 review fix (2026-08-03): Z is 50.005f — within 5 mm of the // wire's bare 50f — NOT 50.48f. The dat-exact human Setup's foot // sphere is (0,0,0.475) r=.48 (LoadedSetupCollisionSource above); // its bottom sits at origin + 0.475 − 0.48 = origin − 0.005, so a // settled origin lands 0.005 m ABOVE the floor it rests on (measured // empirically against this exact fixture), not a full sphere RADIUS // above it. Retail's BlipPlayer (CPhysicsObj::SetPositionSimple // @0x005162B0, called from SmartBox::BlipPlayer @0x00453940) has // never lifted the origin by a sphere radius — the C4-route-2 FIRST // implementation pass (uncommitted; this file asserts a bare 50f at // HEAD, never 50.48f) had fitted a 50.48f assertion to a dummy // fixture sphere whose offset happened to equal its own radius, not // to retail behavior. The comment it carried described the sphere's // CENTRE, then wrongly asserted that description about // controller.Position, which is the body's ORIGIN // (PlayerMovementController.cs -> PhysicsBody.cs Position), not the // sphere centre. Assert.Equal(new Vector3(72f, 73f, 50.005f), controller.Position); // CenterCount is 2: ProjectSpawn's initial centering plus the // ForcePosition's own re-centering above (R2's restored mechanism). Assert.Equal(2, collision.CenterCount); } /// /// 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. /// [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() { 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; // 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 .RegisterEntityWithInitialResidence(Spawn(player), isLocalPlayer: true) .Canonical!; Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn( record, record.CreateIntegrationVersion, record.Snapshot, replaceGeneration: false)); var directProjection = new HeadlessSessionWorldProjection( runtime, new FixtureCollisionNeighborhood(), firstEntry); directProjection.ProjectSpawn(record, isLocalPlayer: true); PlayerMovementController controller = Assert.IsType< PlayerMovementController>(runtime.MovementOwner.Controller); Vector3 positionBefore = controller.Position; Quaternion orientationBefore = controller.BodyOrientation; RuntimePhysicsOwnershipSnapshot physicsBefore = runtime.EntityObjects.Physics.CaptureOwnership(); RuntimePlacementProjectionSnapshot receipt = Placement( runtime, record, RuntimePlacementProjectionKind.Place, new Vector3(600f, 601f, 602f), Quaternion.CreateFromAxisAngle(Vector3.UnitY, 1.2f)); var receiptSink = new HeadlessRuntimePlacementProjectionSink(runtime); Assert.True(receiptSink.TryApply(in receipt)); Assert.Equal(positionBefore, controller.Position); Assert.Equal(orientationBefore, controller.BodyOrientation); Assert.Equal( physicsBefore, runtime.EntityObjects.Physics.CaptureOwnership()); } [Fact] public void PlacementReceiptUsesExactIncarnationAndDiscardIsAckOnly() { 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; RuntimeEntityRecord record = runtime.EntityObjects .RegisterEntity(Spawn(0x50000005u)) .Canonical!; Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn( record, record.CreateIntegrationVersion, record.Snapshot, replaceGeneration: false)); var projection = new HeadlessRuntimePlacementProjectionSink(runtime); RuntimePlacementProjectionSnapshot place = Placement( runtime, record, RuntimePlacementProjectionKind.Place, Vector3.One, Quaternion.Identity); RuntimePlacementProjectionSnapshot stale = place with { Token = place.Token with { Entity = place.Token.Entity with { Incarnation = unchecked((ushort)( place.Token.Entity.Incarnation + 1)), }, }, }; RuntimePlacementProjectionSnapshot discard = stale with { Kind = RuntimePlacementProjectionKind.Discard, Token = stale.Token with { SessionLifetimeVersion = ulong.MaxValue, }, }; Assert.False(projection.TryApply(in stale)); Assert.True(projection.TryApply(in discard)); } [Fact] public void ExecutorCompletedReceiptIsAcknowledgeOnlyRegardlessOfRecordValidity() { // F1: mirrors PlacementReceiptUsesExactIncarnationAndDiscardIsAckOnly's // stale-token half - proves ExecutorCompleted is acknowledged // unconditionally (never gated by the record-lookup/portal-shape // checks Place/Withdraw depend on), so a genuinely stale/mismatched // token can never wedge the FIFO behind it. 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; RuntimeEntityRecord record = runtime.EntityObjects .RegisterEntity(Spawn(0x50000006u)) .Canonical!; Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn( record, record.CreateIntegrationVersion, record.Snapshot, replaceGeneration: false)); var sink = new HeadlessRuntimePlacementProjectionSink(runtime); RuntimePlacementProjectionSnapshot completion = Placement( runtime, record, RuntimePlacementProjectionKind.ExecutorCompleted, Vector3.One, Quaternion.Identity); RuntimePlacementProjectionSnapshot stale = completion with { Token = completion.Token with { Entity = completion.Token.Entity with { Incarnation = unchecked((ushort)( completion.Token.Entity.Incarnation + 1)), }, SessionLifetimeVersion = ulong.MaxValue, }, }; Assert.True(sink.TryApply(in completion)); Assert.True(sink.TryApply(in stale)); } [Fact] public void SessionEventRouteOwnsOneObserverAndUnsubscribesBeforeNetworkDetach() { 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; int subscriberCountDuringDetach = -1; var inner = new FixtureEventRoute( onDispose: () => subscriberCountDuringDetach = runtime.EntityObjects.Events.PlacementSubscriberCount); var placements = new HeadlessRuntimePlacementProjectionSink(runtime); var route = new HeadlessSessionEventRoute( inner, runtime, placements); Assert.Equal( 0, runtime.EntityObjects.Events.PlacementSubscriberCount); route.Attach(); route.Attach(); Assert.Equal( 1, runtime.EntityObjects.Events.PlacementSubscriberCount); Assert.Equal(1, inner.AttachCount); route.Dispose(); route.Dispose(); Assert.Equal(0, subscriberCountDuringDetach); Assert.Equal( 0, runtime.EntityObjects.Events.PlacementSubscriberCount); Assert.Equal(1, inner.DisposeCount); } [Fact] public void ReplacementSessionEventRouteGetsOneFreshObserver() { 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; var placements = new HeadlessRuntimePlacementProjectionSink(runtime); var first = new HeadlessSessionEventRoute( new FixtureEventRoute(), runtime, placements); first.Attach(); Assert.Equal( 1, runtime.EntityObjects.Events.PlacementSubscriberCount); first.Dispose(); Assert.Equal( 0, runtime.EntityObjects.Events.PlacementSubscriberCount); var replacement = new HeadlessSessionEventRoute( new FixtureEventRoute(), runtime, placements); replacement.Attach(); Assert.Equal( 1, runtime.EntityObjects.Events.PlacementSubscriberCount); replacement.Dispose(); Assert.Equal( 0, runtime.EntityObjects.Events.PlacementSubscriberCount); } [Fact] public void SessionEventRouteRetryDoesNotRestorePlacementObserver() { 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; var inner = new FixtureEventRoute { DisposeFailuresRemaining = 1, }; var route = new HeadlessSessionEventRoute( inner, runtime, new HeadlessRuntimePlacementProjectionSink(runtime)); route.Attach(); Assert.Throws(route.Dispose); Assert.Equal( 0, runtime.EntityObjects.Events.PlacementSubscriberCount); Assert.Equal(1, inner.DisposeCount); route.Dispose(); Assert.Equal(2, inner.DisposeCount); Assert.Equal( 0, runtime.EntityObjects.Events.PlacementSubscriberCount); } [Fact] public void CollisionTransactionCancelsPostAdmissionFaultWithoutWithdrawingActiveWorld() { using var lifetime = new RuntimeEntityObjectLifetime(); RuntimePhysicsState physics = lifetime.Physics; const uint landblockId = 0xA9B4FFFFu; CompleteCollisionGeneration( physics, landblockId, afterAdmission: null, (admission, prepared) => physics.StageCollisionAssets( admission, prepared, CollisionAssets(landblockId, 10f))); Assert.Throws(() => CompleteCollisionGeneration( physics, landblockId, _ => throw new FixtureCollisionPublicationException(), (_, _) => throw new InvalidOperationException( "Staging must not run after the injected admission fault."))); Assert.Equal(10f, physics.Engine.SampleTerrainZ(1f, 1f)); RuntimePhysicsOwnershipSnapshot ownership = physics.CaptureOwnership(); Assert.Equal(1, ownership.LandblockCount); Assert.Equal(0, ownership.CollisionAdmissionCount); } [Fact] public void CollisionTransactionYieldsTheFirstNonterminalRuntimePoll() { using var lifetime = new RuntimeEntityObjectLifetime(); RuntimePhysicsState physics = lifetime.Physics; const uint landblockId = 0xA9B4FFFFu; HeadlessCollisionGenerationTransaction transaction = HeadlessCollisionGenerationTransaction.Begin( physics, landblockId, afterAdmission: null, (admission, prepared) => physics.StageCollisionAssets( admission, prepared, CollisionAssets(landblockId, 10f))); HeadlessCollisionGenerationAdvance advance; do { advance = transaction.Advance(); Assert.True(advance.Progressed); } while (!advance.YieldToCaller); Assert.False(advance.Completed); Assert.False(advance.WaitingForProjectionAcknowledgement); Assert.False(transaction.CompletionCommitted); do { advance = transaction.Advance(); Assert.True(advance.Progressed); } while (!advance.Completed && !advance.YieldToCaller); if (!advance.Completed) advance = transaction.Advance(); Assert.True(advance.Completed); Assert.True(transaction.EngineMutationCommitted); Assert.True(transaction.CompletionCommitted); Assert.Equal(0, physics.CaptureOwnership().CollisionAdmissionCount); } [Fact] public void CollisionTransactionRetainsPostEngineCancellationUntilPlaceAck() { using var lifetime = new RuntimeEntityObjectLifetime(); RuntimePhysicsState physics = lifetime.Physics; const uint landblockId = 0xA9B4FFFFu; CompleteCollisionGeneration( physics, landblockId, afterAdmission: null, (admission, prepared) => physics.StageCollisionAssets( admission, prepared, CollisionAssets(landblockId, 10f))); const uint guid = 0x70004201u; const uint cell = 0xA9B40001u; Vector3 position = new(10f, 10f, 0f); RuntimeEntityRecord record = lifetime.RegisterEntity( Spawn(guid)).Canonical!; lifetime.Entities.SetFinalPhysicsState(record, PhysicsStateFlags.Gravity); lifetime.Entities.SetFullCell(record, cell, landblockId); var body = new PhysicsBody { Position = position, Orientation = Quaternion.Identity, LastUpdateTime = 1d, State = PhysicsStateFlags.Gravity, TransientState = TransientStateFlags.Active, }; body.SnapToCell(cell, position, position); lifetime.Entities.SetPhysicsBody(record, body); record.ObjectClock.Activate(); physics.AcknowledgeSpatialProjection(record, spatial: true); RuntimePlacementProjectionToken seeded = SeedRuntimePlacement( physics, record, cell, position); Assert.True(physics.SetPosition.AcknowledgeProjection(seeded)); HeadlessCollisionGenerationTransaction transaction = HeadlessCollisionGenerationTransaction.Begin( physics, landblockId, afterAdmission: null, (admission, prepared) => physics.StageCollisionAssets( admission, prepared, CollisionAssets(landblockId, 20f))); HeadlessCollisionGenerationAdvance advance; do { advance = transaction.Advance(); } while (!advance.WaitingForProjectionAcknowledgement); Assert.False(transaction.EngineMutationCommitted); Assert.True(physics.SetPosition.TryPeekProjection( out RuntimePlacementProjectionSnapshot withdrawal)); Assert.True(physics.SetPosition.AcknowledgeProjection(withdrawal.Token)); do { advance = transaction.Advance(); } while (!advance.WaitingForProjectionAcknowledgement); Assert.True(transaction.EngineMutationCommitted); Assert.True(physics.SetPosition.TryPeekProjection( out RuntimePlacementProjectionSnapshot placement)); Assert.False(transaction.TryCancel()); Assert.True(physics.SetPosition.AcknowledgeProjection(placement.Token)); Assert.True(transaction.TryCancel()); RuntimePhysicsOwnershipSnapshot ownership = physics.CaptureOwnership(); Assert.Equal(0, ownership.CollisionPrefixMutationCount); Assert.Equal(0, ownership.CollisionAdmissionCount); } [Fact] public void CollisionTransactionCancelsStagingFaultWithoutWithdrawingActiveWorld() { using var lifetime = new RuntimeEntityObjectLifetime(); RuntimePhysicsState physics = lifetime.Physics; const uint landblockId = 0xA9B4FFFFu; CompleteCollisionGeneration( physics, landblockId, afterAdmission: null, (admission, prepared) => physics.StageCollisionAssets( admission, prepared, CollisionAssets(landblockId, 10f))); Assert.Throws(() => CompleteCollisionGeneration( physics, landblockId, afterAdmission: null, (admission, prepared) => { physics.StageCollisionAssets( admission, prepared, CollisionAssets(landblockId, 25f)); throw new FixtureCollisionPublicationException(); })); Assert.Equal(10f, physics.Engine.SampleTerrainZ(1f, 1f)); RuntimePhysicsOwnershipSnapshot ownership = physics.CaptureOwnership(); Assert.Equal(1, ownership.LandblockCount); Assert.Equal(0, ownership.CollisionAdmissionCount); } private static HeadlessSessionDescriptor Descriptor( HeadlessCredentialProviderKind provider = HeadlessCredentialProviderKind.Environment, string credentialReference = "BOT_PASSWORD") => new() { Id = "bot", Endpoint = new HeadlessEndpointDescriptor { Host = "127.0.0.1", Port = 9000, }, Account = "account", Character = new HeadlessCharacterSelector { Name = "headless", }, Policy = new HeadlessBotPolicyDescriptor { Id = "idle", }, Credential = new HeadlessCredentialReference { Provider = provider, Reference = credentialReference, }, }; private static void HydrateGroundedPlayer(GameRuntime runtime) { const uint player = 0x50000002u; PhysicsEngine engine = runtime.EntityObjects.Physics.Engine; AddFlatLandblock(engine); RuntimeEntityRecord record = runtime.EntityObjects .RegisterEntity(Spawn(player)) .Canonical!; Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn( record, record.CreateIntegrationVersion, record.Snapshot, replaceGeneration: false)); var controller = new PlayerMovementController(engine); controller.SeedPlacementForTest( new Vector3(96f, 97f, 50f), 0xA9B40001u, new Vector3(96f, 97f, 50f)); runtime.MovementOwner.Controller = controller; } private static void CompleteCollisionGeneration( RuntimePhysicsState physics, uint landblockId, Action? afterAdmission, Action stage) { HeadlessCollisionGenerationTransaction transaction = HeadlessCollisionGenerationTransaction.Begin( physics, landblockId, afterAdmission, stage); while (true) { HeadlessCollisionGenerationAdvance advance = transaction.Advance(); if (advance.Completed) return; Assert.True(advance.Progressed); Assert.False(advance.WaitingForProjectionAcknowledgement); } } private static RuntimePlacementProjectionToken SeedRuntimePlacement( RuntimePhysicsState physics, RuntimeEntityRecord record, uint cell, Vector3 position) { Type coreMarker = typeof(PhysicsEngine); Type requestType = coreMarker.Assembly.GetType( "AcDream.Core.Physics.PhysicsSetPositionRequest", throwOnError: true)!; Type flagsType = coreMarker.Assembly.GetType( "AcDream.Core.Physics.PhysicsSetPositionFlags", throwOnError: true)!; Type placementClassType = coreMarker.Assembly.GetType( "AcDream.Core.Physics.PhysicsPlacementClass", throwOnError: true)!; object request = Activator.CreateInstance( requestType, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, binder: null, args: [ position, Quaternion.Identity, cell, position, ImmutableArray.Empty, 1f, 0.4f, 0.4f, PhysicsStateFlags.None, ObjectInfoState.None, 0u, Enum.ToObject(placementClassType, 0), Enum.ToObject(flagsType, 0x011u), Vector3.Zero, 0f, 0f, 0u, cell, ], culture: null)!; Type runtimeMarker = typeof(RuntimePhysicsState); Type commandType = runtimeMarker.Assembly.GetType( "AcDream.Runtime.Physics.RuntimeSetPositionCommand", throwOnError: true)!; Type kindType = runtimeMarker.Assembly.GetType( "AcDream.Runtime.Physics.RuntimeSetPositionOperationKind", throwOnError: true)!; object command = Activator.CreateInstance( commandType, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, binder: null, args: [ request, Enum.ToObject(kindType, 2), 10d, 0UL, 0f, 0f, default(RuntimePortalPlacementAuthority), ], culture: null)!; MethodInfo apply = physics.SetPosition.GetType().GetMethod( "Apply", BindingFlags.Instance | BindingFlags.NonPublic) ?? throw new MissingMethodException("Runtime SetPosition.Apply"); object outcome = apply.Invoke( physics.SetPosition, [record, record.PositionAuthorityVersion, command])!; return (RuntimePlacementProjectionToken)(outcome.GetType().GetProperty( "Projection", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) ?.GetValue(outcome) ?? throw new MissingMemberException("Runtime placement projection")); } private static RuntimeLandblockCollisionAssets CollisionAssets( uint landblockId, float terrainHeight) { var heights = new byte[81]; var table = new float[256]; table[0] = terrainHeight; return new RuntimeLandblockCollisionAssets( landblockId, new TerrainSurface(heights, table), Array.Empty(), Array.Empty(), 0f, 0f, 0u); } 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( 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 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 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)); /// /// C4 route 2 (2026-08-03): mirrors 's /// construction pattern for the accepted-Position drive controller. No /// real WorldSession is needed for these fixture tests — /// LocalPlayerOutboundController.SendImmediatePosition no-ops on a null /// session. /// private static RuntimeAcceptedPositionDriveController CreateAcceptedPositionDrive(GameRuntime runtime) => new( runtime.EntityObjects, runtime.Clock, new LoadedSetupCollisionSource(), new LocalPlayerOutboundController((_, _, _, _, _, _) => { }), () => runtime.Generation, () => runtime.PlayerIdentity.ServerGuid, () => runtime.MovementOwner.Controller, () => runtime.CharacterOwner.UsePositionFromServer, () => null); 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 ReadSetupCollision( uint sourceFileId, CancellationToken cancellationToken = default) => AcDream.Content.PreparedCollisionReadResult .Loaded(new FlatSetupCollision( System.Collections.Immutable.ImmutableArray< FlatCollisionCylinder>.Empty, // R7 review fix (2026-08-03): the dat-exact human Setup // 0x02000001 spheres (Ts46SphereListConformanceTests.cs // :35-39) — foot (0,0,0.475) r=.48, head/torso // (0,0,1.350) r=.48. The PREVIOUS single dummy sphere at // (0,0,0) r=.48 (offset == radius) made a settled origin // rest a FULL radius above the floor; the real foot // sphere's bottom is origin + 0.475 − 0.48 = origin − // 0.005, so a settled origin lands ON the floor within // 5 mm. Retail's BlipPlayer has never lifted the origin // by a sphere radius — that was a fixture artifact, not // a retail-fidelity gain (docs/ISSUES.md #285 correction). [ new FlatCollisionSphere( new Vector3(0f, 0f, 0.475f), 0.48f), new FlatCollisionSphere( new Vector3(0f, 0f, 1.350f), 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 ReadEnvCellTopology( uint sourceFileId, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public AcDream.Content.PreparedCollisionSourceStats CollisionStats => default; public void Dispose() { } } /// /// D1 (C5b architecture review), the local-player FORCE arm — both /// directions of the reachability rule, driven through the real /// sink. /// /// /// The graphical OnPosition route returns ahead of AD-60's W2 /// for every force status EXCEPT NotApplicable, and falls /// through to it on that one. So: a force the drive HANDLED /// (Committed here) is placement-receipt-authoritative and this /// route must leave residency at the cell the placement RESOLVED, not /// re-stamp the wire cell over it; a force the drive did NOT handle /// (no drive at all — the login-window / route-1 shape) must still /// refresh the cell, because nothing else will. /// /// [Theory] [InlineData(true)] [InlineData(false)] public void LocalForcePosition_CommitsTheWireCellOnlyWhenTheDriveDeclined( bool driveHandlesIt) { 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 = 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); RuntimeFirstEntryDriveController firstEntry = CreateFirstEntryDrive(runtime); 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); PlayerMovementController controller = Assert.IsType( runtime.MovementOwner.Controller); controller.SeedPlacementForTest( new Vector3(48f, 49f, 50f), 0xA9B40001u, new Vector3(48f, 49f, 50f)); // The residence must be closed before the ordinary post-residence // rules apply at all (the conductor is the sole cell authority // while it is open — asserted separately in the Runtime suite). Assert.False(runtime.EntityObjects.TryGetInitialCreateResidence( record, out _)); using var session = new WorldSession( new IPEndPoint(IPAddress.Loopback, 9000)); var entities = new RuntimeLiveEntitySessionController( runtime, session, log: null, projection, driveHandlesIt ? CreateAcceptedPositionDrive(runtime) : null); LiveEntitySessionSink sink = entities.CreateSink(); // A wire cell that is NOT the cell a placement at (72,73) resolves // to, so "committed the wire cell" and "kept the placement's own // resolved cell" are distinguishable values rather than the same // number arrived at two ways. const uint wireCell = 0xA9B40002u; sink.PositionUpdated(new WorldSession.EntityPositionUpdate( player, new CreateObject.ServerPosition( wireCell, 72f, 73f, 50f, 1f, 0f, 0f, 0f), Velocity: null, PlacementId: null, IsGrounded: true, InstanceSequence: 1, PositionSequence: 2, TeleportSequence: 0, ForcePositionSequence: 1)); if (driveHandlesIt) { // The placement committed and resolved its OWN cell — measured // 0xA9B4001C, the outdoor landcell that actually contains // (72, 73) in this flat landblock, which is neither the wire // cell nor the spawn cell. This route added nothing on top of // it. Assert.Equal(new Vector3(72f, 73f, 50.005f), controller.Position); Assert.Equal(0xA9B4001Cu, record.FullCellId); Assert.NotEqual(wireCell, record.FullCellId); } else { Assert.Equal(wireCell, record.FullCellId); Assert.Equal(0xA9B4FFFFu, record.CanonicalLandblockId); } } private static void AddFlatLandblock(PhysicsEngine engine) { var heights = new byte[81]; Array.Fill(heights, (byte)50); var heightTable = new float[256]; for (int index = 0; index < heightTable.Length; index++) heightTable[index] = index; engine.AddLandblock( 0xA9B4FFFFu, new TerrainSurface(heights, heightTable), [], [], worldOffsetX: 0f, worldOffsetY: 0f); } private static WorldSession.EntitySpawn Spawn( uint guid, uint cellId = 0xA9B40001u) { var position = new CreateObject.ServerPosition( cellId, 96f, 97f, 50f, 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: 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: timestamps); return new WorldSession.EntitySpawn( guid, position, 0x02000001u, [], [], [], null, null, "Headless", null, null, null, PhysicsState: physics.RawState, InstanceSequence: 1, MovementSequence: 1, ServerControlSequence: 1, PositionSequence: 1, Physics: physics); } private static RuntimePlacementProjectionSnapshot Placement( GameRuntime runtime, RuntimeEntityRecord record, RuntimePlacementProjectionKind kind, Vector3 position, Quaternion orientation) { RuntimeEntityKey key = Assert.IsType(record.Key); var token = new RuntimePlacementProjectionToken( Sequence: 1, Revision: 1, Entity: key, PositionAuthorityVersion: record.PositionAuthorityVersion, SpatialAuthorityVersion: record.SpatialAuthorityVersion, PlacementCommitVersion: record.PlacementCommitVersion, SessionLifetimeVersion: runtime.EntityObjects.Entities.SessionLifetimeVersion, ExactCellId: record.FullCellId, CollisionGeneration: 1, Portal: default); return new RuntimePlacementProjectionSnapshot( token, kind, position, orientation, CellLocalPosition: position, InContact: false, OnWalkable: false); } private static uint ActionOpcode(byte[] body) => BinaryPrimitives.ReadUInt32LittleEndian( body.AsSpan(8, sizeof(uint))); private sealed class FixtureSessionOperations : ILiveSessionOperations { public List Sessions { get; } = []; public int CreatedSessionCount { get; private set; } public int DisposedSessionCount { get; private set; } public string? LastUser { get; private set; } public string? LastPassword { get; private set; } public IPEndPoint ResolveEndpoint(string host, int port) => new(IPAddress.Loopback, port); public WorldSession CreateSession(IPEndPoint endpoint) { CreatedSessionCount++; var session = new WorldSession(endpoint); Sessions.Add(session); return session; } public void Connect( WorldSession session, string user, string password) { LastUser = user; LastPassword = password; } public CharacterList.Parsed GetCharacters( WorldSession session) => new( 0u, [ new CharacterList.Character( 0x50000001u, "Other", 0u), new CharacterList.Character( 0x50000002u, "Headless", 0u), ], [], 11, "account", true, true); public void EnterWorld( WorldSession session, int activeCharacterIndex) { } public void Tick(WorldSession session) { } public void DisposeSession(WorldSession session) { DisposedSessionCount++; session.Dispose(); } } private sealed class FailOnceTextWriter : StringWriter { public bool FailNextWrite { get; set; } public override void WriteLine(string? value) { if (FailNextWrite) { FailNextWrite = false; throw new IOException("fixture write failure"); } base.WriteLine(value); } } private sealed class FixtureCollisionNeighborhood : IHeadlessCollisionNeighborhood { public int CenterCount { get; private set; } public uint LastCell { get; private set; } public void CenterOn(uint fullCellId) { CenterCount++; LastCell = fullCellId; } 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( Action? onDispose = null) : ILiveSessionEventRouting { public int AttachCount { get; private set; } public int DisposeCount { get; private set; } public int DisposeFailuresRemaining { get; set; } public void Attach() => AttachCount++; public void Dispose() { DisposeCount++; onDispose?.Invoke(); if (DisposeFailuresRemaining > 0) { DisposeFailuresRemaining--; throw new IOException("fixture route detach failure"); } } } }