Bite-sized TDD plan for Phase N.1. Eight tasks: 1. WbSceneryAdapter (LandBlock → TerrainEntry[] adapter) 2. ACDREAM_USE_WB_SCENERY feature flag scaffold 3. Per-helper conformance tests (Displace / OnRoad / GetNormalZ / RotateObj / ScaleObj) 4. Implement GenerateViaWb alternative path 5. Wire feature-flag dispatch in Generate() 6. Visual verification at landblock 0xA9B1 (manual) 7. Flip flag default-on 8. Delete legacy code paths + mark roadmap shipped Each task has explicit code blocks, exact dotnet commands, expected output, and a commit instruction. Conformance tests prove substitution is behavior-preserving before the dispatch is wired in. Spec: docs/superpowers/specs/2026-05-08-phase-n1-scenery-via-wb-helpers-design.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1130 lines
45 KiB
Markdown
1130 lines
45 KiB
Markdown
# Phase N.1 — Scenery via WorldBuilder Helpers Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** Replace the in-line algorithm guts of `SceneryGenerator.Generate()` with calls to WorldBuilder's `SceneryHelpers` (Displace / RotateObj / ScaleObj / CheckSlope / ObjAlign) and `TerrainUtils` (OnRoad / GetNormal). Keep our data flow, our `ScenerySpawn` shape, and our renderer integration. Place behind a `ACDREAM_USE_WB_SCENERY=1` feature flag, prove equivalence with helper-level conformance tests, then flip default-on after visual verification at landblock `0xA9B1`.
|
||
|
||
**Architecture:** Strangler-fig substitution. The 9×9 vertex loop, scene-selection hash, frequency roll, bounds check, building grid, and `BaseLoc.Z` handling stay identical. Only the five algorithm calls (displacement, road test, slope normal, rotation, scale) get replaced. A small adapter `WbSceneryAdapter.BuildTerrainEntries(LandBlock)` produces the `TerrainEntry[]` shape WB's helpers expect.
|
||
|
||
**Tech Stack:** .NET 10 / C# 13, Silk.NET (transitively), `WorldBuilder.Shared` + `Chorizite.OpenGLSDLBackend` (project references already wired up in Phase N.0), DatReaderWriter for `LandBlock` / `Region` / `ObjectDesc` / `Scene` types, xUnit for tests.
|
||
|
||
**Spec:** `docs/superpowers/specs/2026-05-08-phase-n1-scenery-via-wb-helpers-design.md`
|
||
**Parent design:** `docs/superpowers/specs/2026-05-08-phase-n-worldbuilder-migration-design.md`
|
||
**Inventory:** `docs/architecture/worldbuilder-inventory.md`
|
||
|
||
**Prerequisite:** Phase N.0 already shipped (commit `c8782c9`) — `references/WorldBuilder/` is a git submodule pointing at `github.com/eriknihlen/WorldBuilder.git` `acdream` branch; `AcDream.Core.csproj` references `WorldBuilder.Shared` + `Chorizite.OpenGLSDLBackend`.
|
||
|
||
---
|
||
|
||
## File Plan
|
||
|
||
| File | Disposition | Responsibility |
|
||
|---|---|---|
|
||
| `src/AcDream.Core/World/WbSceneryAdapter.cs` | NEW | `LandBlock` (acdream's dat type) → `TerrainEntry[]` (WB's data shape). Stateless. |
|
||
| `src/AcDream.Core/World/SceneryGenerator.cs` | MODIFY | Add `UseWbScenery` feature-flag flag. Add `GenerateViaWb` private alternative path. Wire dispatch in `Generate()`. Keep legacy methods for now — deleted in final task. |
|
||
| `tests/AcDream.Core.Tests/World/WbSceneryAdapterTests.cs` | NEW | Verify field bit-packing round-trips (Road, Type, Scenery, Height) and bounds-check behavior. |
|
||
| `tests/AcDream.Core.Tests/World/SceneryWbConformanceTests.cs` | NEW | Five small tests proving WB's `Displace`/`OnRoad`/`GetNormal`/`RotateObj`/`ScaleObj` match our existing inline logic for representative inputs. |
|
||
| `tests/AcDream.Core.Tests/World/SceneryGeneratorTests.cs` | MODIFY | After Task 7 (legacy delete), prune the now-irrelevant `DisplaceObject_*` tests and update class doc. |
|
||
|
||
**Why split adapter into its own file:** the adapter is a pure stateless utility. Putting it in its own file keeps `SceneryGenerator.cs` focused on the generator algorithm, and makes the adapter trivially reusable in N.2+ (terrain math helpers will need the same `TerrainEntry[]`).
|
||
|
||
**Why combine conformance tests in one file:** all five tests share the same imports and fixtures, and they all measure the same thing (helper equivalence). Splitting would be over-decomposed.
|
||
|
||
---
|
||
|
||
## Task 1: LandBlock → TerrainEntry[] adapter
|
||
|
||
**Files:**
|
||
- Create: `src/AcDream.Core/World/WbSceneryAdapter.cs`
|
||
- Test: `tests/AcDream.Core.Tests/World/WbSceneryAdapterTests.cs`
|
||
|
||
- [ ] **Step 1.1: Write the failing test**
|
||
|
||
Create `tests/AcDream.Core.Tests/World/WbSceneryAdapterTests.cs`:
|
||
|
||
```csharp
|
||
using AcDream.Core.World;
|
||
using DatReaderWriter.DBObjs;
|
||
|
||
namespace AcDream.Core.Tests.World;
|
||
|
||
/// <summary>
|
||
/// Tests for <see cref="WbSceneryAdapter"/>. The adapter converts our
|
||
/// LandBlock dat type (Terrain ushort[81] + Height byte[81]) into
|
||
/// WorldBuilder's <see cref="WorldBuilder.Shared.Models.TerrainEntry"/>[81]
|
||
/// shape, which WB's TerrainUtils / SceneryRenderManager consume.
|
||
///
|
||
/// Bit layout in our LandBlock.Terrain[i] (ushort):
|
||
/// bits 0-1 : Road (2 bits, ACViewer convention)
|
||
/// bits 2-6 : TerrainType (5 bits) → WB calls this Texture
|
||
/// bits 11-15 : SceneType (5 bits) → WB calls this Scenery
|
||
/// Height comes from LandBlock.Height[i] (byte).
|
||
/// </summary>
|
||
public class WbSceneryAdapterTests
|
||
{
|
||
[Fact]
|
||
public void BuildTerrainEntries_PreservesRoadTextureSceneryHeight()
|
||
{
|
||
var block = new LandBlock
|
||
{
|
||
Terrain = new ushort[81],
|
||
Height = new byte[81],
|
||
};
|
||
|
||
// 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] = 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] = 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].Texture);
|
||
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].Texture);
|
||
Assert.Equal((byte)0x00, entries[80].Scenery);
|
||
}
|
||
|
||
[Fact]
|
||
public void BuildTerrainEntries_AllZeros_ProducesEmptyEntries()
|
||
{
|
||
var block = new LandBlock
|
||
{
|
||
Terrain = new ushort[81],
|
||
Height = new byte[81],
|
||
};
|
||
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.Texture);
|
||
Assert.Equal((byte)0, e.Scenery);
|
||
});
|
||
}
|
||
|
||
[Fact]
|
||
public void BuildTerrainEntries_NullBlock_Throws()
|
||
{
|
||
Assert.Throws<ArgumentNullException>(() =>
|
||
WbSceneryAdapter.BuildTerrainEntries(null!));
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 1.2: Run the test to verify it fails**
|
||
|
||
Run: `dotnet test --no-restore tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "WbSceneryAdapterTests" 2>&1 | tail -10`
|
||
Expected: BUILD ERROR or FAIL with "type or namespace 'WbSceneryAdapter' could not be found".
|
||
|
||
- [ ] **Step 1.3: Implement the adapter**
|
||
|
||
Create `src/AcDream.Core/World/WbSceneryAdapter.cs`:
|
||
|
||
```csharp
|
||
using DatReaderWriter.DBObjs;
|
||
using WorldBuilder.Shared.Models;
|
||
|
||
namespace AcDream.Core.World;
|
||
|
||
/// <summary>
|
||
/// Bridges acdream's dat types into WorldBuilder's data shapes for the
|
||
/// Phase N rendering migration. See
|
||
/// <c>docs/architecture/worldbuilder-inventory.md</c> for the full strategy.
|
||
/// </summary>
|
||
internal static class WbSceneryAdapter
|
||
{
|
||
private const int VerticesPerSide = 9;
|
||
private const int TerrainSize = VerticesPerSide * VerticesPerSide; // 81
|
||
|
||
/// <summary>
|
||
/// Builds a 9×9 = 81-entry <see cref="TerrainEntry"/> array from a
|
||
/// <see cref="LandBlock"/>'s packed terrain bits + height bytes. WB's
|
||
/// <c>TerrainUtils.OnRoad</c> / <c>GetNormal</c> / <c>GetHeight</c>
|
||
/// consume this shape.
|
||
///
|
||
/// Bit layout in our <c>LandBlock.Terrain[i]</c> (ushort):
|
||
/// bits 0-1 : Road (2 bits) → WB Road
|
||
/// bits 2-6 : TerrainType (5 bits) → WB Texture
|
||
/// bits 11-15 : SceneType (5 bits) → WB Scenery
|
||
/// Height comes from <c>LandBlock.Height[i]</c> (byte).
|
||
/// </summary>
|
||
public static TerrainEntry[] BuildTerrainEntries(LandBlock block)
|
||
{
|
||
ArgumentNullException.ThrowIfNull(block);
|
||
if (block.Terrain.Length != TerrainSize)
|
||
throw new ArgumentException(
|
||
$"LandBlock.Terrain must be {TerrainSize} entries (9×9), got {block.Terrain.Length}",
|
||
nameof(block));
|
||
if (block.Height.Length != TerrainSize)
|
||
throw new ArgumentException(
|
||
$"LandBlock.Height must be {TerrainSize} entries (9×9), got {block.Height.Length}",
|
||
nameof(block));
|
||
|
||
var entries = new TerrainEntry[TerrainSize];
|
||
for (int i = 0; i < TerrainSize; i++)
|
||
{
|
||
ushort raw = block.Terrain[i];
|
||
byte road = (byte)(raw & 0x3);
|
||
byte texture = (byte)((raw >> 2) & 0x1F);
|
||
byte scenery = (byte)((raw >> 11) & 0x1F);
|
||
byte height = block.Height[i];
|
||
entries[i] = new TerrainEntry(height, texture, scenery, road, encounters: null);
|
||
}
|
||
return entries;
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 1.4: Run the test to verify it passes**
|
||
|
||
Run: `dotnet test --no-restore tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "WbSceneryAdapterTests" 2>&1 | tail -5`
|
||
Expected: `Passed! - Failed: 0, Passed: 3` (or similar — three tests).
|
||
|
||
- [ ] **Step 1.5: Commit**
|
||
|
||
```bash
|
||
git add src/AcDream.Core/World/WbSceneryAdapter.cs tests/AcDream.Core.Tests/World/WbSceneryAdapterTests.cs
|
||
git commit -m "$(cat <<'EOF'
|
||
phase(N.1): add LandBlock → TerrainEntry[] adapter
|
||
|
||
Phase N.1 step 1: WbSceneryAdapter.BuildTerrainEntries converts our
|
||
LandBlock dat type into the TerrainEntry[81] shape WorldBuilder's
|
||
TerrainUtils / SceneryRenderManager consume.
|
||
|
||
Bit-pack mapping (ours → WB):
|
||
Terrain bits 0-1 (Road) → TerrainEntry.Road
|
||
Terrain bits 2-6 (TerrainType) → TerrainEntry.Texture
|
||
Terrain bits 11-15 (SceneType) → TerrainEntry.Scenery
|
||
Height byte → TerrainEntry.Height
|
||
|
||
Spec: docs/superpowers/specs/2026-05-08-phase-n1-scenery-via-wb-helpers-design.md
|
||
|
||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
EOF
|
||
)"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 2: Feature flag scaffold
|
||
|
||
**Files:**
|
||
- Modify: `src/AcDream.Core/World/SceneryGenerator.cs`
|
||
|
||
This task only adds the flag-read field. It changes no behavior and there is nothing to assert beyond "it compiles." The flag is consumed in Task 5.
|
||
|
||
- [ ] **Step 2.1: Add the feature flag field**
|
||
|
||
Open `src/AcDream.Core/World/SceneryGenerator.cs`. Find the line:
|
||
|
||
```csharp
|
||
// AC landblock geometry — matches LandblockMesh.
|
||
private const int VerticesPerSide = 9;
|
||
private const float CellSize = 24.0f;
|
||
private const float LandblockSize = 192.0f; // 8 cells * 24 units
|
||
```
|
||
|
||
Immediately AFTER the `LandblockSize` constant, ADD:
|
||
|
||
```csharp
|
||
|
||
/// <summary>
|
||
/// Phase N.1 feature flag — when set to "1", scenery placement uses
|
||
/// WorldBuilder's <c>SceneryHelpers</c> + <c>TerrainUtils</c> instead of
|
||
/// our hand-ported algorithms. Default off until visual verification at
|
||
/// landblock 0xA9B1 confirms behavior. See
|
||
/// <c>docs/superpowers/specs/2026-05-08-phase-n1-scenery-via-wb-helpers-design.md</c>.
|
||
/// </summary>
|
||
internal static readonly bool UseWbScenery =
|
||
System.Environment.GetEnvironmentVariable("ACDREAM_USE_WB_SCENERY") == "1";
|
||
```
|
||
|
||
- [ ] **Step 2.2: Verify build still passes**
|
||
|
||
Run: `dotnet build src/AcDream.Core/AcDream.Core.csproj 2>&1 | tail -5`
|
||
Expected: `Build succeeded.` and `0 Error(s)`.
|
||
|
||
- [ ] **Step 2.3: Verify all existing tests still pass**
|
||
|
||
Run: `dotnet test --no-build tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "SceneryGenerator|Wb" 2>&1 | tail -3`
|
||
Expected: `Passed!` with all scenery-area tests passing.
|
||
|
||
- [ ] **Step 2.4: Commit**
|
||
|
||
```bash
|
||
git add src/AcDream.Core/World/SceneryGenerator.cs
|
||
git commit -m "$(cat <<'EOF'
|
||
phase(N.1): add ACDREAM_USE_WB_SCENERY feature flag scaffold
|
||
|
||
Phase N.1 step 2: read the env var into a static bool. No behavior
|
||
change yet — the flag is consumed in step 5 when GenerateViaWb is
|
||
wired in.
|
||
|
||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
EOF
|
||
)"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 3: Per-helper conformance tests
|
||
|
||
**Files:**
|
||
- Create: `tests/AcDream.Core.Tests/World/SceneryWbConformanceTests.cs`
|
||
|
||
These five tests prove WB's `SceneryHelpers.Displace` / `TerrainUtils.OnRoad` / `TerrainUtils.GetNormal` / `SceneryHelpers.RotateObj` / `SceneryHelpers.ScaleObj` produce the same answers as the hand-ported logic currently inlined in `Generate()`. After this task, we have empirical evidence that the substitution is safe.
|
||
|
||
If a test fails, that is the bug — investigate before proceeding to Task 4.
|
||
|
||
- [ ] **Step 3.1: Write all five conformance tests**
|
||
|
||
Create `tests/AcDream.Core.Tests/World/SceneryWbConformanceTests.cs`:
|
||
|
||
```csharp
|
||
using System.Numerics;
|
||
using AcDream.Core.World;
|
||
using DatReaderWriter.DBObjs;
|
||
using DatReaderWriter.Types;
|
||
using WB_TerrainUtils = WorldBuilder.Shared.Modules.Landscape.Lib.TerrainUtils;
|
||
using WB_SceneryHelpers = Chorizite.OpenGLSDLBackend.Lib.SceneryHelpers;
|
||
|
||
namespace AcDream.Core.Tests.World;
|
||
|
||
/// <summary>
|
||
/// Phase N.1 helper-level conformance tests. Each test compares an algorithm
|
||
/// in our existing <see cref="SceneryGenerator"/> path against WorldBuilder's
|
||
/// equivalent for representative inputs. Passing tests are empirical evidence
|
||
/// that swapping our inline logic for WB's helpers is behavior-preserving.
|
||
///
|
||
/// If any of these fails the substitution would silently change rendered
|
||
/// scenery; investigate before proceeding to Task 4 (GenerateViaWb).
|
||
///
|
||
/// Inputs are chosen to exercise:
|
||
/// - A non-edge vertex (gx=100, gy=100, j=0) — typical case
|
||
/// - The edge vertex at y=8 specifically (Issue #49 territory)
|
||
/// </summary>
|
||
public class SceneryWbConformanceTests
|
||
{
|
||
private static ObjectDesc MakeObj(
|
||
float displaceX = 12f,
|
||
float displaceY = 12f,
|
||
float minScale = 1f,
|
||
float maxScale = 1f,
|
||
float maxRotation = 0f,
|
||
float minSlope = 0f,
|
||
float maxSlope = 1f,
|
||
int align = 0)
|
||
{
|
||
return new ObjectDesc
|
||
{
|
||
ObjectId = 0x02000258u,
|
||
DisplaceX = displaceX,
|
||
DisplaceY = displaceY,
|
||
MinScale = minScale,
|
||
MaxScale = maxScale,
|
||
MaxRotation = maxRotation,
|
||
MinSlope = minSlope,
|
||
MaxSlope = maxSlope,
|
||
Align = (uint)align,
|
||
BaseLoc = new Frame { Origin = new Vector3(0, 0, 0) },
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// Our DisplaceObject ↔ WB's SceneryHelpers.Displace must produce the
|
||
/// same Vector3 for the same (obj, ix, iy, iq).
|
||
/// </summary>
|
||
[Theory]
|
||
[InlineData(100u, 100u, 0u)] // typical
|
||
[InlineData( 50u, 50u, 1u)] // typical, j=1
|
||
[InlineData( 4u, 8u, 0u)] // edge vertex y=8
|
||
[InlineData( 8u, 4u, 0u)] // edge vertex x=8
|
||
public void Displace_OursMatchesWb(uint ix, uint iy, uint iq)
|
||
{
|
||
var obj = MakeObj();
|
||
var ours = SceneryGenerator.DisplaceObject(obj, ix, iy, iq);
|
||
var wb = WB_SceneryHelpers.Displace(obj, ix, iy, iq);
|
||
|
||
Assert.Equal(ours.X, wb.X, precision: 4);
|
||
Assert.Equal(ours.Y, wb.Y, precision: 4);
|
||
Assert.Equal(ours.Z, wb.Z, precision: 4);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Our IsOnRoad ↔ WB's TerrainUtils.OnRoad must produce the same bool
|
||
/// for the same (lx, ly) when the underlying terrain bits match.
|
||
/// </summary>
|
||
[Theory]
|
||
[InlineData( 12.0f, 12.0f)] // cell (0,0) center
|
||
[InlineData( 85.08f, 190.97f)] // the 0xA9B1 edge-vertex bug location
|
||
[InlineData( 3.0f, 3.0f)] // near a road if r0 is set
|
||
[InlineData( 23.5f, 12.0f)] // edge of cell, between cells
|
||
public void OnRoad_OursMatchesWb_DiagonalRoad(float lx, float ly)
|
||
{
|
||
// Build a synthetic LandBlock with road bits at SW (0,0) and NE (1,1)
|
||
// of cell (0,0) — the diagonal pattern we saw at 0xA9B1.
|
||
var block = new LandBlock
|
||
{
|
||
Terrain = new ushort[81],
|
||
Height = new byte[81],
|
||
};
|
||
// road bit at vertex (0,0) — index 0*9+0 = 0
|
||
block.Terrain[0] = 0x0003; // road=3
|
||
// road bit at vertex (1,1) — index 1*9+1 = 10
|
||
block.Terrain[10] = 0x0003;
|
||
|
||
bool ours = SceneryGenerator.IsOnRoad(block, lx, ly);
|
||
|
||
var entries = WbSceneryAdapter.BuildTerrainEntries(block);
|
||
bool wb = WB_TerrainUtils.OnRoad(new Vector3(lx, ly, 0), entries);
|
||
|
||
Assert.Equal(ours, wb);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Our SampleNormalZFromHeightmap ↔ WB's TerrainUtils.GetNormal(...).Z
|
||
/// must produce the same Z for representative slope inputs.
|
||
/// </summary>
|
||
[Theory]
|
||
[InlineData( 12.0f, 12.0f)] // cell center
|
||
[InlineData( 85.08f, 190.97f)] // the 0xA9B1 edge-vertex location
|
||
[InlineData( 3.0f, 188.0f)] // near a y-edge
|
||
public void GetNormalZ_OursMatchesWb_LinearTable(float lx, float ly)
|
||
{
|
||
// Heightmap with non-flat terrain so normals are non-trivial.
|
||
var heights = new byte[81];
|
||
for (int x = 0; x < 9; x++)
|
||
for (int y = 0; y < 9; y++)
|
||
heights[x * 9 + y] = (byte)((x * 17 + y * 13) % 256);
|
||
|
||
var heightTable = new float[256];
|
||
for (int i = 0; i < 256; i++) heightTable[i] = i * 1.0f;
|
||
|
||
const uint lbX = 0xA9, lbY = 0xB1;
|
||
|
||
// Build a Region-shaped object with the LandHeightTable populated.
|
||
// (TerrainUtils.GetNormal calls region.LandDefs.LandHeightTable[height].)
|
||
// LandHeightTable is float[] (size 256) in DatReaderWriter — see
|
||
// src/AcDream.App/Rendering/GameWindow.cs:1306-1308 for the runtime check.
|
||
var region = new DatReaderWriter.DBObjs.Region
|
||
{
|
||
LandDefs = new LandDefs(),
|
||
};
|
||
// If LandDefs default-initializes LandHeightTable to a non-null float[256],
|
||
// copy into it. If it's null, assign directly. The implementer should
|
||
// pick whichever pattern compiles in DatReaderWriter 2.1.7's API:
|
||
// Option A: region.LandDefs.LandHeightTable = heightTable;
|
||
// Option B: Array.Copy(heightTable, region.LandDefs.LandHeightTable, 256);
|
||
region.LandDefs.LandHeightTable = heightTable;
|
||
|
||
var block = new LandBlock
|
||
{
|
||
Terrain = new ushort[81],
|
||
Height = heights,
|
||
};
|
||
var entries = WbSceneryAdapter.BuildTerrainEntries(block);
|
||
|
||
float ours = AcDream.Core.Physics.TerrainSurface.SampleNormalZFromHeightmap(
|
||
heights, heightTable, lbX, lbY, lx, ly);
|
||
float wb = WB_TerrainUtils.GetNormal(region, entries, lbX, lbY,
|
||
new Vector3(lx, ly, 0)).Z;
|
||
|
||
Assert.Equal(ours, wb, precision: 4);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Our inline rotation logic ↔ WB's SceneryHelpers.RotateObj must
|
||
/// produce the same Quaternion for non-Align objects with MaxRotation.
|
||
/// </summary>
|
||
[Theory]
|
||
[InlineData( 100u, 100u, 0u, 360f)]
|
||
[InlineData( 4u, 8u, 0u, 360f)]
|
||
[InlineData( 200u, 250u, 1u, 180f)]
|
||
public void RotateObj_OursMatchesWb_NonAlign(uint gx, uint gy, uint j, float maxRot)
|
||
{
|
||
var obj = MakeObj(maxRotation: maxRot);
|
||
|
||
// Our inline logic from SceneryGenerator.Generate (~lines 220-231):
|
||
Quaternion ours = obj.BaseLoc.Orientation;
|
||
if (ours.LengthSquared() < 0.0001f) ours = Quaternion.Identity;
|
||
if (obj.MaxRotation > 0f)
|
||
{
|
||
double rotNoise = unchecked((uint)(1813693831u * gy
|
||
- (j + 63127u) * (1360117743u * gy * gx + 1888038839u)
|
||
- 1109124029u * gx)) * 2.3283064e-10;
|
||
float degrees = (float)(rotNoise * obj.MaxRotation);
|
||
float yawDeg = -((450f - degrees) % 360f);
|
||
float yawRad = yawDeg * MathF.PI / 180f;
|
||
var headingQuat = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, yawRad);
|
||
ours = headingQuat * ours;
|
||
}
|
||
|
||
// WB's SceneryHelpers.Displace returns the localPos that RotateObj
|
||
// expects for its loc parameter (used only when SetHeading is called
|
||
// with non-zero matrix, but a stub Vector3 works since BaseLoc is identity).
|
||
var localPos = WB_SceneryHelpers.Displace(obj, gx, gy, j);
|
||
Quaternion wb = WB_SceneryHelpers.RotateObj(obj, gx, gy, j, localPos);
|
||
|
||
Assert.Equal(ours.X, wb.X, precision: 4);
|
||
Assert.Equal(ours.Y, wb.Y, precision: 4);
|
||
Assert.Equal(ours.Z, wb.Z, precision: 4);
|
||
Assert.Equal(ours.W, wb.W, precision: 4);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Our inline scale logic ↔ WB's SceneryHelpers.ScaleObj must produce
|
||
/// the same float for representative inputs.
|
||
/// </summary>
|
||
[Theory]
|
||
[InlineData(100u, 100u, 0u, 0.5f, 1.5f)]
|
||
[InlineData( 4u, 8u, 0u, 1.0f, 1.0f)]
|
||
[InlineData(200u, 250u, 1u, 0.8f, 1.2f)]
|
||
public void ScaleObj_OursMatchesWb(uint gx, uint gy, uint j, float minScale, float maxScale)
|
||
{
|
||
var obj = MakeObj(minScale: minScale, maxScale: maxScale);
|
||
|
||
// Our inline logic from SceneryGenerator.Generate (~lines 236-247):
|
||
float ours;
|
||
if (obj.MinScale == obj.MaxScale)
|
||
{
|
||
ours = obj.MaxScale;
|
||
}
|
||
else
|
||
{
|
||
double scaleNoise = unchecked((uint)(1813693831u * gy
|
||
- (j + 32593u) * (1360117743u * gy * gx + 1888038839u)
|
||
- 1109124029u * gx)) * 2.3283064e-10;
|
||
ours = (float)(Math.Pow(obj.MaxScale / obj.MinScale, scaleNoise) * obj.MinScale);
|
||
}
|
||
if (ours <= 0) ours = 1f;
|
||
|
||
float wb = WB_SceneryHelpers.ScaleObj(obj, gx, gy, j);
|
||
if (wb <= 0) wb = 1f;
|
||
|
||
Assert.Equal(ours, wb, precision: 4);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3.2: Make our `IsOnRoad` accessible to the test**
|
||
|
||
`IsOnRoad` is currently `private`. Bump to `internal` so the conformance test can call it. Open `src/AcDream.Core/World/SceneryGenerator.cs` and change:
|
||
|
||
```csharp
|
||
private static bool IsOnRoad(LandBlock block, float lx, float ly)
|
||
```
|
||
|
||
to:
|
||
|
||
```csharp
|
||
internal static bool IsOnRoad(LandBlock block, float lx, float ly)
|
||
```
|
||
|
||
- [ ] **Step 3.3: Run the conformance tests**
|
||
|
||
Run: `dotnet test --no-restore tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "SceneryWbConformance" 2>&1 | tail -10`
|
||
Expected: ALL TESTS PASS.
|
||
|
||
If any test fails, **stop and investigate** — that's the bug WB is hiding from us. Report which assertion failed (e.g., "Displace at gx=4 gy=8 returns different Y") and confer with the user before proceeding.
|
||
|
||
- [ ] **Step 3.4: Commit**
|
||
|
||
```bash
|
||
git add tests/AcDream.Core.Tests/World/SceneryWbConformanceTests.cs src/AcDream.Core/World/SceneryGenerator.cs
|
||
git commit -m "$(cat <<'EOF'
|
||
phase(N.1): per-helper conformance tests for WB substitutions
|
||
|
||
Phase N.1 step 3: prove our inline algorithms (Displace, IsOnRoad,
|
||
slope normal Z, RotateObj, ScaleObj) match WorldBuilder's helpers
|
||
for representative inputs including the 0xA9B1 edge-vertex case.
|
||
|
||
Bumps IsOnRoad to internal so the test can call it directly.
|
||
|
||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
EOF
|
||
)"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 4: Implement `GenerateViaWb`
|
||
|
||
**Files:**
|
||
- Modify: `src/AcDream.Core/World/SceneryGenerator.cs`
|
||
|
||
Add a new private method `GenerateViaWb` that produces `IReadOnlyList<ScenerySpawn>` using only WB helpers for the substituted algorithm calls. The 9×9 loop, scene selection, frequency check, bounds check, building grid, and `BaseLoc.Z` handling stay structurally identical to `Generate`.
|
||
|
||
- [ ] **Step 4.1: Add the required `using` directives**
|
||
|
||
Open `src/AcDream.Core/World/SceneryGenerator.cs`. The file currently has:
|
||
|
||
```csharp
|
||
using System.Numerics;
|
||
using DatReaderWriter;
|
||
using DatReaderWriter.DBObjs;
|
||
using DatReaderWriter.Types;
|
||
|
||
namespace AcDream.Core.World;
|
||
```
|
||
|
||
Replace with:
|
||
|
||
```csharp
|
||
using System.Numerics;
|
||
using Chorizite.OpenGLSDLBackend.Lib;
|
||
using DatReaderWriter;
|
||
using DatReaderWriter.DBObjs;
|
||
using DatReaderWriter.Types;
|
||
using WorldBuilder.Shared.Modules.Landscape.Lib;
|
||
|
||
namespace AcDream.Core.World;
|
||
```
|
||
|
||
- [ ] **Step 4.2: Add `GenerateViaWb` immediately after `Generate`**
|
||
|
||
Find the closing `}` of `Generate(...)` in `SceneryGenerator.cs` (just before the `IsRoadVertex` method). Immediately AFTER `Generate`'s closing brace, ADD:
|
||
|
||
```csharp
|
||
|
||
/// <summary>
|
||
/// Phase N.1 alternative implementation that delegates the
|
||
/// algorithm calls to WorldBuilder's <c>SceneryHelpers</c> +
|
||
/// <c>TerrainUtils</c>. Structurally identical to <see cref="Generate"/>
|
||
/// but with WB's tested ports doing the work. Selected by
|
||
/// <see cref="UseWbScenery"/>.
|
||
/// </summary>
|
||
private static IReadOnlyList<ScenerySpawn> GenerateViaWb(
|
||
DatCollection dats,
|
||
Region region,
|
||
LandBlock block,
|
||
uint landblockId,
|
||
HashSet<int>? buildingCells)
|
||
{
|
||
var result = new List<ScenerySpawn>();
|
||
|
||
if (region.TerrainInfo?.TerrainTypes is null || region.SceneInfo?.SceneTypes is null)
|
||
return result;
|
||
|
||
// Build the TerrainEntry[] WB's helpers consume — once per landblock.
|
||
var terrainEntries = WbSceneryAdapter.BuildTerrainEntries(block);
|
||
|
||
uint blockX = (landblockId >> 24) * 8;
|
||
uint blockY = ((landblockId >> 16) & 0xFFu) * 8;
|
||
uint lbX = landblockId >> 24;
|
||
uint lbY = (landblockId >> 16) & 0xFFu;
|
||
|
||
for (int x = 0; x < VerticesPerSide; x++)
|
||
{
|
||
for (int y = 0; y < VerticesPerSide; y++)
|
||
{
|
||
int i = x * VerticesPerSide + y;
|
||
ushort raw = block.Terrain[i];
|
||
|
||
uint terrainType = (uint)((raw >> 2) & 0x1F);
|
||
uint sceneType = (uint)((raw >> 11) & 0x1F);
|
||
|
||
if (terrainType >= region.TerrainInfo.TerrainTypes.Count) continue;
|
||
var sceneTypeList = region.TerrainInfo.TerrainTypes[(int)terrainType].SceneTypes;
|
||
if (sceneType >= sceneTypeList.Count) continue;
|
||
|
||
uint sceneInfo = sceneTypeList[(int)sceneType];
|
||
if (sceneInfo >= region.SceneInfo.SceneTypes.Count) continue;
|
||
|
||
var scenes = region.SceneInfo.SceneTypes[(int)sceneInfo].Scenes;
|
||
if (scenes.Count == 0) continue;
|
||
|
||
uint cellX = (uint)x;
|
||
uint cellY = (uint)y;
|
||
uint globalCellX = cellX + blockX;
|
||
uint globalCellY = cellY + blockY;
|
||
|
||
// Scene-selection hash: identical to Generate.
|
||
uint cellMat = globalCellY * (712977289u * globalCellX + 1813693831u)
|
||
- 1109124029u * globalCellX + 2139937281u;
|
||
double offset = cellMat * 2.3283064e-10;
|
||
int sceneIdx = (int)(scenes.Count * offset);
|
||
if (sceneIdx >= scenes.Count || sceneIdx < 0) sceneIdx = 0;
|
||
|
||
uint sceneId = (uint)scenes[sceneIdx];
|
||
var scene = dats.Get<Scene>(sceneId);
|
||
if (scene is null) continue;
|
||
|
||
// Per-object frequency setup: identical to Generate.
|
||
uint cellXMat = unchecked(0u - 1109124029u * globalCellX);
|
||
uint cellYMat = 1813693831u * globalCellY;
|
||
uint cellMat2 = 1360117743u * globalCellX * globalCellY + 1888038839u;
|
||
|
||
for (uint j = 0; j < scene.Objects.Count; j++)
|
||
{
|
||
var obj = scene.Objects[(int)j];
|
||
if (obj.WeenieObj != 0) continue;
|
||
|
||
double noise = unchecked((uint)(cellXMat + cellYMat - cellMat2 * (23399u + j))) * 2.3283064e-10;
|
||
if (noise >= obj.Frequency) continue;
|
||
|
||
// ─── WB substitution: displacement ───────────────────
|
||
var localPos = SceneryHelpers.Displace(obj, globalCellX, globalCellY, j);
|
||
|
||
float lx = cellX * CellSize + localPos.X;
|
||
float ly = cellY * CellSize + localPos.Y;
|
||
|
||
if (lx < 0 || ly < 0 || lx >= LandblockSize || ly >= LandblockSize)
|
||
continue;
|
||
|
||
// ─── WB substitution: road check ──────────────────────
|
||
if (TerrainUtils.OnRoad(new Vector3(lx, ly, 0), terrainEntries))
|
||
continue;
|
||
|
||
// Building check: identical to Generate.
|
||
if (buildingCells is not null)
|
||
{
|
||
int dcx = Math.Clamp((int)(lx / CellSize), 0, CellsPerSide - 1);
|
||
int dcy = Math.Clamp((int)(ly / CellSize), 0, CellsPerSide - 1);
|
||
if (buildingCells.Contains(dcx * VerticesPerSide + dcy))
|
||
continue;
|
||
}
|
||
|
||
// ─── WB substitution: slope check ─────────────────────
|
||
Vector3 normal = TerrainUtils.GetNormal(
|
||
region, terrainEntries, lbX, lbY,
|
||
new Vector3(lx, ly, 0));
|
||
if (!SceneryHelpers.CheckSlope(obj, normal.Z))
|
||
continue;
|
||
|
||
float lz = obj.BaseLoc.Origin.Z;
|
||
|
||
// ─── WB substitution: rotation ────────────────────────
|
||
Quaternion rotation;
|
||
if (obj.Align != 0)
|
||
rotation = SceneryHelpers.ObjAlign(obj, normal, lz, localPos);
|
||
else
|
||
rotation = SceneryHelpers.RotateObj(obj, globalCellX, globalCellY, j, localPos);
|
||
|
||
// ─── WB substitution: scale ───────────────────────────
|
||
float scale = SceneryHelpers.ScaleObj(obj, globalCellX, globalCellY, j);
|
||
if (scale <= 0) scale = 1f;
|
||
|
||
result.Add(new ScenerySpawn(
|
||
ObjectId: obj.ObjectId,
|
||
LocalPosition: new Vector3(lx, ly, lz),
|
||
Rotation: rotation,
|
||
Scale: scale));
|
||
}
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4.3: Verify build**
|
||
|
||
Run: `dotnet build src/AcDream.Core/AcDream.Core.csproj 2>&1 | tail -5`
|
||
Expected: `Build succeeded.` `0 Error(s)`.
|
||
|
||
If you get an error like `'SceneryHelpers' is an ambiguous reference`, it's because both Chorizite.OpenGLSDLBackend.Lib and WorldBuilder.Shared expose helpers — fix by qualifying: `Chorizite.OpenGLSDLBackend.Lib.SceneryHelpers.Displace(...)`.
|
||
|
||
- [ ] **Step 4.4: Verify existing tests still pass**
|
||
|
||
Run: `dotnet test --no-build tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "SceneryGenerator|WbSceneryAdapter|SceneryWbConformance" 2>&1 | tail -3`
|
||
Expected: all scenery-area tests pass (the ones added in Tasks 1 and 3, plus the original SceneryGeneratorTests). No behavior change yet for the live `Generate` path — `GenerateViaWb` is added but not called.
|
||
|
||
- [ ] **Step 4.5: Commit**
|
||
|
||
```bash
|
||
git add src/AcDream.Core/World/SceneryGenerator.cs
|
||
git commit -m "$(cat <<'EOF'
|
||
phase(N.1): implement GenerateViaWb alternative path
|
||
|
||
Phase N.1 step 4: parallel implementation of Generate() that calls
|
||
WB's SceneryHelpers (Displace/CheckSlope/RotateObj/ObjAlign/ScaleObj)
|
||
and TerrainUtils (OnRoad/GetNormal) instead of the inline ports.
|
||
|
||
Not yet wired in — Generate() still runs the legacy path. Step 5
|
||
adds the dispatch.
|
||
|
||
Per-helper conformance tests in step 3 prove this implementation is
|
||
behavior-equivalent to the legacy path.
|
||
|
||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
EOF
|
||
)"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 5: Wire feature-flag dispatch
|
||
|
||
**Files:**
|
||
- Modify: `src/AcDream.Core/World/SceneryGenerator.cs`
|
||
|
||
- [ ] **Step 5.1: Add the dispatch at the top of `Generate`**
|
||
|
||
In `src/AcDream.Core/World/SceneryGenerator.cs`, find the body of `Generate`:
|
||
|
||
```csharp
|
||
public static IReadOnlyList<ScenerySpawn> Generate(
|
||
DatCollection dats,
|
||
Region region,
|
||
LandBlock block,
|
||
uint landblockId,
|
||
HashSet<int>? buildingCells = null,
|
||
float[]? heightTable = null)
|
||
{
|
||
var result = new List<ScenerySpawn>();
|
||
|
||
if (region.TerrainInfo?.TerrainTypes is null || region.SceneInfo?.SceneTypes is null)
|
||
return result;
|
||
```
|
||
|
||
Immediately AFTER the opening `{` and BEFORE `var result = new List<ScenerySpawn>();`, ADD:
|
||
|
||
```csharp
|
||
// Phase N.1: route to the WorldBuilder-backed implementation when
|
||
// ACDREAM_USE_WB_SCENERY=1. See
|
||
// docs/superpowers/specs/2026-05-08-phase-n1-scenery-via-wb-helpers-design.md.
|
||
if (UseWbScenery)
|
||
return GenerateViaWb(dats, region, block, landblockId, buildingCells);
|
||
|
||
```
|
||
|
||
So the method's opening becomes:
|
||
|
||
```csharp
|
||
public static IReadOnlyList<ScenerySpawn> Generate(
|
||
DatCollection dats,
|
||
Region region,
|
||
LandBlock block,
|
||
uint landblockId,
|
||
HashSet<int>? buildingCells = null,
|
||
float[]? heightTable = null)
|
||
{
|
||
// Phase N.1: route to the WorldBuilder-backed implementation when
|
||
// ACDREAM_USE_WB_SCENERY=1. See
|
||
// docs/superpowers/specs/2026-05-08-phase-n1-scenery-via-wb-helpers-design.md.
|
||
if (UseWbScenery)
|
||
return GenerateViaWb(dats, region, block, landblockId, buildingCells);
|
||
|
||
var result = new List<ScenerySpawn>();
|
||
|
||
if (region.TerrainInfo?.TerrainTypes is null || region.SceneInfo?.SceneTypes is null)
|
||
return result;
|
||
```
|
||
|
||
Note: `heightTable` is NOT passed to `GenerateViaWb` — the WB path uses `region.LandDefs.LandHeightTable` via `TerrainUtils.GetNormal`. The legacy path keeps the parameter for backward compatibility.
|
||
|
||
- [ ] **Step 5.2: Verify build**
|
||
|
||
Run: `dotnet build src/AcDream.Core/AcDream.Core.csproj 2>&1 | tail -5`
|
||
Expected: `Build succeeded.`
|
||
|
||
- [ ] **Step 5.3: Verify all existing tests still pass with flag OFF (default)**
|
||
|
||
Run: `dotnet test --no-build tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "SceneryGenerator|WbSceneryAdapter|SceneryWbConformance" 2>&1 | tail -3`
|
||
Expected: all pass (the legacy path is still default, so behavior is unchanged).
|
||
|
||
- [ ] **Step 5.4: Commit**
|
||
|
||
```bash
|
||
git add src/AcDream.Core/World/SceneryGenerator.cs
|
||
git commit -m "$(cat <<'EOF'
|
||
phase(N.1): wire ACDREAM_USE_WB_SCENERY dispatch in Generate()
|
||
|
||
Phase N.1 step 5: when the flag is set, Generate() delegates to
|
||
GenerateViaWb. Default off; flag flips to default-on in step 7
|
||
after visual verification.
|
||
|
||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
EOF
|
||
)"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 6: Visual verification — manual checkpoint
|
||
|
||
This task is interactive. **You must work with the user.** They run the client, look at landblock `0xA9B1`, and confirm two things visually:
|
||
|
||
1. The road-edge tree we have been chasing all session is **not present** in the WB-backed render.
|
||
2. The Issue #49 missing scenery (the trees the 9×9 loop expansion fixed) is **still visible**.
|
||
|
||
- [ ] **Step 6.1: Make sure build is green**
|
||
|
||
Run: `dotnet build 2>&1 | tail -3`
|
||
Expected: `Build succeeded.`
|
||
|
||
- [ ] **Step 6.2: Tell the user how to launch with the flag set**
|
||
|
||
Tell the user (paraphrase): "Set `$env:ACDREAM_USE_WB_SCENERY = '1'` in your launch terminal alongside the other env vars, then launch the client and navigate to Holtburg. Specifically check the road-edge area near coordinates (87, 191) in landblock 0xA9B1 — the tree we have been chasing all session should be gone now. Also confirm Issue #49's previously missing trees are still there."
|
||
|
||
If `dotnet run` is the standard launch command, the full PowerShell launch is:
|
||
|
||
```powershell
|
||
$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call"
|
||
$env:ACDREAM_LIVE = "1"
|
||
$env:ACDREAM_TEST_HOST = "127.0.0.1"
|
||
$env:ACDREAM_TEST_PORT = "9000"
|
||
$env:ACDREAM_TEST_USER = "testaccount"
|
||
$env:ACDREAM_TEST_PASS = "testpassword"
|
||
$env:ACDREAM_USE_WB_SCENERY = "1"
|
||
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 | Tee-Object -FilePath "launch.log"
|
||
```
|
||
|
||
- [ ] **Step 6.3: Wait for user's verification report**
|
||
|
||
The user will tell you "yes the offending tree is gone and Issue #49 is still fine" or "still wrong". If still wrong, do NOT proceed — investigate and report back.
|
||
|
||
- [ ] **Step 6.4: Commit nothing**
|
||
|
||
This task does not produce a code change. The commit happens in Task 7 once the flag is flipped to default-on.
|
||
|
||
---
|
||
|
||
## Task 7: Flip default-on
|
||
|
||
**Files:**
|
||
- Modify: `src/AcDream.Core/World/SceneryGenerator.cs`
|
||
|
||
After visual verification passes in Task 6, the WB-backed path becomes the default. The env var still exists as an escape hatch (`ACDREAM_USE_WB_SCENERY=0` reverts to legacy) so that if a regression is reported the next day, we can flip back without redeploying.
|
||
|
||
- [ ] **Step 7.1: Flip the flag default**
|
||
|
||
In `src/AcDream.Core/World/SceneryGenerator.cs`, find:
|
||
|
||
```csharp
|
||
internal static readonly bool UseWbScenery =
|
||
System.Environment.GetEnvironmentVariable("ACDREAM_USE_WB_SCENERY") == "1";
|
||
```
|
||
|
||
Replace with:
|
||
|
||
```csharp
|
||
/// <summary>
|
||
/// Phase N.1: scenery placement uses WorldBuilder's <c>SceneryHelpers</c>
|
||
/// + <c>TerrainUtils</c> by default. Set <c>ACDREAM_USE_WB_SCENERY=0</c>
|
||
/// to restore the legacy in-line algorithms (escape hatch — to be deleted
|
||
/// in a follow-up commit once we have a few sessions of green visuals).
|
||
/// </summary>
|
||
internal static readonly bool UseWbScenery =
|
||
System.Environment.GetEnvironmentVariable("ACDREAM_USE_WB_SCENERY") != "0";
|
||
```
|
||
|
||
- [ ] **Step 7.2: Verify build + tests**
|
||
|
||
Run: `dotnet build 2>&1 | tail -3 && dotnet test --no-build tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "SceneryGenerator|WbSceneryAdapter|SceneryWbConformance" 2>&1 | tail -3`
|
||
Expected: build green, all targeted tests pass.
|
||
|
||
- [ ] **Step 7.3: Commit**
|
||
|
||
```bash
|
||
git add src/AcDream.Core/World/SceneryGenerator.cs
|
||
git commit -m "$(cat <<'EOF'
|
||
phase(N.1): WB-backed scenery is now default-on
|
||
|
||
Phase N.1 step 7: flips ACDREAM_USE_WB_SCENERY to default-on after
|
||
visual verification at Holtburg confirmed the road-edge tree at
|
||
0xA9B1 is gone and Issue #49 trees are still visible.
|
||
|
||
ACDREAM_USE_WB_SCENERY=0 still reverts to the legacy path. Follow-up
|
||
commit will delete the legacy code entirely.
|
||
|
||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
EOF
|
||
)"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 8: Delete the legacy code path
|
||
|
||
**Files:**
|
||
- Modify: `src/AcDream.Core/World/SceneryGenerator.cs`
|
||
- Modify: `tests/AcDream.Core.Tests/World/SceneryGeneratorTests.cs`
|
||
|
||
After at least one session of clean visuals on the default-on flag, remove the legacy code so we don't accumulate dead-code drift.
|
||
|
||
- [ ] **Step 8.1: Delete the legacy `Generate` body and rename `GenerateViaWb`**
|
||
|
||
In `src/AcDream.Core/World/SceneryGenerator.cs`:
|
||
|
||
1. Delete the `UseWbScenery` field entirely.
|
||
2. Delete the entire body of `Generate` after its signature.
|
||
3. Replace it with a body that just calls `GenerateViaWb`'s logic (or rename `GenerateViaWb` to `Generate`'s body).
|
||
|
||
The simplest approach: rename `GenerateViaWb` to `GenerateInternal` and have the public `Generate` call it. Then delete the legacy logic. Final shape:
|
||
|
||
```csharp
|
||
public static IReadOnlyList<ScenerySpawn> Generate(
|
||
DatCollection dats,
|
||
Region region,
|
||
LandBlock block,
|
||
uint landblockId,
|
||
HashSet<int>? buildingCells = null,
|
||
float[]? heightTable = null)
|
||
{
|
||
// heightTable kept for backward compat; WB path uses
|
||
// region.LandDefs.LandHeightTable internally.
|
||
_ = heightTable;
|
||
return GenerateInternal(dats, region, block, landblockId, buildingCells);
|
||
}
|
||
|
||
private static IReadOnlyList<ScenerySpawn> GenerateInternal(
|
||
DatCollection dats,
|
||
Region region,
|
||
LandBlock block,
|
||
uint landblockId,
|
||
HashSet<int>? buildingCells)
|
||
{
|
||
// ... body that was GenerateViaWb ...
|
||
}
|
||
```
|
||
|
||
4. Delete the now-unused private helpers: `IsOnRoad`, `DisplaceObject`, `RoadHalfWidth`, `CellsPerSide` (if only used by legacy path — keep if `GenerateInternal`'s building check still references it).
|
||
|
||
Concretely, keep:
|
||
- `VerticesPerSide`, `CellSize`, `LandblockSize`, `CellsPerSide` constants (still used in `GenerateInternal`)
|
||
- `IsRoadVertex` (still useful as a tiny public predicate)
|
||
- `WbSceneryAdapter` (still used)
|
||
|
||
Delete:
|
||
- `UseWbScenery`
|
||
- `IsOnRoad` (and its `RoadHalfWidth` dependency)
|
||
- `DisplaceObject` (now dead)
|
||
|
||
- [ ] **Step 8.2: Update SceneryGeneratorTests.cs to remove now-irrelevant tests**
|
||
|
||
In `tests/AcDream.Core.Tests/World/SceneryGeneratorTests.cs`, the existing
|
||
`DisplaceObject_EdgeVertex_CanProduceValidPosition` and
|
||
`DisplaceObject_InteriorVertex_AlwaysNearOrigin` tests reference the deleted
|
||
`SceneryGenerator.DisplaceObject` helper. Delete them.
|
||
|
||
Keep:
|
||
- All `IsRoadVertex_*` tests (`IsRoadVertex` is preserved).
|
||
|
||
The class doc comment at the top should be updated to reflect the new state:
|
||
|
||
```csharp
|
||
/// <summary>
|
||
/// Tests for SceneryGenerator: the road-vertex predicate (only piece of
|
||
/// our own algorithm code remaining post Phase N.1). The displacement /
|
||
/// road / slope / rotation / scale algorithms now run through
|
||
/// WorldBuilder's helpers — see SceneryWbConformanceTests.cs for the
|
||
/// helper-level equivalence proof.
|
||
/// </summary>
|
||
```
|
||
|
||
- [ ] **Step 8.3: Update the SceneryWbConformanceTests now that legacy helpers are gone**
|
||
|
||
`SceneryWbConformanceTests` currently calls `SceneryGenerator.DisplaceObject` and `SceneryGenerator.IsOnRoad`. Once those are deleted, the tests are now testing "WB matches WB" which is meaningless.
|
||
|
||
Delete `SceneryWbConformanceTests.cs` entirely. The conformance tests served their purpose during the migration — they proved the substitution was safe. Now that we're committed to the WB path, they're vestigial.
|
||
|
||
Run: `rm tests/AcDream.Core.Tests/World/SceneryWbConformanceTests.cs`
|
||
|
||
- [ ] **Step 8.4: Verify build + tests**
|
||
|
||
Run: `dotnet build 2>&1 | tail -3 && dotnet test --no-build tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "SceneryGenerator|WbSceneryAdapter" 2>&1 | tail -3`
|
||
Expected: build green, tests pass (just the IsRoadVertex tests + WbSceneryAdapter tests).
|
||
|
||
- [ ] **Step 8.5: Commit**
|
||
|
||
```bash
|
||
git add src/AcDream.Core/World/SceneryGenerator.cs tests/AcDream.Core.Tests/World/SceneryGeneratorTests.cs tests/AcDream.Core.Tests/World/SceneryWbConformanceTests.cs
|
||
git commit -m "$(cat <<'EOF'
|
||
phase(N.1): delete legacy scenery code path; WB is the only path
|
||
|
||
Phase N.1 step 8 (final): now that ACDREAM_USE_WB_SCENERY has been
|
||
default-on for a session with no regressions, remove the legacy
|
||
in-line algorithms so we don't accumulate dead-code drift.
|
||
|
||
Deleted:
|
||
- SceneryGenerator.UseWbScenery (feature flag)
|
||
- SceneryGenerator.IsOnRoad / DisplaceObject / RoadHalfWidth (legacy ports)
|
||
- SceneryGeneratorTests.DisplaceObject_* (test the deleted method)
|
||
- SceneryWbConformanceTests.cs (purpose served — proved equivalence pre-migration)
|
||
|
||
Renamed:
|
||
- GenerateViaWb → GenerateInternal (the only path now)
|
||
|
||
Kept:
|
||
- IsRoadVertex (small predicate, still used by tests + may be useful elsewhere)
|
||
- WbSceneryAdapter (consumed by GenerateInternal; reusable in N.2)
|
||
|
||
Phase N.1 complete. Issues #48, #49 are addressed via WB's tested
|
||
algorithms. Roadmap entry under Phase N can be marked shipped.
|
||
|
||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
EOF
|
||
)"
|
||
```
|
||
|
||
- [ ] **Step 8.6: Mark Phase N.1 shipped in the roadmap**
|
||
|
||
In `docs/plans/2026-04-11-roadmap.md`, find the Phase N section (search for `### Phase N — WorldBuilder Rendering Migration`). Inside the sub-phases table find the row for **N.1**, currently:
|
||
|
||
```
|
||
- **N.1 — Scenery algorithm calls.** Replace `IsOnRoad` /
|
||
`DisplaceObject` / slope-normal calc / rotation / scale inside
|
||
`SceneryGenerator.Generate()` with calls to WB's `SceneryHelpers` +
|
||
`TerrainUtils`. Tiny adapter `LandBlock → TerrainEntry[]`. Keeps our
|
||
data flow + `ScenerySpawn` shape. Feature flag
|
||
`ACDREAM_USE_WB_SCENERY=1`. ~1-2 days.
|
||
```
|
||
|
||
Add a status marker at the start of the line:
|
||
|
||
```
|
||
- **✓ SHIPPED — N.1 — Scenery algorithm calls.** Shipped 2026-05-08.
|
||
Replaced `IsOnRoad` / `DisplaceObject` / slope-normal calc / rotation /
|
||
scale inside `SceneryGenerator.Generate()` with calls to WB's
|
||
`SceneryHelpers` + `TerrainUtils`. Adapter `WbSceneryAdapter` produces
|
||
`TerrainEntry[]`. Visual verification at Holtburg confirmed the
|
||
road-edge tree at 0xA9B1 is gone and Issue #49 trees are still visible.
|
||
```
|
||
|
||
Also: add a row to the top of the file's "Phases already shipped" table, in commit-shipped order:
|
||
|
||
```
|
||
| N.1 | WorldBuilder-backed scenery (Chorizite/WorldBuilder fork as submodule, SceneryHelpers + TerrainUtils replace our inline ports) | Live ✓ |
|
||
```
|
||
|
||
Then commit:
|
||
|
||
```bash
|
||
git add docs/plans/2026-04-11-roadmap.md
|
||
git commit -m "$(cat <<'EOF'
|
||
docs(roadmap): mark Phase N.1 (scenery via WB helpers) shipped
|
||
|
||
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
EOF
|
||
)"
|
||
```
|
||
|
||
---
|
||
|
||
## Done definition
|
||
|
||
After all 8 tasks land cleanly:
|
||
|
||
- [x] `dotnet build` and `dotnet test` (excluding the 8 pre-existing `DispatcherToMovementIntegrationTests` failures unrelated to this work) green.
|
||
- [x] Visual verification at Holtburg confirms:
|
||
- The road-edge tree near `0xA9B1` is **gone**.
|
||
- Issue #49's missing scenery is **still visible**.
|
||
- No new visual regressions in surrounding landblocks during a brief flight.
|
||
- [x] Phase N.1 marked shipped in `docs/plans/2026-04-11-roadmap.md`.
|
||
- [x] `SceneryGenerator.Generate` calls only WB helpers for displacement / road / slope / rotation / scale.
|
||
- [x] Issue #49 stays closed; no new related issues filed.
|