diff --git a/src/AcDream.App/Streaming/StreamingRegion.cs b/src/AcDream.App/Streaming/StreamingRegion.cs index 0bef84c..b28b547 100644 --- a/src/AcDream.App/Streaming/StreamingRegion.cs +++ b/src/AcDream.App/Streaming/StreamingRegion.cs @@ -21,10 +21,18 @@ public sealed class StreamingRegion private readonly HashSet _resident = new(); /// - /// Landblock IDs (8.8 coordinate form: (lbX << 24) | (lbY << 16) | 0xFFFE) - /// in the current visible window. This is strictly the (2r+1)×(2r+1) set; - /// it does NOT include hysteresis-retained landblocks outside the window. - /// Use to enumerate everything actually loaded. + /// Landblock IDs in the current visible window in the AC 8.8 coordinate + /// form: (lbX << 24) | (lbY << 16) | 0xFFFF. The trailing + /// 0xFFFF selects the LandBlock dat (terrain heightmap); the + /// matching LandBlockInfo (static-object metadata) is at 0xFFFE + /// on the same coordinates and is resolved internally by + /// LandblockLoader. + /// + /// + /// This set is strictly the (2r+1)×(2r+1) window; it does NOT include + /// hysteresis-retained landblocks outside the window. Use + /// to enumerate everything actually loaded. + /// /// public IReadOnlyCollection Visible => _visible; @@ -61,8 +69,17 @@ public sealed class StreamingRegion _resident.UnionWith(_visible); } + /// + /// Encode a landblock at (lbX, lbY) into the AC dat id form. Always uses + /// the 0xFFFF terminator (LandBlock = terrain). The earlier + /// version of this method used 0xFFFE by mistake — that's the + /// LandBlockInfo id, and asking LandblockLoader.Load to read a + /// LandBlock at the LandBlockInfo coords corrupts the dat reader's + /// buffer position, returning a half-populated LandBlock.Height[] + /// array which renders as wildly distorted "ball with spikes" terrain. + /// internal static uint EncodeLandblockId(int lbX, int lbY) - => ((uint)lbX << 24) | ((uint)lbY << 16) | 0xFFFEu; + => ((uint)lbX << 24) | ((uint)lbY << 16) | 0xFFFFu; /// /// Recompute the visible window around a new center and return the diff --git a/tests/AcDream.Core.Tests/Streaming/StreamingRegionTests.cs b/tests/AcDream.Core.Tests/Streaming/StreamingRegionTests.cs index d65b480..741ea2b 100644 --- a/tests/AcDream.Core.Tests/Streaming/StreamingRegionTests.cs +++ b/tests/AcDream.Core.Tests/Streaming/StreamingRegionTests.cs @@ -103,11 +103,17 @@ public class StreamingRegionTests [Fact] public void Constructor_SmallRadius_IDsMatchEncodingRule() { - // Verify EncodeLandblockId is correct (not swapped shifts). - // radius=0 at (0x12, 0x34) → exactly one entry, id = 0x1234FFFE. + // 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(0x1234FFFEu, region.Visible); + Assert.Contains(0x1234FFFFu, region.Visible); } }