diff --git a/src/AcDream.App/Streaming/StreamingController.cs b/src/AcDream.App/Streaming/StreamingController.cs index 67ed631..c320429 100644 --- a/src/AcDream.App/Streaming/StreamingController.cs +++ b/src/AcDream.App/Streaming/StreamingController.cs @@ -79,7 +79,7 @@ public sealed class StreamingController } else if (_region.CenterX != observerCx || _region.CenterY != observerCy) { - var diff = _region.RecenterTo(observerCx, observerCy); + var diff = _region.RecenterToSingleTier(observerCx, observerCy); foreach (var id in diff.ToLoad) _enqueueLoad(id); foreach (var id in diff.ToUnload) _enqueueUnload(id); } diff --git a/src/AcDream.App/Streaming/StreamingRegion.cs b/src/AcDream.App/Streaming/StreamingRegion.cs index bcebe44..7d0fc57 100644 --- a/src/AcDream.App/Streaming/StreamingRegion.cs +++ b/src/AcDream.App/Streaming/StreamingRegion.cs @@ -87,13 +87,47 @@ public sealed class StreamingRegion internal static uint EncodeLandblockId(int lbX, int lbY) => ((uint)lbX << 24) | ((uint)lbY << 16) | 0xFFFFu; + /// + /// First-tick bootstrap: emit ToLoadNear for every LB in the inner ring, + /// ToLoadFar for every LB in the outer ring (between near and far). Used + /// by on the first call before any + /// RecenterTo. + /// + public TwoTierDiff ComputeFirstTickDiff() + { + var near = new List(); + var far = new List(); + for (int dx = -FarRadius; dx <= FarRadius; dx++) + { + for (int dy = -FarRadius; dy <= FarRadius; dy++) + { + int nx = CenterX + dx; + int ny = CenterY + dy; + if (nx < 0 || nx > 0xFF || ny < 0 || ny > 0xFF) continue; + int absDx = System.Math.Abs(dx); + int absDy = System.Math.Abs(dy); + var id = EncodeLandblockId(nx, ny); + if (absDx <= NearRadius && absDy <= NearRadius) + near.Add(id); + else + far.Add(id); + } + } + return new TwoTierDiff( + ToLoadFar: far, + ToLoadNear: near, + ToPromote: System.Array.Empty(), + ToDemote: System.Array.Empty(), + ToUnload: System.Array.Empty()); + } + /// /// Recompute the visible window around a new center and return the /// delta vs. the previous state. Hysteresis: landblocks aren't unloaded /// until they're further than Radius + 2 from the new center, /// so boundary crossings don't thrash. /// - public RegionDiff RecenterTo(int newCx, int newCy) + public RegionDiff RecenterToSingleTier(int newCx, int newCy) { // Snapshot the old resident set so we can diff against it. var oldResident = new HashSet(_resident); diff --git a/tests/AcDream.Core.Tests/Streaming/StreamingRegionTests.cs b/tests/AcDream.Core.Tests/Streaming/StreamingRegionTests.cs index 741ea2b..899291e 100644 --- a/tests/AcDream.Core.Tests/Streaming/StreamingRegionTests.cs +++ b/tests/AcDream.Core.Tests/Streaming/StreamingRegionTests.cs @@ -36,7 +36,7 @@ public class StreamingRegionTests { var region = new StreamingRegion(cx: 50, cy: 50, radius: 2); - var diff = region.RecenterTo(50, 50); + var diff = region.RecenterToSingleTier(50, 50); Assert.Empty(diff.ToLoad); Assert.Empty(diff.ToUnload); @@ -52,7 +52,7 @@ public class StreamingRegionTests // 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); + var diff = region.RecenterToSingleTier(51, 50); Assert.Equal(5, diff.ToLoad.Count); Assert.Empty(diff.ToUnload); @@ -71,7 +71,7 @@ public class StreamingRegionTests // 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); + var diff = region.RecenterToSingleTier(53, 50); Assert.Equal(15, diff.ToLoad.Count); Assert.Equal(5, diff.ToUnload.Count); @@ -82,7 +82,7 @@ public class StreamingRegionTests { var region = new StreamingRegion(cx: 50, cy: 50, radius: 2); - var diff = region.RecenterTo(200, 200); + var diff = region.RecenterToSingleTier(200, 200); Assert.Equal(25, diff.ToLoad.Count); Assert.Equal(25, diff.ToUnload.Count); diff --git a/tests/AcDream.Core.Tests/Streaming/StreamingRegionTwoTierTests.cs b/tests/AcDream.Core.Tests/Streaming/StreamingRegionTwoTierTests.cs index 65df093..105b224 100644 --- a/tests/AcDream.Core.Tests/Streaming/StreamingRegionTwoTierTests.cs +++ b/tests/AcDream.Core.Tests/Streaming/StreamingRegionTwoTierTests.cs @@ -20,4 +20,18 @@ public class StreamingRegionTwoTierTests // becomes (NearRadius+2) for the far-tier unload, which is wrong. Assert.Equal(region.FarRadius, region.Radius); } + + [Fact] + public void RecenterTo_FirstTick_SplitsLoadIntoNearAndFar() + { + // near=1, far=3 → near window is 3×3=9, far window is 7×7-3×3=40 LBs. + var region = new StreamingRegion(centerX: 100, centerY: 100, nearRadius: 1, farRadius: 3); + var diff = region.ComputeFirstTickDiff(); + + Assert.Equal(9, diff.ToLoadNear.Count); + Assert.Equal(40, diff.ToLoadFar.Count); + Assert.Empty(diff.ToPromote); + Assert.Empty(diff.ToDemote); + Assert.Empty(diff.ToUnload); + } }