using System; using System.Collections.Generic; using System.Linq; using AcDream.App.Streaming; using AcDream.Core.World; using Xunit; namespace AcDream.Core.Tests.Streaming; /// /// The dungeon streaming gate (#133 FPS). AC dungeons have no adjacent /// landblocks (ACE LandblockManager.GetAdjacentIDs returns empty for a /// dungeon); they sit packed in the ocean grid, so the normal 25×25 window /// pulls in ~129 unrelated neighbor dungeons + their emitters. When the player /// is inside a sealed dungeon cell, Tick(insideDungeon: true) collapses /// streaming to the single dungeon landblock and unloads the neighbors. /// public class StreamingControllerDungeonGateTests { private static uint Encode(int x, int y) => ((uint)x << 24) | ((uint)y << 16) | 0xFFFFu; private static LoadedLandblock MakeLb(int x, int y) => new LoadedLandblock( Encode(x, y), Heightmap: null!, Entities: Array.Empty()); private sealed record Harness( StreamingController Ctrl, List<(uint Id, LandblockStreamJobKind Kind)> Loads, List Unloads, Func ClearCalls, GpuWorldState State); private static Harness Make() { var loads = new List<(uint, LandblockStreamJobKind)>(); var unloads = new List(); int clearCalls = 0; var state = new GpuWorldState(); var ctrl = new StreamingController( enqueueLoad: (id, kind) => loads.Add((id, kind)), enqueueUnload: unloads.Add, drainCompletions: _ => Array.Empty(), applyTerrain: (_, _) => { }, state: state, nearRadius: 4, farRadius: 12, clearPendingLoads: () => clearCalls++); return new Harness(ctrl, loads, unloads, () => clearCalls, state); } [Fact] public void EntersDungeon_CancelsPending_UnloadsNeighbors_KeepsCenter() { var h = Make(); uint center = Encode(0, 7); h.State.AddLandblock(MakeLb(0, 7)); // the dungeon landblock h.State.AddLandblock(MakeLb(0, 8)); // a neighbor ocean dungeon h.State.AddLandblock(MakeLb(1, 7)); // another neighbor h.Ctrl.Tick(observerCx: 0, observerCy: 7, insideDungeon: true); Assert.Equal(1, h.ClearCalls()); // in-flight window load cancelled Assert.Contains(Encode(0, 8), h.Unloads); // neighbor unloaded Assert.Contains(Encode(1, 7), h.Unloads); // neighbor unloaded Assert.DoesNotContain(center, h.Unloads); // dungeon landblock kept Assert.DoesNotContain(h.Loads, l => l.Id == center); // already loaded → no reload } [Fact] public void EntersDungeon_CenterNotLoaded_EnqueuesCenterLoad() { var h = Make(); // empty state — the dungeon landblock isn't resident yet h.Ctrl.Tick(observerCx: 0, observerCy: 7, insideDungeon: true); Assert.Equal(1, h.ClearCalls()); Assert.Contains(h.Loads, l => l.Id == Encode(0, 7) && l.Kind == LandblockStreamJobKind.LoadNear); } [Fact] public void StayingCollapsed_SweepsStragglerThatFinishedAfterTheEdge() { var h = Make(); h.State.AddLandblock(MakeLb(0, 7)); h.Ctrl.Tick(0, 7, insideDungeon: true); // collapse edge h.Unloads.Clear(); // A Load the worker had already dequeued before ClearLoads now completes. h.State.AddLandblock(MakeLb(0, 8)); h.Ctrl.Tick(0, 7, insideDungeon: true); // sweep Assert.Contains(Encode(0, 8), h.Unloads); Assert.DoesNotContain(Encode(0, 7), h.Unloads); } [Fact] public void StayingCollapsed_DoesNotReClearOrReloadCenter() { var h = Make(); h.State.AddLandblock(MakeLb(0, 7)); h.Ctrl.Tick(0, 7, insideDungeon: true); // collapse (clear #1) h.Loads.Clear(); h.Ctrl.Tick(0, 7, insideDungeon: true); // stay collapsed Assert.Equal(1, h.ClearCalls()); // clear only fired on the edge Assert.Empty(h.Loads); // no spurious center reloads } [Fact] public void Collapsed_CurrCellFlickersToAdjacentOffByOne_DoesNotExpand() { // Regression: the live run broke because a dungeon cell's negative local-Y // makes the position-derived observer landblock land one row off (0,7→0,6). // When CurrCell flickers null mid-frame, GameWindow stops overriding to the // cell landblock and passes that adjacent (0,6). The Chebyshev>1 guard must // treat that as a flicker and HOLD — never expand (which would unload the // real dungeon and re-stream the 25×25 neighbor window). var h = Make(); h.State.AddLandblock(MakeLb(0, 7)); h.Ctrl.Tick(0, 7, insideDungeon: true); // collapse onto the dungeon (0,7) h.Loads.Clear(); h.Unloads.Clear(); h.Ctrl.Tick(0, 6, insideDungeon: false); // flicker → adjacent off-by-one Assert.Empty(h.Loads); // NO full-window reload Assert.Empty(h.Unloads); // dungeon (0,7) preserved; nothing else resident } [Fact] public void ExitsDungeon_RebuildsFullWindow_UnloadsStaleDungeonLandblock() { var h = Make(); h.State.AddLandblock(MakeLb(0, 7)); h.Ctrl.Tick(0, 7, insideDungeon: true); // collapse h.Loads.Clear(); h.Unloads.Clear(); // Exit through a portal to an outdoor location far from the dungeon block. h.Ctrl.Tick(observerCx: 100, observerCy: 100, insideDungeon: false); Assert.Contains(h.Loads, l => l.Kind == LandblockStreamJobKind.LoadNear); Assert.Contains(h.Loads, l => l.Kind == LandblockStreamJobKind.LoadFar); Assert.Contains(Encode(0, 7), h.Unloads); // stale dungeon block, outside new window } [Fact] public void PreCollapse_BeforeAnyTick_LoadsOnlyDungeon_NeverBootstrapsWindow() { // #135: at a dungeon login/teleport we pre-collapse the instant we recenter, // BEFORE the first Tick. The full 25×25 neighbor window must NEVER be enqueued // — only the single dungeon landblock loads. var h = Make(); // empty state — nothing resident, _region is null h.Ctrl.PreCollapseToDungeon(0, 7); Assert.Single(h.Loads); // exactly one load Assert.Equal(Encode(0, 7), h.Loads[0].Id); // the dungeon landblock Assert.Equal(LandblockStreamJobKind.LoadNear, h.Loads[0].Kind); Assert.DoesNotContain(h.Loads, l => l.Kind == LandblockStreamJobKind.LoadFar); } [Fact] public void PreCollapse_AfterBootstrapTick_CancelsWindow_UnloadsResidentNeighbors_KeepsDungeon() { // The REAL runtime ordering at a dungeon login: the per-frame streaming Tick // runs FIRST and bootstraps the full 25×25 window, THEN the spawn handler fires // PreCollapseToDungeon. The pre-collapse must cancel the queued window loads // (_clearPendingLoads) and unload any neighbor that already finished streaming. var h = Make(); h.Ctrl.Tick(0, 7, insideDungeon: false); // frame 1: NormalTick bootstraps the window Assert.True(h.Loads.Count > 1); // the full window was enqueued // Simulate neighbor landblocks that finished loading during the bootstrap, // before the collapse edge. h.State.AddLandblock(MakeLb(0, 7)); // the dungeon landblock itself h.State.AddLandblock(MakeLb(0, 8)); // a neighbor ocean dungeon that loaded h.State.AddLandblock(MakeLb(1, 7)); // another neighbor h.Loads.Clear(); h.Unloads.Clear(); h.Ctrl.PreCollapseToDungeon(0, 7); Assert.Equal(1, h.ClearCalls()); // queued window loads cancelled Assert.Contains(Encode(0, 8), h.Unloads); // resident neighbor unloaded Assert.Contains(Encode(1, 7), h.Unloads); Assert.DoesNotContain(Encode(0, 7), h.Unloads); // dungeon landblock kept } [Fact] public void PreCollapse_ThenHoldTicksWithStaleObserver_StaysCollapsed() { // After pre-collapse the player is held (CurrCell still null → insideDungeon // false) while the dungeon hydrates. A stale observer that is the SAME dungeon // landblock must keep streaming collapsed — no full-window reload. var h = Make(); h.Ctrl.PreCollapseToDungeon(0, 7); h.Loads.Clear(); h.Unloads.Clear(); h.Ctrl.Tick(0, 7, insideDungeon: false); // hold frame: not placed yet Assert.Empty(h.Loads); // no neighbor window Assert.Empty(h.Unloads); } [Fact] public void PreCollapse_IsIdempotent_OnSameLandblock() { // A re-sent player spawn / a same-frame double call must not re-clear or // re-enqueue. var h = Make(); h.Ctrl.PreCollapseToDungeon(0, 7); h.Loads.Clear(); h.Ctrl.PreCollapseToDungeon(0, 7); Assert.Equal(1, h.ClearCalls()); // clear fired only on the first collapse Assert.Empty(h.Loads); // no second dungeon load } [Fact] public void PreCollapse_ThenPlaced_InsideDungeonTick_StaysCollapsed() { // When placement finally fires, the per-frame Tick(insideDungeon: true) sees // the same collapsed landblock and holds — no re-collapse churn. var h = Make(); h.State.AddLandblock(MakeLb(0, 7)); // dungeon landblock finished loading h.Ctrl.PreCollapseToDungeon(0, 7); h.Loads.Clear(); h.Unloads.Clear(); h.Ctrl.Tick(0, 7, insideDungeon: true); // placed: gate now fires Assert.Equal(1, h.ClearCalls()); // no second clear Assert.Empty(h.Loads); Assert.DoesNotContain(Encode(0, 7), h.Unloads); } [Fact] public void NormalOutdoorTick_Unchanged_NoCollapseNoClear() { var h = Make(); h.Ctrl.Tick(observerCx: 100, observerCy: 100); // default insideDungeon: false Assert.Equal(0, h.ClearCalls()); Assert.Empty(h.Unloads); // 9 near (9×9? no — nearRadius 4 → 9×9=81) + far ring loads enqueued. Assert.Contains(h.Loads, l => l.Kind == LandblockStreamJobKind.LoadNear); Assert.Contains(h.Loads, l => l.Kind == LandblockStreamJobKind.LoadFar); } }