From b4c4318c8b9f580134552039be4db33b16c9bb39 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 2 Jun 2026 09:17:57 +0200 Subject: [PATCH] =?UTF-8?q?feat(core):=20UCG=20Stage=201=20=E2=80=94=20Lan?= =?UTF-8?q?dCell=20synthesized=20from=20TerrainSurface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Outdoor terrain cell (retail CLandCell) synthesized on demand from a landblock's TerrainSurface. Factory Synthesize() samples four quad corners to establish Z bounds; PointInCell() tests the 24 m XY quad in world-local space. BuildingCellId stub is null (Stage 2). 2/2 tests RED→GREEN. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/AcDream.Core/World/Cells/LandCell.cs | 55 +++++++++++++++++++ .../World/Cells/LandCellTests.cs | 36 ++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 src/AcDream.Core/World/Cells/LandCell.cs create mode 100644 tests/AcDream.Core.Tests/World/Cells/LandCellTests.cs diff --git a/src/AcDream.Core/World/Cells/LandCell.cs b/src/AcDream.Core/World/Cells/LandCell.cs new file mode 100644 index 0000000..f6d4f55 --- /dev/null +++ b/src/AcDream.Core/World/Cells/LandCell.cs @@ -0,0 +1,55 @@ +using System; +using System.Numerics; +using AcDream.Core.Physics; // TerrainSurface + +namespace AcDream.Core.World.Cells; + +/// +/// Outdoor terrain cell — synthesized on demand from a landblock's +/// (retail CLandCell is positionally resolved, not stored). +/// Retail anchor: CLandCell (acclient.h:31886) / CSortCell (acclient.h:31880). +/// +public sealed class LandCell : ObjCell +{ + public const float CellSize = 24f; // TerrainSurface 24 m cell + + public TerrainSurface Terrain { get; } + public int Cx { get; } + public int Cy { get; } + /// CSortCell building bridge ref (population logic is Stage 2). Always null in Stage 1. + public uint? BuildingCellId { get; } + + private readonly Vector3 _worldOrigin; + + private LandCell(uint id, TerrainSurface terrain, Vector3 worldOrigin, int cx, int cy, + Matrix4x4 worldTransform, Matrix4x4 inverseWorldTransform, + Vector3 localBoundsMin, Vector3 localBoundsMax) + : base(id, worldTransform, inverseWorldTransform, localBoundsMin, localBoundsMax, + Array.Empty(), Array.Empty(), seenOutside: false) + { + Terrain = terrain; Cx = cx; Cy = cy; _worldOrigin = worldOrigin; BuildingCellId = null; + } + + public static LandCell Synthesize(uint id, TerrainSurface terrain, Vector3 worldOrigin, int cx, int cy) + { + float ox = cx * CellSize, oy = cy * CellSize; + float z0 = terrain.SampleZ(ox, oy), z1 = terrain.SampleZ(ox + CellSize, oy); + float z2 = terrain.SampleZ(ox, oy + CellSize), z3 = terrain.SampleZ(ox + CellSize, oy + CellSize); + float zMin = MathF.Min(MathF.Min(z0, z1), MathF.Min(z2, z3)); + float zMax = MathF.Max(MathF.Max(z0, z1), MathF.Max(z2, z3)); + var min = new Vector3(ox, oy, zMin); + var max = new Vector3(ox + CellSize, oy + CellSize, zMax); + + var transform = Matrix4x4.CreateTranslation(worldOrigin); + Matrix4x4.Invert(transform, out var inverse); + return new LandCell(id, terrain, worldOrigin, cx, cy, transform, inverse, min, max); + } + + public override bool PointInCell(Vector3 worldPoint) + { + float lx = worldPoint.X - _worldOrigin.X; + float ly = worldPoint.Y - _worldOrigin.Y; + return lx >= Cx * CellSize && lx < (Cx + 1) * CellSize + && ly >= Cy * CellSize && ly < (Cy + 1) * CellSize; + } +} diff --git a/tests/AcDream.Core.Tests/World/Cells/LandCellTests.cs b/tests/AcDream.Core.Tests/World/Cells/LandCellTests.cs new file mode 100644 index 0000000..df245bb --- /dev/null +++ b/tests/AcDream.Core.Tests/World/Cells/LandCellTests.cs @@ -0,0 +1,36 @@ +using System.Numerics; +using AcDream.Core.Physics; +using AcDream.Core.World.Cells; +using Xunit; + +namespace AcDream.Core.Tests.World.Cells; + +public class LandCellTests +{ + private static TerrainSurface FlatTerrain() + => new TerrainSurface(new byte[81], new float[256], landblockX: 0, landblockY: 0); + + [Fact] + public void Synthesize_SetsCellIndicesAndQuadBounds() + { + var origin = new Vector3(1000f, 2000f, 0f); + var cell = LandCell.Synthesize(0xA9B40014u, FlatTerrain(), origin, cx: 2, cy: 3); + Assert.Equal(2, cell.Cx); + Assert.Equal(3, cell.Cy); + Assert.Equal(2 * 24f, cell.LocalBoundsMin.X); + Assert.Equal(3 * 24f, cell.LocalBoundsMin.Y); + Assert.Equal(3 * 24f, cell.LocalBoundsMax.X); + Assert.Equal(4 * 24f, cell.LocalBoundsMax.Y); + Assert.False(cell.IsEnv); + } + + [Fact] + public void PointInCell_TestsWorldXyAgainstThe24mQuad() + { + var origin = new Vector3(1000f, 2000f, 0f); + var cell = LandCell.Synthesize(0xA9B40014u, FlatTerrain(), origin, cx: 2, cy: 3); + Assert.True(cell.PointInCell(new Vector3(1060f, 2080f, 12.3f))); + Assert.False(cell.PointInCell(new Vector3(1000f, 2080f, 0f))); + Assert.False(cell.PointInCell(new Vector3(1060f, 2100f, 0f))); + } +}