From 26cf2b84e712be9bf92ac71be03f1e7c1b44437f Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 8 May 2026 09:11:59 +0200 Subject: [PATCH] =?UTF-8?q?phase(N.1):=20add=20LandBlock=20=E2=86=92=20Ter?= =?UTF-8?q?rainEntry[]=20adapter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase N.1 step 1: WbSceneryAdapter.BuildTerrainEntries converts our LandBlock dat type into the TerrainEntry[81] shape WorldBuilder's TerrainUtils / SceneryRenderManager consume. Field mapping (TerrainInfo → TerrainEntry): TerrainInfo.Road (bits 0-1) → TerrainEntry.Road TerrainInfo.Type (bits 2-6) → TerrainEntry.Type TerrainInfo.Scenery (bits 11-15) → TerrainEntry.Scenery LandBlock.Height[i] → TerrainEntry.Height The spec listed the texture property as 'Texture' but TerrainEntry's actual property is named 'Type' (confirmed from source). The spec also described LandBlock.Terrain as ushort[81] but it is TerrainInfo[81] — DatReaderWriter already decodes the bit fields so the adapter uses TerrainInfo's named properties rather than raw bit-shift expressions. Spec: docs/superpowers/specs/2026-05-08-phase-n1-scenery-via-wb-helpers-design.md Co-Authored-By: Claude Opus 4.7 (1M context) --- src/AcDream.Core/World/WbSceneryAdapter.cs | 45 +++++++++++ .../World/WbSceneryAdapterTests.cs | 74 +++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 src/AcDream.Core/World/WbSceneryAdapter.cs create mode 100644 tests/AcDream.Core.Tests/World/WbSceneryAdapterTests.cs diff --git a/src/AcDream.Core/World/WbSceneryAdapter.cs b/src/AcDream.Core/World/WbSceneryAdapter.cs new file mode 100644 index 0000000..ca73809 --- /dev/null +++ b/src/AcDream.Core/World/WbSceneryAdapter.cs @@ -0,0 +1,45 @@ +using DatReaderWriter.DBObjs; +using WorldBuilder.Shared.Models; + +namespace AcDream.Core.World; + +/// +/// Bridges acdream's dat types into WorldBuilder's data shapes for the +/// Phase N rendering migration. See +/// docs/architecture/worldbuilder-inventory.md for the full strategy. +/// +internal static class WbSceneryAdapter +{ + private const int VerticesPerSide = 9; + private const int TerrainSize = VerticesPerSide * VerticesPerSide; // 81 + + /// + /// Builds a 9×9 = 81-entry array from a + /// 's packed terrain bits + height bytes. WB's + /// TerrainUtils.OnRoad / GetNormal / GetHeight + /// consume this shape. + /// + /// Field mapping (TerrainInfo): + /// TerrainInfo.Road (bits 0-1) → + /// TerrainInfo.Type (bits 2-6) → + /// TerrainInfo.Scenery (bits 11-15) → + /// LandBlock.Height[i] + /// + public static TerrainEntry[] BuildTerrainEntries(LandBlock block) + { + ArgumentNullException.ThrowIfNull(block); + + var entries = new TerrainEntry[TerrainSize]; + for (int i = 0; i < TerrainSize; i++) + { + var ti = block.Terrain[i]; + entries[i] = new TerrainEntry( + height: block.Height[i], + texture: (byte)ti.Type, + scenery: ti.Scenery, + road: ti.Road, + encounters: null); + } + return entries; + } +} diff --git a/tests/AcDream.Core.Tests/World/WbSceneryAdapterTests.cs b/tests/AcDream.Core.Tests/World/WbSceneryAdapterTests.cs new file mode 100644 index 0000000..79fd358 --- /dev/null +++ b/tests/AcDream.Core.Tests/World/WbSceneryAdapterTests.cs @@ -0,0 +1,74 @@ +using AcDream.Core.World; +using DatReaderWriter.DBObjs; +using DatReaderWriter.Types; + +namespace AcDream.Core.Tests.World; + +/// +/// Tests for . The adapter converts our +/// LandBlock dat type (Terrain TerrainInfo[81] + Height byte[81]) into +/// WorldBuilder's [81] +/// shape, which WB's TerrainUtils / SceneryRenderManager consume. +/// +/// Bit layout in LandBlock.Terrain[i] (TerrainInfo / ushort): +/// bits 0-1 : Road (2 bits) → WB TerrainEntry.Road +/// bits 2-6 : TerrainType (5 bits) → WB TerrainEntry.Type +/// bits 11-15 : SceneType (5 bits) → WB TerrainEntry.Scenery +/// Height comes from LandBlock.Height[i] (byte) → WB TerrainEntry.Height. +/// +public class WbSceneryAdapterTests +{ + [Fact] + public void BuildTerrainEntries_PreservesRoadTextureSceneryHeight() + { + var block = new LandBlock(); + + // Vertex 0: road=0x3, type=0x00, scenery=0x1F, height=42 + // raw layout: bits 0-1=11, bits 2-6=00000, bits 7-10=0000, bits 11-15=11111 + // = 0xF803 + block.Terrain[0] = (TerrainInfo)0xF803; + block.Height[0] = 42; + + // Vertex 80: road=0x0, type=0x1F, scenery=0x00, height=200 + // raw layout: bits 0-1=00, bits 2-6=11111, bits 11-15=00000 + // = 0x007C + block.Terrain[80] = (TerrainInfo)0x007C; + block.Height[80] = 200; + + var entries = WbSceneryAdapter.BuildTerrainEntries(block); + + Assert.Equal(81, entries.Length); + + Assert.Equal((byte)42, entries[0].Height); + Assert.Equal((byte)0x3, entries[0].Road); + Assert.Equal((byte)0x00, entries[0].Type); + Assert.Equal((byte)0x1F, entries[0].Scenery); + + Assert.Equal((byte)200, entries[80].Height); + Assert.Equal((byte)0x0, entries[80].Road); + Assert.Equal((byte)0x1F, entries[80].Type); + Assert.Equal((byte)0x00, entries[80].Scenery); + } + + [Fact] + public void BuildTerrainEntries_AllZeros_ProducesEmptyEntries() + { + var block = new LandBlock(); + // Terrain and Height are already zero-initialized by LandBlock constructor. + var entries = WbSceneryAdapter.BuildTerrainEntries(block); + Assert.All(entries, e => + { + Assert.Equal((byte)0, e.Height); + Assert.Equal((byte)0, e.Road); + Assert.Equal((byte)0, e.Type); + Assert.Equal((byte)0, e.Scenery); + }); + } + + [Fact] + public void BuildTerrainEntries_NullBlock_Throws() + { + Assert.Throws(() => + WbSceneryAdapter.BuildTerrainEntries(null!)); + } +}