using AcDream.App.Streaming; using Xunit; namespace AcDream.Core.Tests.Streaming; public class StreamingRegionTests { [Fact] public void Constructor_Radius2_Produces25Landblocks() { var region = new StreamingRegion(cx: 50, cy: 50, radius: 2); Assert.Equal(25, region.Visible.Count); } [Fact] public void Constructor_NearOrigin_ClampsToWorldEdge() { // Center at (0, 0) with radius 2: only the +X / +Y quadrant is // in-bounds. That's a 3×3 subset of the 5×5 window = 9 landblocks. var region = new StreamingRegion(cx: 0, cy: 0, radius: 2); Assert.Equal(9, region.Visible.Count); } [Fact] public void Constructor_NearFarEdge_ClampsToWorldEdge() { var region = new StreamingRegion(cx: 0xFF, cy: 0xFF, radius: 2); Assert.Equal(9, region.Visible.Count); } [Fact] public void RecenterTo_SamePosition_EmptyDiff() { var region = new StreamingRegion(cx: 50, cy: 50, radius: 2); var diff = region.RecenterTo(50, 50); Assert.Empty(diff.ToLoad); Assert.Empty(diff.ToUnload); } [Fact] public void RecenterTo_SingleStepEast_LoadsColumn_NoUnloadsDueToHysteresis() { // Radius 2 → unload threshold is radius+2 = 4. // Starting center (50,50) covers X in [48..52]. Step to (51,50): // new coverage X in [49..53]. New column is x=53 (5 entries). // Departing column x=48 is at distance |48-51| = 3, which is within // the radius+2 threshold, so it stays loaded (hysteresis keeps radius+2). var region = new StreamingRegion(cx: 50, cy: 50, radius: 2); var diff = region.RecenterTo(51, 50); Assert.Equal(5, diff.ToLoad.Count); Assert.Empty(diff.ToUnload); // Visible is strictly the 5×5 window around (51, 50). Assert.Equal(25, region.Visible.Count); // Resident includes the hysteresis-retained x=48 column (5 entries) // plus the full new window, for 30 total. Assert.Equal(30, region.Resident.Count); } [Fact] public void RecenterTo_ThreeStepEast_LoadsAndUnloadsColumns() { // Starting (50,50) covers X in [48..52]. Step to (53,50): // new coverage X in [51..55]. New columns: x=53,54,55 (15 entries). // x=48 is now 5 away, > radius+2 = 4 → unload. x=49 is 4 away, not > 4 → keep. x=50 is 3 away, not > 4 → keep. var region = new StreamingRegion(cx: 50, cy: 50, radius: 2); var diff = region.RecenterTo(53, 50); Assert.Equal(15, diff.ToLoad.Count); Assert.Equal(5, diff.ToUnload.Count); } [Fact] public void RecenterTo_LongTeleport_UnloadsEverythingLoadsEverything() { var region = new StreamingRegion(cx: 50, cy: 50, radius: 2); var diff = region.RecenterTo(200, 200); Assert.Equal(25, diff.ToLoad.Count); Assert.Equal(25, diff.ToUnload.Count); } [Fact] public void Constructor_NearXEdgeOnly_ClampsOnlyXAxis() { // cx=0, cy=50, radius=2: // X is clamped to [0..2] (3 entries) // Y is unclamped [48..52] (5 entries) // Total = 3 × 5 = 15 landblocks. var region = new StreamingRegion(cx: 0, cy: 50, radius: 2); Assert.Equal(15, region.Visible.Count); } [Fact] public void Constructor_SmallRadius_IDsMatchEncodingRule() { // Verify EncodeLandblockId emits the LandBlock terminator (0xFFFF), // not the LandBlockInfo terminator (0xFFFE). The earlier version of // this test pinned 0xFFFE and codified the bug that produced "ball // of spikes" terrain in Phase A.1's first live run — LandblockLoader // was being asked to read the LandBlockInfo file as a LandBlock, // which corrupted the dat reader's buffer position and returned a // half-populated heightmap. radius=0 at (0x12, 0x34) → exactly one // entry, id = 0x1234FFFF. var region = new StreamingRegion(cx: 0x12, cy: 0x34, radius: 0); Assert.Single(region.Visible); Assert.Contains(0x1234FFFFu, region.Visible); } }