diff --git a/src/AcDream.Core/World/WorldView.cs b/src/AcDream.Core/World/WorldView.cs new file mode 100644 index 0000000..50968dc --- /dev/null +++ b/src/AcDream.Core/World/WorldView.cs @@ -0,0 +1,55 @@ +// src/AcDream.Core/World/WorldView.cs +using DatReaderWriter; + +namespace AcDream.Core.World; + +public sealed class WorldView +{ + public uint CenterLandblockId { get; } + public IReadOnlyList Landblocks { get; } + public IEnumerable AllEntities => Landblocks.SelectMany(lb => lb.Entities); + + private WorldView(uint centerLandblockId, IReadOnlyList landblocks) + { + CenterLandblockId = centerLandblockId; + Landblocks = landblocks; + } + + /// + /// Load the 3x3 grid of landblocks around . + /// Missing neighbors (edges of the world or absent from the cell dat) are silently skipped. + /// + public static WorldView Load(DatCollection dats, uint centerLandblockId) + { + var loaded = new List(); + foreach (var id in NeighborLandblockIds(centerLandblockId)) + { + var lb = LandblockLoader.Load(dats, id); + if (lb is not null) + loaded.Add(lb); + } + return new WorldView(centerLandblockId, loaded); + } + + /// + /// Enumerate the 3x3 neighbor landblock ids around a center. Clamps at the world edges + /// (skipping neighbors that would underflow or overflow the 8-bit coordinate range). + /// + public static IEnumerable NeighborLandblockIds(uint centerLandblockId) + { + int cx = (int)((centerLandblockId >> 24) & 0xFFu); + int cy = (int)((centerLandblockId >> 16) & 0xFFu); + + for (int dy = -1; dy <= 1; dy++) + { + for (int dx = -1; dx <= 1; dx++) + { + int nx = cx + dx; + int ny = cy + dy; + if (nx < 0 || nx > 0xFF || ny < 0 || ny > 0xFF) + continue; + yield return (uint)((nx << 24) | (ny << 16) | 0xFFFFu); + } + } + } +} diff --git a/tests/AcDream.Core.Tests/World/WorldViewTests.cs b/tests/AcDream.Core.Tests/World/WorldViewTests.cs new file mode 100644 index 0000000..7badf04 --- /dev/null +++ b/tests/AcDream.Core.Tests/World/WorldViewTests.cs @@ -0,0 +1,46 @@ +// tests/AcDream.Core.Tests/World/WorldViewTests.cs +using AcDream.Core.World; + +namespace AcDream.Core.Tests.World; + +public class WorldViewTests +{ + [Fact] + public void NeighborIds_Center_Returns9Ids() + { + var ids = WorldView.NeighborLandblockIds(0xA9B4FFFFu).ToList(); + + Assert.Equal(9, ids.Count); + Assert.Contains(0xA9B4FFFFu, ids); // center + Assert.Contains(0xA8B3FFFFu, ids); // NW + Assert.Contains(0xAAB5FFFFu, ids); // SE + } + + [Fact] + public void NeighborIds_LowerEdge_ClampsUnderflow() + { + // Landblock 0x0000FFFF — no west or south neighbors. + var ids = WorldView.NeighborLandblockIds(0x0000FFFFu).ToList(); + + // 4 neighbors should exist: center + E + N + NE + Assert.Equal(4, ids.Count); + Assert.Contains(0x0000FFFFu, ids); + Assert.Contains(0x0100FFFFu, ids); + Assert.Contains(0x0001FFFFu, ids); + Assert.Contains(0x0101FFFFu, ids); + } + + [Fact] + public void NeighborIds_UpperEdge_ClampsOverflow() + { + // Landblock 0xFFFFFFFF — no east or north neighbors. + var ids = WorldView.NeighborLandblockIds(0xFFFFFFFFu).ToList(); + + // 4 neighbors: center + W + S + SW + Assert.Equal(4, ids.Count); + Assert.Contains(0xFFFFFFFFu, ids); + Assert.Contains(0xFEFFFFFFu, ids); + Assert.Contains(0xFFFEFFFFu, ids); + Assert.Contains(0xFEFEFFFFu, ids); + } +}