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.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Core.World; 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(); WorldSession.EntityPositionUpdate destination = Position( cellId: 0x20210001u, teleportSequence: 7, x: 12f, y: 34f, z: 5f); harness.Controller.OfferDestination( destination, teleportTimestampAdvanced: true); Assert.Empty(harness.Streaming.PriorityWrites); 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((0x2021FFFFu, LocalPlayerTeleportController.NearRingRadius), Assert.Single(harness.Streaming.PriorityWrites)); Assert.True(harness.Reveal.Snapshot.IsActive); Assert.Equal(WorldRevealKind.Portal, harness.Reveal.Snapshot.Kind); harness.Controller.OfferDestination( destination, teleportTimestampAdvanced: false); Assert.Single(harness.Streaming.PriorityWrites); } [Fact] public void MissingPlayerController_KeepsStartAndDestinationPendingUntilProjectionExists() { var harness = new Harness(); harness.Mode.Controller = null; harness.Controller.OnTeleportStarted(2); harness.Controller.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.SetPosition( 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.PriorityWrites); } [Fact] public void ControllerWithdrawnAfterActivation_KeepsAcceptedDestinationUntilItReturns() { var harness = new Harness(); harness.Controller.OnTeleportStarted(14); Assert.True(harness.Controller.IsActive); harness.Mode.Controller = null; harness.Controller.OfferDestination( Position(0x20210001u, 14, 7f, 8f, 9f), teleportTimestampAdvanced: true); Assert.Equal(0u, harness.Controller.ActiveDestinationCell); Assert.Empty(harness.Streaming.PriorityWrites); harness.Mode.Controller = new PlayerMovementController(new PhysicsEngine()); harness.Mode.Controller.SetPosition(Vector3.Zero, 0x20210001u, Vector3.Zero); harness.Controller.Tick(0.016f); Assert.Equal(0x20210001u, harness.Controller.ActiveDestinationCell); Assert.Single(harness.Streaming.PriorityWrites); } [Fact] public void ControllerWithdrawnAfterAim_HoldsPresentationUntilModeRebuildsIt() { var harness = new Harness(); harness.Controller.OnTeleportStarted(15); harness.Controller.OfferDestination( Position(0x20210001u, 15, 7f, 8f, 9f), teleportTimestampAdvanced: true); harness.Mode.Controller = null; harness.Presentation.Enqueue(TeleportAnimEvent.Place); harness.Controller.Tick(0.016f); Assert.Equal(default, harness.Placement.Position); Assert.Empty(harness.Presentation.WorldReadyValues); harness.Mode.RebuildOnEnter = () => { var controller = new PlayerMovementController(new PhysicsEngine()); controller.SetPosition(Vector3.Zero, 0x20210001u, Vector3.Zero); return controller; }; harness.Controller.Tick(0.016f); Assert.NotNull(harness.Mode.Controller); Assert.Equal(new Vector3(7f, 8f, 9f), harness.Placement.Position); } [Fact] public void SameLandblockDestination_DoesNotRecenterAndKeepsTranslatedPosition() { var harness = new Harness(centerX: 0x20, centerY: 0x21); harness.Controller.OnTeleportStarted(3); harness.Controller.OfferDestination( Position(0x20210123u, 3, 20f, 30f, 4f), teleportTimestampAdvanced: true); harness.Presentation.EmitPlaceWhenReady = true; harness.Controller.Tick(0.016f); Assert.Empty(harness.Streaming.Recenters); Assert.Equal(new Vector3(20f, 30f, 4f), harness.Placement.Position); Assert.Equal(0x20210123u, harness.Placement.CellId); Assert.False(harness.Placement.Forced); } [Fact] public void CrossLandblockDestination_RecentersBeforeItCanBecomeReady() { var harness = new Harness(centerX: 0x20, centerY: 0x21); harness.Mode.Controller!.SetPosition( Vector3.Zero, 0x20210001u, Vector3.Zero); harness.Controller.OnTeleportStarted(4); harness.Streaming.RecenterPending = true; harness.Controller.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.Equal(default, harness.Placement.Position); harness.Streaming.RecenterPending = false; harness.Controller.Tick(0.016f); Assert.Equal(new Vector3(-2f, 8f, 9f), harness.Placement.Position); } [Fact] public void ReadinessHold_UsesWallClockAndForcesExactlyAtTenSeconds() { var harness = new Harness(worldReady: false); harness.Controller.OnTeleportStarted(5); harness.Controller.OfferDestination( Position(0x20210001u, 5, 1f, 2f, 3f), teleportTimestampAdvanced: true); harness.Presentation.EmitPlaceWhenReady = true; harness.Controller.Tick(9.5f); Assert.False(Assert.Single(harness.Presentation.WorldReadyValues)); Assert.Equal(default, harness.Placement.Position); harness.Controller.Tick(0.5f); Assert.True(harness.Presentation.WorldReadyValues[^1]); Assert.True(harness.Placement.Forced); Assert.Equal(new Vector3(1f, 2f, 3f), harness.Placement.Position); } [Fact] public void Place_ReconcilesInsidePlacementBeforeRevealMaterialized() { var order = new List(); var harness = new Harness(order: order); harness.Controller.OnTeleportStarted(8); harness.Controller.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.True(Index(order, "[world-reveal] event=materialized") < Index(order, "priority-clear")); Assert.Equal(1, harness.Reveal.PortalMaterializationCount); } [Fact] public void LoginComplete_EntersWorldThenSendsThenCompletesAndResets() { var order = new List(); var harness = new Harness(order: order); harness.Controller.OnTeleportStarted(9); harness.Controller.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); } [Fact] public void NewerStart_ReplacesOldDestinationWithoutReusingIt() { var harness = new Harness(); harness.Controller.OnTeleportStarted(10); harness.Controller.OfferDestination( Position(0x20210001u, 10, 1f, 1f, 1f), teleportTimestampAdvanced: true); harness.Controller.OnTeleportStarted(11); harness.Controller.Tick(LocalPlayerTeleportController.MaximumWorldHoldSeconds); Assert.Equal(default, harness.Placement.Position); Assert.Equal(0u, harness.Controller.ActiveDestinationCell); harness.Controller.OfferDestination( Position(0x20210001u, 11, 2f, 2f, 2f), teleportTimestampAdvanced: true); harness.Presentation.Enqueue(TeleportAnimEvent.Place); harness.Controller.Tick(0.016f); Assert.Equal(new Vector3(2f), harness.Placement.Position); } [Fact] public void SessionReset_ClearsTransitCancelsRevealAndRequiresRecenterConvergence() { var harness = new Harness(); harness.Controller.OnTeleportStarted(12); harness.Controller.OfferDestination( Position(0x20210001u, 12, 1f, 2f, 3f), teleportTimestampAdvanced: true); harness.Controller.ResetSession(); Assert.False(harness.Controller.IsActive); Assert.True(harness.Reveal.Snapshot.Cancelled); Assert.True(harness.Streaming.LastResetWasSessionEnding); var failed = new Harness(); failed.Streaming.ResetResult = false; Assert.Throws(() => failed.Controller.ResetSession()); } [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.Controller.OfferDestination( Position(0x20210001u, 20, 1f, 2f, 3f), teleportTimestampAdvanced: true); harness.Streaming.PriorityClearCount = 0; 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.Equal(1, harness.Streaming.PriorityClearCount); } [Fact] public void ReentrantNewStartDuringLoginComplete_PreservesReplacementLifetime() { var harness = new Harness(); harness.Controller.OnTeleportStarted(30); harness.Controller.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 = new LiveEntityRuntime(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 LocalPlayerControllerSlot { Controller = new PlayerMovementController(new PhysicsEngine()), }; controllerSlot.Controller.SetPosition(Vector3.Zero, cell, Vector3.Zero); var cameras = new ChaseCameraInputState { Legacy = new ChaseCamera(), Retail = new RetailChaseCamera(), }; var origin = new LiveWorldOriginState(); origin.SetPlaceholder(0x20, 0x21); 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( new PhysicsEngine(), runtime, identity, controllerSlot, new LocalPlayerPhysicsHostSlot(), cameras, origin, spatial); var position = new Vector3(12f, 24f, 6f); Quaternion rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.5f); placement.Place(position, cell, rotation, forced: false); 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); } 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 WorldSession.EntityPositionUpdate Position( uint cellId, ushort teleportSequence, float x, float y, float z) => new( 0x50000001u, new CreateObject.ServerPosition( cellId, x, y, z, 1f, 0f, 0f, 0f), null, null, true, 1, 1, teleportSequence, 1); private static WorldSession.EntitySpawn Spawn(uint guid, uint cell) => new( 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); 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); private sealed class Harness { 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 WorldRevealCoordinator Reveal; public readonly LocalPlayerTeleportController Controller; 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); Reveal = new WorldRevealCoordinator( isRenderNeighborhoodReady: (_, _) => worldReady, isSpawnCellReady: _ => worldReady, isTerrainNeighborhoodReady: (_, _) => worldReady, areCompositeTexturesReady: () => worldReady, prepareCompositeTextures: (_, _) => { }, invalidateCompositeTextures: () => { }, isSpawnClaimUnhydratable: _ => false, log: order.Add); Controller = new LocalPlayerTeleportController( Authority, Input, Mode, Streaming, Reveal, Placement, Session, Presentation); } } 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.SetPosition(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 { 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 int PriorityClearCount; public readonly List<(int X, int Y, bool Sealed)> Recenters = new(); public readonly List<(uint Landblock, int Radius)> PriorityWrites = 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 SetPriority(uint landblockId, int radius) => PriorityWrites.Add((landblockId, radius)); public void ClearPriority() { PriorityClearCount++; _order.Add("priority-clear"); } } private sealed class FakePlacement : ILocalPlayerTeleportPlacement { private readonly List _order; public FakePlacement(List order) => _order = order; public Vector3 Position; public uint CellId; public Quaternion Rotation; public bool Forced; public Action? OnPlace; public void Place(Vector3 position, uint cellId, Quaternion rotation, bool forced) { _order.Add("placement"); Position = position; CellId = cellId; Rotation = rotation; Forced = forced; 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( WorldSession.EntityPositionUpdate update, bool teleportTimestampAdvanced) { } public void ResetSession() { } } 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 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 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"); } } }