using System.Numerics; using AcDream.App.Input; using AcDream.App.Rendering; using AcDream.App.Streaming; using AcDream.App.Update; using AcDream.App.World; using AcDream.Content; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Core.World; using AcDream.Runtime; using AcDream.Runtime.Entities; using AcDream.Runtime.Gameplay; using AcDream.Runtime.Physics; using AcDream.Runtime.Session; using AcDream.Runtime.World; using DatReaderWriter.Enums; namespace AcDream.App.Tests.Streaming; public sealed class LocalPlayerTeleportControllerTests { [Fact] public void DeferredNetworkOwnedBindingReleasesExactlyAndAllowsRebind() { var deferred = new DeferredLocalPlayerTeleportNetworkSink(); var first = new FakeNetworkSink(); var second = new FakeNetworkSink(); IDisposable firstBinding = deferred.BindOwned(first); Assert.Throws(() => deferred.BindOwned(second)); firstBinding.Dispose(); using IDisposable secondBinding = deferred.BindOwned(second); firstBinding.Dispose(); deferred.OnTeleportStarted(9); Assert.Empty(first.Starts); Assert.Equal([9u], second.Starts); } [Fact] public void DestinationBeforeStart_IsReplayedOnceAfterPresentationActivates() { var harness = new Harness(); RuntimeTeleportDestination destination = Position( cellId: 0x20210001u, teleportSequence: 7, x: 12f, y: 34f, z: 5f); harness.OfferDestination( destination, teleportTimestampAdvanced: true); Assert.Empty(harness.Streaming.Reservations); harness.Controller.OnTeleportStarted(7); Assert.Equal(1, harness.Input.EndCount); Assert.Equal(1, harness.Mode.EnterPortalCount); Assert.Equal(Matrix4x4.Identity, harness.Presentation.BeginProjection); Assert.Equal( (1L, 0x20210001u, harness.RevealWindow.FarRadius), Assert.Single(harness.Streaming.Reservations)); Assert.True(harness.Reveal.Snapshot.IsActive); Assert.Equal(RuntimePortalKind.Portal, harness.Reveal.Snapshot.Kind); harness.OfferDestination( destination, teleportTimestampAdvanced: false); Assert.Single(harness.Streaming.Reservations); } [Fact] public void MissingPlayerController_KeepsStartAndDestinationPendingUntilProjectionExists() { var harness = new Harness(); harness.Mode.Controller = null; harness.Controller.OnTeleportStarted(2); harness.OfferDestination( Position(0x20210001u, 2, 7f, 8f, 9f), teleportTimestampAdvanced: true); Assert.False(harness.Controller.IsActive); Assert.Equal(0u, harness.Controller.ActiveDestinationCell); Assert.Null(harness.Presentation.BeginProjection); harness.Mode.Controller = new PlayerMovementController(new PhysicsEngine()); harness.Mode.Controller.SeedPlacementForTest( Vector3.Zero, 0x20210001u, Vector3.Zero); harness.Controller.Tick(0.016f); Assert.True(harness.Controller.IsActive); Assert.Equal(0x20210001u, harness.Controller.ActiveDestinationCell); Assert.NotNull(harness.Presentation.BeginProjection); Assert.Single(harness.Streaming.Reservations); } [Fact] public void ControllerWithdrawnAfterActivation_KeepsAcceptedDestinationUntilItReturns() { var harness = new Harness(); harness.Controller.OnTeleportStarted(14); Assert.True(harness.Controller.IsActive); harness.Mode.Controller = null; harness.OfferDestination( Position(0x20210001u, 14, 7f, 8f, 9f), teleportTimestampAdvanced: true); Assert.Equal(0u, harness.Controller.ActiveDestinationCell); Assert.Empty(harness.Streaming.Reservations); harness.Mode.Controller = new PlayerMovementController(new PhysicsEngine()); harness.Mode.Controller.SeedPlacementForTest(Vector3.Zero, 0x20210001u, Vector3.Zero); harness.Controller.Tick(0.016f); Assert.Equal(0x20210001u, harness.Controller.ActiveDestinationCell); Assert.Single(harness.Streaming.Reservations); } // C4 route 3 root-caused (closed): the earlier "bare-fixture InvalidData" // diagnosis was WRONG - these never reached // RuntimeSetPositionMoverPreparer.TryBuild at all. The real defect was in // LocalPlayerTeleportController.TryExecuteCanonicalPortalPlacement: it // re-read _transit.TryGetAcceptedTeleportDestination at Place time, but // RuntimeWorldTransitState.TryBeginPortalReveal (driven by AimDestination // via WorldRevealCoordinator.TryBeginPortal) atomically CONSUMES that one // accepted-destination slot the instant Aim claims the reveal generation // - so the Place-time re-read always found it empty and refused with // "cause=host-token-unavailable" before ever building a mover. Fixed by // caching the destination at Aim time (_pendingDestination), mirroring // the existing _pendingCell/_pendingRotation/_pendingRevealGeneration // pattern in the same method. Production fix, not a fixture workaround. [Fact] public void ControllerWithdrawnAfterAim_HoldsPresentationUntilModeRebuildsIt() { var harness = new Harness(); harness.Controller.OnTeleportStarted(15); harness.OfferDestination( Position(0x20210001u, 15, 7f, 8f, 9f), teleportTimestampAdvanced: true); harness.Mode.Controller = null; harness.Presentation.Enqueue(TeleportAnimEvent.Place); harness.Controller.Tick(0.016f); Assert.False(harness.Placement.Called); Assert.Empty(harness.Presentation.WorldReadyValues); harness.Mode.RebuildOnEnter = () => { var controller = new PlayerMovementController(new PhysicsEngine()); controller.SeedPlacementForTest(Vector3.Zero, 0x20210001u, Vector3.Zero); return controller; }; harness.Controller.Tick(0.016f); Assert.NotNull(harness.Mode.Controller); Assert.True(harness.Placement.Called); // A6 review fix: assert the REAL canonical body resolved the // offered destination (7,8,9), not just that the acknowledge-only // suffix ran. Assert.Equal( new Vector3(7f, 8f, 9f), harness.Movement.Controller!.Position); Assert.Equal(0x20210001u, harness.Movement.Controller.CellId); } [Fact] public void SameLandblockDestination_DoesNotRecenterAndKeepsTranslatedPosition() { var harness = new Harness(centerX: 0x20, centerY: 0x21); // 0x20210123 is an INDOOR cell (low word 0x0123 >= 0x0100) within the // same landblock the constructor already committed. It still needs // its own synthetic CellStruct - PhysicsEngine.IsSpawnCellReady // requires DataCache.GetCellStruct to resolve for indoor cells, and // a bare AddLandblock never populates one. harness.AddSyntheticIndoorCell(0x20210123u); harness.Controller.OnTeleportStarted(3); harness.OfferDestination( Position(0x20210123u, 3, 20f, 30f, 4f), teleportTimestampAdvanced: true); harness.Presentation.EmitPlaceWhenReady = true; harness.Controller.Tick(0.016f); Assert.Empty(harness.Streaming.Recenters); // C4 route 3: the canonical Runtime arm - not this fake - resolves // and commits the destination; the acknowledge-only suffix carries // no position/cell to capture (D-T4). The observable fact left at // this layer is that the committed receipt reached the suffix. Assert.True(harness.Placement.Called); // A6/R4 review fix: assert the REAL canonical body resolved the // offered destination, not just that the suffix ran. Assert.Equal( new Vector3(20f, 30f, 4f), harness.Movement.Controller!.Position); Assert.Equal(0x20210123u, harness.Movement.Controller.CellId); } /// /// A1/R1 review fix (2026-08-05): the retail/architecture review's own /// required test — "refused arm -> the anim stream does not reach /// RevealWorldViewport/FireLoginComplete with an unplaced body." Forces /// a genuine Runtime-level Contention refusal (a competing operation /// already owns the entity's placement token, same shape as the Runtime /// layer's PortalContention_WhenTheEntityAlreadyOwnsAnActiveOperation) /// and drives several ticks past where the OLD unconditional-march code /// would have already fired LoginComplete. The player must still be /// standing at the origin, in portal space, with the transit still /// active — never released into the world unplaced. /// [Fact] public void RefusedPlace_HoldsTheStreamAndConvergesOnlyAfterContentionClears() { var harness = new Harness(); harness.Controller.OnTeleportStarted(50); harness.OfferDestination( Position(0x20210001u, 50, 11f, 12f, 13f), teleportTimestampAdvanced: true); harness.Presentation.EmitPlaceWhenReady = true; Vector3 positionBefore = harness.Movement.Controller!.Position; // A competing operation already owns the entity's placement token - // the canonical Runtime Begin call inside TryAdvancePortalCommit // refuses with Contention, exactly like the Runtime-layer // PortalContention_WhenTheEntityAlreadyOwnsAnActiveOperation test. RuntimeEntityRecord record = harness.Lifetime.Entities .TryGetActive(0x50000001u, out RuntimeEntityRecord active) ? active : throw new InvalidOperationException("fixture entity missing"); RuntimeEntityPlacementToken displaced = harness.Lifetime.Physics .SetPosition.TryBeginExclusiveAuthoredPlacement( record, record.PositionAuthorityVersion, RuntimeSetPositionOperationKind.LocalAuthoritative); Assert.True(displaced.IsValid); // Several ticks, well past where the pre-fix code would already // have reached PlayExitSound -> WorldFadeIn -> FireLoginComplete // and reset the transit (the tunnel's own retail timing is 2-5s; // this drives 3x that in 0.1s steps). for (int i = 0; i < 100; i++) harness.Controller.Tick(0.1f); Assert.False(harness.Placement.Called); Assert.Equal(positionBefore, harness.Movement.Controller.Position); Assert.True(harness.Controller.IsActive); Assert.False(harness.Reveal.Snapshot.Completed); Assert.Equal(0, harness.Session.LoginCompleteCount); // Release the competing operation - the SAME `dataReady` predicate // that already held the stream in Tunnel now lets the commit // succeed on the very next tick (D-T5 row 2's "next Tick // re-attempts the Place edge", made real without ever touching // TeleportAnimSequencer). RuntimePlacementCancellationReceipt cancellation = harness.Lifetime .Physics.SetPosition.ForgetExactPlacement(displaced); if (cancellation.IsValid) harness.Lifetime.Physics.SetPosition.PublishCancellation(cancellation); harness.Controller.Tick(0.1f); Assert.True(harness.Placement.Called); Assert.Equal( new Vector3(11f, 12f, 13f), harness.Movement.Controller.Position); } /// /// B1 review fix (2026-08-05): the required test #1 — a DeferredCell /// park killed by an ORDINARY merge-time Forget (not a /// collision-generation commit) must NOT be latched as "committed". /// The destination landblock is never committed /// (CommitLandblockCollision is deliberately not called), so the /// portal parks; an unrelated ordinary accepted Position for the same /// entity then Forgets the outstanding operation exactly like a normal /// 5-10 Hz ACE broadcast would — RuntimeSetPositionState's own /// doc names this the EXPECTED way a far-destination park resolves /// without committing. Before the fix, PendingCount hitting 0 /// made the host gate latch _placementCommitted = true, release /// the sequencer, and run the ENTIRE completion sequence /// (materialize/reveal/LoginComplete) against the unmoved body. /// [Fact] public void ParkedPlace_ForgottenByOrdinaryMergeDoesNotLatchAsCommitted() { var harness = new Harness(); const uint destinationLandblock = 0x40410000u; harness.Controller.OnTeleportStarted(60); harness.OfferDestination( Position(destinationLandblock | 0x0001u, 60, 21f, 22f, 23f), teleportTimestampAdvanced: true); harness.Presentation.EmitPlaceWhenReady = true; // First tick: destinationLandblock's collision generation was never // committed, so the canonical arm parks DeferredCell. harness.Controller.Tick(0.016f); Assert.False(harness.Placement.Called); Assert.Equal(1, harness.AcceptedPositionDrive.PendingCount); // An ordinary, UNRELATED accepted Position for the same entity - // no new teleport, just a normal broadcast - Forgets the parked // operation the same way ACE's 5-10 Hz cadence would. harness.MergeOrdinaryPosition( new Vector3(48f, 49f, 50f), 0x20210001u, teleportSequence: 60); // Forget (inside TryApplyPosition) cancels the underlying // RuntimeSetPositionState operation immediately, but the drive's // OWN _pending cache only notices on its next Advance() pump - the // App host's real per-frame retry lease does this; the test drives // it explicitly. harness.AcceptedPositionDrive.Advance(); Assert.Equal(0, harness.AcceptedPositionDrive.PendingCount); // Drive well past where the pre-fix inference would have latched // "committed" on the very next tick and then marched to // LoginComplete. for (int i = 0; i < 100; i++) harness.Controller.Tick(0.1f); // NOTE: the body's Position/CellId are NOT asserted unchanged here. // The DeferredCell park's dormant stage (RuntimeSetPositionState's // SubmitPreparedPlacementCore deferred-commit path, // StageDormantCellFrame) already writes the staged destination onto // the body's controller while body.InWorld stays false and the // canonical placement has not committed - the reader-visible // Position/CellId moving early is that benign staging mechanism, not // evidence the placement committed (see the analogous headless test // HeadlessPortalPrepareDestinationParksThenCommitsOnCollisionGenerationWake // for the same finding). The discriminators below - the presentation // suffix never running, no materialize/reveal/LoginComplete, and the // controller staying active in an incomplete transit - are what // actually distinguish "committed" from "parked-then-forgotten". Assert.False(harness.Placement.Called); Assert.Equal(0, harness.Reveal.PortalMaterializationCount); Assert.Equal(0, harness.Session.LoginCompleteCount); Assert.True(harness.Controller.IsActive); Assert.False(harness.Reveal.Snapshot.Completed); } [Fact] public void CrossLandblockDestination_RecentersBeforeItCanBecomeReady() { var harness = new Harness(centerX: 0x20, centerY: 0x21); harness.CommitLandblockCollision(0x30310000u); // 0x30310100 is an INDOOR cell (low word 0x0100 >= 0x0100); see the // AddSyntheticIndoorCell doc comment for why CommitLandblockCollision // alone (terrain-only) cannot make it spawn-ready. harness.AddSyntheticIndoorCell(0x30310100u); harness.Mode.Controller!.SeedPlacementForTest( Vector3.Zero, 0x20210001u, Vector3.Zero); harness.Controller.OnTeleportStarted(4); harness.Streaming.RecenterPending = true; harness.OfferDestination( Position(0x30310100u, 4, -2f, 8f, 9f), teleportTimestampAdvanced: true); harness.Presentation.EmitPlaceWhenReady = true; harness.Controller.Tick(1f); Assert.Equal((0x30, 0x31, true), Assert.Single(harness.Streaming.Recenters)); Assert.False(Assert.Single(harness.Presentation.WorldReadyValues)); Assert.False(harness.Placement.Called); harness.Streaming.RecenterPending = false; harness.Controller.Tick(0.016f); Assert.True(harness.Placement.Called); // A6/R4 review fix: assert the REAL canonical body resolved the // cross-landblock destination, not just that the suffix ran. Assert.Equal( new Vector3(-2f, 8f, 9f), harness.Movement.Controller!.Position); Assert.Equal(0x30310100u, harness.Movement.Controller.CellId); } [Fact] public void ReadinessHold_NeverRevealsIncompleteWorldAndShowsWaitCueAfterFiveSeconds() { var harness = new Harness(worldReady: false); harness.Controller.OnTeleportStarted(5); harness.OfferDestination( Position(0x20210001u, 5, 1f, 2f, 3f), teleportTimestampAdvanced: true); harness.Presentation.EmitPlaceWhenReady = true; harness.Controller.Tick(4.9f); Assert.False(Assert.Single(harness.Presentation.WorldReadyValues)); Assert.False(harness.Presentation.WaitCueValues[^1]); Assert.False(harness.Placement.Called); harness.Controller.Tick(0.1f); harness.Controller.Tick(30f); Assert.All(harness.Presentation.WorldReadyValues, Assert.False); Assert.True(harness.Presentation.WaitCueValues[^1]); Assert.False(harness.Placement.Called); Assert.True(harness.Reveal.WaitCueShown); } [Fact] public void Place_ReconcilesInsidePlacementBeforeRevealMaterialized() { var order = new List(); var harness = new Harness(order: order); harness.Controller.OnTeleportStarted(8); harness.OfferDestination( Position(0x20210001u, 8, 4f, 5f, 6f), teleportTimestampAdvanced: true); order.Clear(); harness.Presentation.Enqueue(TeleportAnimEvent.Place); harness.Controller.Tick(0.016f); Assert.True(Index(order, "placement") < Index(order, "[world-reveal] event=materialized")); Assert.Equal(1, harness.Reveal.PortalMaterializationCount); } [Fact] public void Place_RechecksExactRuntimeLifetimeBeforeMutatingPresentation() { var harness = new Harness(); harness.Controller.OnTeleportStarted(81); harness.OfferDestination( Position(0x20210001u, 81, 4f, 5f, 6f), teleportTimestampAdvanced: true); long generation = harness.Reveal.Snapshot.Generation; Assert.True(harness.Transit.Cancel(generation)); harness.Presentation.Enqueue(TeleportAnimEvent.Place); harness.Controller.Tick(0.016f); Assert.False(harness.Placement.Called); Assert.Equal(0, harness.Reveal.PortalMaterializationCount); } [Fact] public void LoginComplete_EntersWorldThenSendsThenCompletesAndResets() { var order = new List(); var harness = new Harness(order: order); harness.Controller.OnTeleportStarted(9); harness.OfferDestination( Position(0x20210001u, 9, 4f, 5f, 6f), teleportTimestampAdvanced: true); harness.Presentation.Enqueue(TeleportAnimEvent.Place); harness.Controller.Tick(0.016f); order.Clear(); harness.Presentation.Enqueue(TeleportAnimEvent.FireLoginComplete); harness.Controller.Tick(0.016f); Assert.True(Index(order, "enter-world") < Index(order, "login-complete")); Assert.True(Index(order, "login-complete") < Index(order, "[world-reveal] event=complete")); Assert.True(Index(order, "[world-reveal] event=complete") < Index(order, "presentation-reset")); Assert.False(harness.Controller.IsActive); Assert.Equal(0u, harness.Controller.ActiveDestinationCell); Assert.Equal(1, harness.Session.LoginCompleteCount); Assert.True(harness.Reveal.Snapshot.Completed); Assert.False(harness.Reveal.Snapshot.Cancelled); } [Fact] public void ExitSound_ReleasesDestinationBeforeHidingTunnelButKeepsProtocolActive() { var order = new List(); var harness = new Harness(order: order); harness.Controller.OnTeleportStarted(91); harness.OfferDestination( Position(0x20210001u, 91, 4f, 5f, 6f), teleportTimestampAdvanced: true); harness.Streaming.ReservationEnds.Clear(); harness.Presentation.Enqueue(TeleportAnimEvent.PlayExitSound); harness.Controller.Tick(0.016f); Assert.False(harness.Presentation.IsPortalViewportVisible); Assert.True(harness.Controller.IsActive); Assert.False(harness.Reveal.Snapshot.Completed); Assert.Single(harness.Streaming.ReservationEnds); } [Fact] public void NewerStart_ReplacesOldDestinationWithoutReusingIt() { var harness = new Harness(); harness.Controller.OnTeleportStarted(10); harness.OfferDestination( Position(0x20210001u, 10, 1f, 1f, 1f), teleportTimestampAdvanced: true); harness.Controller.OnTeleportStarted(11); harness.Controller.Tick(30f); Assert.False(harness.Placement.Called); Assert.Equal(0u, harness.Controller.ActiveDestinationCell); harness.OfferDestination( Position(0x20210001u, 11, 2f, 2f, 2f), teleportTimestampAdvanced: true); harness.Presentation.Enqueue(TeleportAnimEvent.Place); harness.Controller.Tick(0.016f); Assert.True(harness.Placement.Called); // A6 review fix: this is the specific test A6 named — the removed // assertion was the ONLY thing proving the SECOND destination // (2,2,2) placed and not the superseded first one (1,1,1). // Assert.True(Placement.Called) alone cannot distinguish them. X/Y // are exact; Z settles to the Harness's uniform terrain height (5) // because both candidate wire Z values (1 and 2) sit BELOW the // floor and the placement resolve's anti-penetration push-out is // instant (unlike gravity settling from ABOVE the floor, which // takes further physics ticks this test never runs) - X/Y alone // already discriminate (1,1) from (2,2) unambiguously. Assert.Equal(2f, harness.Movement.Controller!.Position.X); Assert.Equal(2f, harness.Movement.Controller.Position.Y); Assert.Equal(0x20210001u, harness.Movement.Controller.CellId); } [Fact] public void SessionReset_ClearsTransitAndLetsRecenterConvergeAcrossLaterFrames() { var harness = new Harness(); harness.Controller.OnTeleportStarted(12); harness.OfferDestination( Position(0x20210001u, 12, 1f, 2f, 3f), teleportTimestampAdvanced: true); harness.Controller.ResetSession(); Assert.False(harness.Controller.IsActive); Assert.Equal(RuntimePortalSnapshot.Idle, harness.Reveal.Snapshot); Assert.True(harness.Streaming.LastResetWasSessionEnding); var failed = new Harness(); failed.Streaming.ResetResult = false; failed.Controller.ResetSession(); Assert.False(failed.Controller.IsActive); Assert.True(failed.Streaming.LastResetWasSessionEnding); } [Fact] public void StaleStart_DoesNotMutateInputModeOrPresentation() { var harness = new Harness(); harness.Authority.IsFresh = false; harness.Controller.OnTeleportStarted(13); Assert.Equal(0, harness.Input.EndCount); Assert.Equal(0, harness.Mode.EnterPortalCount); Assert.Null(harness.Presentation.BeginProjection); Assert.False(harness.Controller.IsActive); } [Fact] public void ReentrantNewStartDuringPlacement_CannotBeCompletedByOldTail() { var harness = new Harness(); harness.Controller.OnTeleportStarted(20); harness.OfferDestination( Position(0x20210001u, 20, 1f, 2f, 3f), teleportTimestampAdvanced: true); harness.Streaming.ReservationEnds.Clear(); harness.Placement.OnPlace = () => harness.Controller.OnTeleportStarted(21); harness.Presentation.Enqueue(TeleportAnimEvent.Place); harness.Controller.Tick(0.016f); Assert.True(harness.Controller.IsActive); Assert.Equal(0u, harness.Controller.ActiveDestinationCell); Assert.Equal(0, harness.Reveal.PortalMaterializationCount); Assert.Single(harness.Streaming.ReservationEnds); } [Fact] public void ReentrantNewStartDuringLoginComplete_PreservesReplacementLifetime() { var harness = new Harness(); harness.Controller.OnTeleportStarted(30); harness.OfferDestination( Position(0x20210001u, 30, 1f, 2f, 3f), teleportTimestampAdvanced: true); harness.Session.OnSend = () => harness.Controller.OnTeleportStarted(31); harness.Presentation.Enqueue(TeleportAnimEvent.FireLoginComplete); harness.Controller.Tick(0.016f); Assert.True(harness.Controller.IsActive); Assert.Equal(0u, harness.Controller.ActiveDestinationCell); Assert.False(harness.Reveal.Snapshot.Completed); Assert.Equal(1, harness.Session.LoginCompleteCount); } [Fact] public void DisposeFailure_CanBeRetriedWithoutDoubleDisposingAfterSuccess() { var harness = new Harness(); harness.Presentation.DisposeFailuresRemaining = 1; Assert.Throws(() => harness.Controller.Dispose()); harness.Controller.Dispose(); harness.Controller.Dispose(); Assert.Equal(2, harness.Presentation.DisposeCount); } [Fact] public void ConcretePlacement_CommitsEntityControllerAndOneSpatialReconcile() { const uint guid = 0x50000001u; const uint cell = 0x20210001u; var world = new GpuWorldState(); world.AddLandblock(new LoadedLandblock( 0x2021FFFFu, new DatReaderWriter.DBObjs.LandBlock(), Array.Empty())); var runtime = LiveEntityRuntimeFixture.Create(world, new NullResources()); runtime.RegisterLiveEntity(Spawn(guid, cell)); WorldEntity entity = runtime.MaterializeLiveEntity( guid, cell, id => new WorldEntity { Id = id, ServerGuid = guid, SourceGfxObjOrSetupId = 0x02000001u, Position = Vector3.Zero, Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), })!; var identity = new LocalPlayerIdentityState { ServerGuid = guid }; var controllerSlot = new RuntimeLocalPlayerMovementState { Controller = new PlayerMovementController(new PhysicsEngine()), }; var position = new Vector3(12f, 24f, 6f); Quaternion rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.5f); // C4 route 3: the canonical Runtime commit (RuntimeSetPositionState // .CommitCanonical, driven by RuntimeAcceptedPositionDriveController // .ReconcileAndAcknowledgePortal -> PlayerMovementController // .CommitCanonicalTeleportFrame in production) already resolves the // body/cell/orientation BEFORE this suffix runs. Reproduce that // pre-state directly rather than re-resolving it here - Place() no // longer resolves anything (D-T4). controllerSlot.Controller.SeedPlacementForTest(position, cell, position); controllerSlot.Controller.SetBodyOrientation(rotation); var cameras = new ChaseCameraInputState { Legacy = new ChaseCamera(), Retail = new RetailChaseCamera(), }; var spatial = new FakeSpatialReconcile(() => new PlacementSnapshot( entity.Position, entity.ParentCellId ?? 0u, entity.Rotation, controllerSlot.Controller.Position, controllerSlot.Controller.CellId, controllerSlot.Controller.BodyOrientation)); var placement = new LocalPlayerTeleportPlacement( runtime, identity, controllerSlot, new LocalPlayerPhysicsHostSlot(), cameras, spatial); placement.Place(rotation); Assert.Equal(entity.Position, controllerSlot.Controller.Position); Assert.Equal(entity.ParentCellId, controllerSlot.Controller.CellId); Assert.Equal(cell & 0xFFFF0000u, entity.ParentCellId & 0xFFFF0000u); Assert.Equal(rotation, entity.Rotation); Assert.Equal(rotation, controllerSlot.Controller.BodyOrientation); Assert.Equal(1, spatial.Count); Assert.Equal(entity.Position, spatial.Snapshot.EntityPosition); Assert.Equal(entity.ParentCellId, spatial.Snapshot.EntityCell); Assert.Equal(entity.Rotation, spatial.Snapshot.EntityRotation); Assert.Equal( controllerSlot.Controller.Position, spatial.Snapshot.ControllerPosition); Assert.Equal( controllerSlot.Controller.CellId, spatial.Snapshot.ControllerCell); Assert.Equal( controllerSlot.Controller.BodyOrientation, spatial.Snapshot.ControllerRotation); } [Fact] public void ConcretePlacement_CommitsDestinationSpatialBucketBeforeReconcile() { const uint guid = 0x50000001u; const uint sourceCell = 0x20210001u; const uint destinationCell = 0x30310001u; var world = new GpuWorldState(); world.AddLandblock(new LoadedLandblock( sourceCell & 0xFFFF0000u | 0xFFFFu, new DatReaderWriter.DBObjs.LandBlock(), Array.Empty())); world.AddLandblock(new LoadedLandblock( destinationCell & 0xFFFF0000u | 0xFFFFu, new DatReaderWriter.DBObjs.LandBlock(), Array.Empty())); var runtime = LiveEntityRuntimeFixture.Create(world, new NullResources()); runtime.RegisterLiveEntity(Spawn(guid, sourceCell)); WorldEntity entity = runtime.MaterializeLiveEntity( guid, sourceCell, id => new WorldEntity { Id = id, ServerGuid = guid, SourceGfxObjOrSetupId = 0x02000001u, Position = Vector3.Zero, Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), })!; var identity = new LocalPlayerIdentityState { ServerGuid = guid }; var controllerSlot = new RuntimeLocalPlayerMovementState { Controller = new PlayerMovementController(new PhysicsEngine()), }; controllerSlot.Controller.SeedPlacementForTest( Vector3.Zero, sourceCell, Vector3.Zero); // C4 route 3: reproduce the canonical Runtime commit's pre-state // (body/cell already resolved) - Place() no longer resolves it. var destinationPosition = new Vector3(12f, 24f, 6f); controllerSlot.Controller.SeedPlacementForTest( destinationPosition, destinationCell, destinationPosition); var cameras = new ChaseCameraInputState { Legacy = new ChaseCamera(), Retail = new RetailChaseCamera(), }; LiveEntityRecord? recordAtReconcile = null; var spatial = new FakeSpatialReconcile(() => { Assert.True(runtime.TryGetRecord(guid, out recordAtReconcile)); return new PlacementSnapshot( entity.Position, entity.ParentCellId ?? 0u, entity.Rotation, controllerSlot.Controller.Position, controllerSlot.Controller.CellId, controllerSlot.Controller.BodyOrientation); }); var placement = new LocalPlayerTeleportPlacement( runtime, identity, controllerSlot, new LocalPlayerPhysicsHostSlot(), cameras, spatial); placement.Place(Quaternion.Identity); Assert.NotNull(recordAtReconcile); uint resolvedDestinationCell = controllerSlot.Controller.CellId; Assert.Equal( destinationCell & 0xFFFF0000u, resolvedDestinationCell & 0xFFFF0000u); Assert.Equal(resolvedDestinationCell, recordAtReconcile.FullCellId); Assert.Equal( destinationCell & 0xFFFF0000u | 0xFFFFu, recordAtReconcile.CanonicalLandblockId); Assert.True(recordAtReconcile.IsSpatiallyProjected); Assert.True(recordAtReconcile.IsSpatiallyVisible); Assert.Contains(entity, world.Entities); Assert.Equal(resolvedDestinationCell, entity.ParentCellId); Assert.Equal(1, spatial.Count); } private static int Index(IReadOnlyList events, string prefix) { for (int i = 0; i < events.Count; i++) { if (events[i].StartsWith(prefix, StringComparison.Ordinal)) return i; } return int.MaxValue; } private static RuntimeTeleportDestination Position( uint cellId, ushort teleportSequence, float x, float y, float z) => new( EntityGuid: 0x50000001u, InstanceSequence: 1, PositionSequence: 1, TeleportSequence: teleportSequence, ForcePositionSequence: 1, Position: new Position( cellId, new Vector3(x, y, z), Quaternion.Identity)); private static WorldSession.EntitySpawn Spawn(uint guid, uint cell) => new WorldSession.EntitySpawn( Guid: guid, Position: new CreateObject.ServerPosition( cell, 0f, 0f, 0f, 1f, 0f, 0f, 0f), SetupTableId: 0x02000001u, AnimPartChanges: [], TextureChanges: [], SubPalettes: [], BasePaletteId: null, ObjScale: null, Name: "player", ItemType: null, MotionState: null, MotionTableId: null).WithConsistentPhysics(); private sealed class NullResources : ILiveEntityResourceLifecycle { public void Register(WorldEntity entity) { } public void Unregister(WorldEntity entity) { } } private sealed class FakeSpatialReconcile : ILiveSpatialReconcilePhase { private readonly Func _capture; public FakeSpatialReconcile(Func capture) => _capture = capture; public int Count; public PlacementSnapshot Snapshot; public void Reconcile() { Snapshot = _capture(); Count++; } } private readonly record struct PlacementSnapshot( Vector3 EntityPosition, uint EntityCell, Quaternion EntityRotation, Vector3 ControllerPosition, uint ControllerCell, Quaternion ControllerRotation); /// /// C4 route 3: the Place edge now drives a REAL /// portal arm /// against a REAL (bare, session-less) local-player canonical record - /// the class this file's LocalPlayerTeleportController fake /// wiring cannot substitute (it is a sealed Runtime type, not an /// interface). also performs the SAME /// upstream merge production runs before ever offering a destination /// (RuntimeEntityObjectLifetime.TryApplyPosition, called by /// LiveEntityNetworkUpdateController.OnPosition before /// OfferDestination) so BeginAcceptedPlacementCore's /// portal-vs-latest-cell gate sees the SAME destination the offer /// carries - exactly the production ordering, not a shortcut. /// private sealed class Harness { private const uint PlayerGuid = 0x50000001u; private const uint HomeCell = 0x20210001u; private ushort _mergePositionSequence = 1; public readonly FakeAuthority Authority = new(); public readonly FakeInput Input = new(); public readonly FakeMode Mode; public readonly FakeStreaming Streaming; public readonly FakePlacement Placement; public readonly FakeSession Session; public readonly FakePresentation Presentation; public readonly RuntimeWorldTransitState Transit; public readonly WorldRevealCoordinator Reveal; /// /// The live streaming window this harness's reveal barrier derives its /// outdoor radius from (#280). Assertions read this rather than a /// literal so the test cannot silently re-encode the value under test. /// public StreamingRevealWindow RevealWindow { get; set; } = new(NearRadius: 1, FarRadius: 1); public readonly LocalPlayerTeleportController Controller; public readonly RuntimeEntityObjectLifetime Lifetime; public readonly RuntimeAcceptedPositionDriveController AcceptedPositionDrive; /// /// A6/R4/A5 review fix (2026-08-05): exposed so tests can assert the /// REAL canonical body's resolved position/cell after a committed /// portal placement, not just Placement.Called — the property /// that let A6's superseded-teleport test lose its destination /// discriminator (which of two destinations actually placed). /// public readonly RuntimeLocalPlayerMovementState Movement; public IPreparedCollisionSource DiagnosticCollisionSource => new UnusedCollisionSource(); public Harness( int centerX = 0x20, int centerY = 0x21, bool worldReady = true, List? order = null) { order ??= new List(); Mode = new FakeMode(order); Streaming = new FakeStreaming(centerX, centerY, order); Placement = new FakePlacement(order); Session = new FakeSession(order); Presentation = new FakePresentation(order); Transit = new RuntimeWorldTransitState(order.Add); Reveal = new WorldRevealCoordinator( Transit, revealWindow: () => RevealWindow, isRenderNeighborhoodReady: (_, _, _) => worldReady, isSpawnCellReady: _ => worldReady, isTerrainNeighborhoodReady: (_, _) => worldReady, areCompositeTexturesReady: () => worldReady, prepareCompositeTextures: (_, _) => { }, invalidateCompositeTextures: () => { }, isSpawnClaimUnhydratable: _ => false, streaming: Streaming); var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() }; var heights = new byte[81]; Array.Fill(heights, (byte)5f); var heightTable = new float[256]; for (int i = 0; i < heightTable.Length; i++) heightTable[i] = i; engine.AddLandblock( HomeCell & 0xFFFF0000u, new TerrainSurface(heights, heightTable), Array.Empty(), Array.Empty(), worldOffsetX: 0f, worldOffsetY: 0f); Lifetime = new RuntimeEntityObjectLifetime(engine); // The bare AddLandblock above only seeds the ENGINE's terrain - // TryPrepareAndSubmitAuthoredPlacement's DeferredCell park keys // off the SEPARATE collision-GENERATION ledger // (RuntimeSetPositionState.BeginCollisionGeneration/ // CommitCollisionGeneration), which a bare AddLandblock never // touches. Commit it too, or every placement parks forever. Lifetime.Physics.SetPosition.BeginCollisionGeneration( HomeCell & 0xFFFF0000u, 1UL); Lifetime.Physics.SetPosition.CommitCollisionGeneration( HomeCell & 0xFFFF0000u, 1UL, ready: true); Lifetime.Physics.ObserveLocalWorldFrame(HomeCell, teleportAdvanced: false); Lifetime.BindEventContext( static () => new RuntimeGenerationToken(1UL), static () => 1UL); Movement = new RuntimeLocalPlayerMovementState(); RuntimeLocalPlayerMovementState movement = Movement; var identity = new RuntimeLocalPlayerIdentityState { ServerGuid = PlayerGuid, }; movement.AttachPhysicsPublication(new RuntimeLocalPlayerPhysicsPublicationState( Lifetime.Entities, Lifetime.Physics, movement, identity)); Lifetime.LocalPlayerFirstEntry.BindPublication(movement.PhysicsPublication); Lifetime.BindLiveInputs(() => false, () => movement.Controller?.Position); var clock = new GameRuntimeClock(); // C4 route 3 fixture note: RuntimeFirstEntryDriveController's // ctor subscribes to RuntimeEntityObjectLifetime // .BindInitialResidenceBeginNotification - it must exist BEFORE // RegisterEntityWithInitialResidence runs, or it misses the // notification and never learns the residence exists at all // (production constructs it once per host session, ahead of // every Create). var firstEntry = new RuntimeFirstEntryDriveController( Lifetime, clock, new UnusedCollisionSource(), () => PlayerMovementConstructionOptions.Fallback, static _ => new RuntimeLocalPlayerPhysicsActivationPreparation( 0.48f, 1.835f, RuntimeLocalPlayerShadowDisposition.ProvenShapeless)); // Production's OnSpawned shape: RegisterEntityWithInitialResidence // then ApplyAcceptedSpawn (RuntimeLiveEntitySessionController.cs). RuntimeEntityRegistrationResult registration = Lifetime.RegisterEntityWithInitialResidence( Spawn(PlayerGuid, HomeCell), isLocalPlayer: true); RuntimeEntityRecord canonical = registration.Canonical ?? throw new InvalidOperationException( "fixture failed to register the local player"); Lifetime.ApplyAcceptedSpawn( canonical, canonical.CreateIntegrationVersion, canonical.Snapshot, replaceGeneration: registration.Inbound.Disposition is CreateObjectTimestampDisposition.NewGeneration); for (int attempt = 0; attempt < 8 && firstEntry.PendingCount != 0; attempt++) { firstEntry.DriveAll(); DrainPlacementFifo(); } Assert.Equal(0, firstEntry.PendingCount); Assert.True( Lifetime.Entities.TryGetActive(PlayerGuid, out RuntimeEntityRecord seeded), "fixture failed to register the local player"); Assert.False( Lifetime.TryGetInitialCreateResidence(seeded, out _), "fixture left the local player's initial-create residence open"); AcceptedPositionDrive = new RuntimeAcceptedPositionDriveController( Lifetime, clock, new UnusedCollisionSource(), new LocalPlayerOutboundController((_, _, _, _, _, _) => { }), () => new RuntimeGenerationToken(1UL), () => PlayerGuid, () => movement.Controller, () => false, () => null, () => movement); Controller = new LocalPlayerTeleportController( Authority, Input, Mode, Streaming, Transit, Reveal, Placement, Session, Presentation, AcceptedPositionDrive); } /// /// Merges the destination into the SAME canonical record the portal /// arm reads from, exactly like production's upstream merge, then /// offers it to the controller. /// public void OfferDestination( RuntimeTeleportDestination destination, bool teleportTimestampAdvanced) { _mergePositionSequence++; var update = new WorldSession.EntityPositionUpdate( destination.EntityGuid, 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: _mergePositionSequence, TeleportSequence: destination.TeleportSequence, ForcePositionSequence: 0); Lifetime.TryApplyPosition( update, isLocalPlayer: true, forcePositionRotation: Quaternion.Identity, currentLocalVelocity: Vector3.Zero, acknowledgeProjection: null, out _, out _, out _); Controller.OfferDestination(destination, teleportTimestampAdvanced); } /// /// B1 review fix test support (2026-08-05): merges an ORDINARY /// accepted Position for the local player — no portal offer, no /// transit involvement — the same /// RuntimeEntityObjectLifetime.TryApplyPosition call every /// real inbound ACE broadcast makes. Its doc names this the /// mechanism that Forgets any outstanding SetPosition /// operation for the entity, including a DeferredCell park — "the /// exact far-destination case the park exists to serve". /// public void MergeOrdinaryPosition( Vector3 position, uint cellId, ushort teleportSequence) { _mergePositionSequence++; var update = new WorldSession.EntityPositionUpdate( PlayerGuid, new CreateObject.ServerPosition( cellId, position.X, position.Y, position.Z, 1f, 0f, 0f, 0f), Velocity: null, PlacementId: null, IsGrounded: true, InstanceSequence: 1, PositionSequence: _mergePositionSequence, // The SAME already-accepted teleport sequence, not a // regression - an ordinary ACE broadcast Apply during an // active teleport still carries it unchanged; only a NEW // portal advances it. TeleportSequence: teleportSequence, ForcePositionSequence: 0); Lifetime.TryApplyPosition( update, isLocalPlayer: true, forcePositionRotation: Quaternion.Identity, currentLocalVelocity: Vector3.Zero, acknowledgeProjection: null, out _, out _, out _); } /// /// Commits a SECOND landblock's collision so a cross-landblock /// portal destination can resolve Committed instead of /// parking DeferredCell - the constructor's own home /// landblock is the only one committed by default. /// public void CommitLandblockCollision(uint landblockId) { var heights = new byte[81]; Array.Fill(heights, (byte)5f); var heightTable = new float[256]; for (int i = 0; i < heightTable.Length; i++) heightTable[i] = i; Lifetime.Physics.SetPosition.BeginCollisionGeneration(landblockId, 1UL); Lifetime.Physics.Engine.AddLandblock( landblockId, new TerrainSurface(heights, heightTable), Array.Empty(), Array.Empty(), worldOffsetX: 0f, worldOffsetY: 0f); Lifetime.Physics.SetPosition.CommitCollisionGeneration( landblockId, 1UL, ready: true); } /// /// Registers a minimal synthetic indoor cell so a portal destination /// whose low word is >= 0x0100 can resolve Committed on its /// FIRST placement attempt instead of parking DeferredCell /// forever. treats every /// outdoor cell (low word < 0x0100) as trivially ready, but an /// indoor cell requires DataCache.GetCellStruct to resolve a /// real entry - the bare fixture's AddLandblock calls only /// ever pass an empty CellSurface list, so without this an /// indoor destination cell is permanently un-spawnable regardless of /// how many times collision generation is committed for its /// landblock. Mirrors /// RuntimeSetPositionStateTests.AddSyntheticCell. /// public void AddSyntheticIndoorCell(uint envCellId) { Lifetime.Physics.DataCache.RegisterCellStructForTest( envCellId, new CellPhysics { WorldTransform = Matrix4x4.Identity, InverseWorldTransform = Matrix4x4.Identity, Resolved = new Dictionary(), Portals = [new PortalInfo(0, 0, 0)], CellBSP = new DatReaderWriter.Types.CellBSPTree { Root = new DatReaderWriter.Types.CellBSPNode { Type = BSPNodeType.Leaf, }, }, }); Lifetime.Physics.DataCache.CellGraph.Add( new AcDream.Core.World.Cells.EnvCell( envCellId, Matrix4x4.Identity, Matrix4x4.Identity, Vector3.Zero, Vector3.One, Array.Empty(), Array.Empty(), seenOutside: false, containmentBsp: null)); } public void DrainPlacementFifo() { while (Lifetime.Physics.SetPosition.TryPeekProjection( out RuntimePlacementProjectionSnapshot head)) { if (!Lifetime.Physics.SetPosition.AcknowledgeProjection(head.Token)) break; } } private static WorldSession.EntitySpawn Spawn(uint guid, uint cell) { var position = new CreateObject.ServerPosition( cell, 10f, 10f, 5f, 1f, 0f, 0f, 0f); var timestamps = new PhysicsTimestamps( Position: 1, Movement: 1, State: 1, Vector: 1, Teleport: 0, ServerControlledMove: 1, ForcePosition: 0, ObjDesc: 1, Instance: 1); var physics = new PhysicsSpawnData( RawState: (uint)PhysicsStateFlags.ReportCollisions, Position: position, Movement: null, AnimationFrame: null, SetupTableId: null, MotionTableId: null, SoundTableId: null, PhysicsScriptTableId: null, Parent: null, Children: null, Scale: null, Friction: null, Elasticity: null, Translucency: null, Velocity: null, Acceleration: null, AngularVelocity: null, DefaultScriptType: null, DefaultScriptIntensity: null, Timestamps: timestamps); return new WorldSession.EntitySpawn( guid, position, null, [], [], [], null, null, "teleport-controller-fixture", null, null, null, PhysicsState: physics.RawState, InstanceSequence: 1, MovementSequence: 1, ServerControlSequence: 1, PositionSequence: 1, Physics: physics); } private sealed class UnusedCollisionSource : IPreparedCollisionSource { public PreparedAssetPresence ProbeCollision( AcDream.Content.Pak.PakAssetType type, uint sourceFileId) => PreparedAssetPresence.Available; public PreparedCollisionReadResult ReadSetupCollision( uint sourceFileId, CancellationToken cancellationToken = default) => PreparedCollisionReadResult.Missing; public PreparedCollisionReadResult ReadGfxObjCollision( uint sourceFileId, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public PreparedCollisionReadResult ReadCellStructureCollision( uint sourceFileId, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public PreparedCollisionReadResult ReadEnvCellTopology( uint sourceFileId, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public PreparedCollisionSourceStats CollisionStats => default; public void Dispose() { } } } private sealed class FakeAuthority : ILocalPlayerTeleportAuthority { public bool IsFresh = true; public bool IsFreshStart(ushort sequence) => IsFresh; } private sealed class FakeInput : ILocalPlayerTeleportInputLifetime { public int EndCount; public void EndMouseLook() => EndCount++; } private sealed class FakeMode : ILocalPlayerTeleportModeOperations { private readonly List _order; public FakeMode(List order) { _order = order; Controller = new PlayerMovementController(new PhysicsEngine()); Controller.SeedPlacementForTest(Vector3.Zero, 0x20210001u, Vector3.Zero); } public PlayerMovementController? Controller { get; set; } public Matrix4x4 Projection => Matrix4x4.Identity; public int EnterPortalCount; public Func? RebuildOnEnter; public bool TryEnterPortalSpace() { EnterPortalCount++; Controller ??= RebuildOnEnter?.Invoke(); if (Controller is null) return false; Controller.State = PlayerState.PortalSpace; return true; } public void EnterWorld() { _order.Add("enter-world"); if (Controller is { } controller) controller.State = PlayerState.InWorld; } } private sealed class FakeStreaming : ILocalPlayerTeleportStreamingOperations, IWorldRevealStreamingScheduler { private readonly List _order; public FakeStreaming(int centerX, int centerY, List order) { CenterX = centerX; CenterY = centerY; _order = order; } public int CenterX { get; } public int CenterY { get; } public bool IsRecenterPending => RecenterPending; public bool RecenterPending; public bool ResetResult = true; public bool LastResetWasSessionEnding; public readonly List<(int X, int Y, bool Sealed)> Recenters = new(); public readonly List<(long Generation, uint Cell, int Radius)> Reservations = new(); public readonly List ReservationEnds = new(); public bool BeginRecenter(int x, int y, bool isSealedDungeon) { Recenters.Add((x, y, isSealedDungeon)); return !RecenterPending; } public bool ResetRecenter(bool sessionEnding) { LastResetWasSessionEnding = sessionEnding; return ResetResult; } public bool IsSealedDungeon(uint cellId) => (cellId & 0xFFFFu) >= 0x0100u; public void BeginDestinationReservation( long revealGeneration, uint destinationCell, int requiredRenderRadius) => Reservations.Add( (revealGeneration, destinationCell, requiredRenderRadius)); public void EndDestinationReservation(long revealGeneration) { ReservationEnds.Add(revealGeneration); _order.Add("reservation-end"); } } /// /// C4 route 3: acknowledge-only per D-T4 - no position/cell arguments /// (the canonical Runtime commit already resolved them; this suffix only /// runs on a COMMITTED receipt, never authors one). /// stands in for the deleted Position/CellId capture: it is /// the fact that matters now (whether the arm committed and the Place /// edge acknowledged it), never a value this fake could plausibly /// mis-record. /// private sealed class FakePlacement : ILocalPlayerTeleportPlacement { private readonly List _order; public FakePlacement(List order) => _order = order; public bool Called; public Quaternion Rotation; public Action? OnPlace; public void Place(Quaternion rotation) { _order.Add("placement"); Called = true; Rotation = rotation; OnPlace?.Invoke(); } } private sealed class FakeSession : ILocalPlayerTeleportSession { private readonly List _order; public FakeSession(List order) => _order = order; public int LoginCompleteCount; public Action? OnSend; public void SendLoginComplete() { _order.Add("login-complete"); LoginCompleteCount++; OnSend?.Invoke(); } } private sealed class FakeNetworkSink : ILocalPlayerTeleportNetworkSink { public List Starts { get; } = []; public void OnTeleportStarted(uint sequence) => Starts.Add(sequence); public void OfferDestination( RuntimeTeleportDestination destination, bool teleportTimestampAdvanced) { } public void ResetSession() { } public void ResetGenerationPresentation() { } } private sealed class FakePresentation : ILocalPlayerTeleportPresentation { private readonly List _order; private readonly Queue> _events = new(); public FakePresentation(List order) => _order = order; public bool IsPortalViewportVisible { get; private set; } public int CurrentTunnelFrame => 72; public Matrix4x4? BeginProjection; public bool EmitPlaceWhenReady; public readonly List WorldReadyValues = new(); public readonly List WaitCueValues = new(); public int DisposeFailuresRemaining; public int DisposeCount; public void Enqueue(params TeleportAnimEvent[] events) => _events.Enqueue(events); public void Begin(Matrix4x4 projection) { BeginProjection = projection; _order.Add("presentation-begin"); } public (TeleportAnimSnapshot Snapshot, IReadOnlyList Events) Tick(float deltaSeconds, bool worldReady) { WorldReadyValues.Add(worldReady); if (_events.Count > 0) return (default, _events.Dequeue()); if (EmitPlaceWhenReady && worldReady) { EmitPlaceWhenReady = false; return (default, new[] { TeleportAnimEvent.Place }); } return (default, Array.Empty()); } public void TickTunnel(float deltaSeconds) => _order.Add("tunnel-tick"); public void EnterTunnel() => IsPortalViewportVisible = true; public void ExitTunnel() => IsPortalViewportVisible = false; public void SetWaitCue(bool visible) => WaitCueValues.Add(visible); public void Reset() { _order.Add("presentation-reset"); IsPortalViewportVisible = false; BeginProjection = null; _events.Clear(); } public Matrix4x4 ApplyViewPlane(Matrix4x4 projection) => projection; public ICamera ApplyViewPlane(ICamera camera) => camera; public void DrawPortalViewport(int width, int height, Matrix4x4 projection) { } public void Dispose() { DisposeCount++; if (DisposeFailuresRemaining-- > 0) throw new InvalidOperationException("injected disposal failure"); } } }