acdream/tests/AcDream.App.Tests/Rendering/Walk/WalkLandscapeAssemblerTests.cs
Erik b10ad662b0 feat(render) Campaign FW3.1: production walk world data behind the seam
The retail frame walk's world model now materializes from production
landblock-build owners through the legal IDatReaderWriter seam, with
zero frame wiring (FW3.2 roots the frame):

- WalkCellFactory: WalkCell built in the SAME pass as LoadedCell
  (EnvCellLandblockBuild.BuildVisibilityCell) from the raw portal
  Flags/polygons/planes/stab lists already parsed there; stored as
  LoadedCell.Walk, committed atomically with the cell. The
  fixture-pinned decodes (inverse-0x2 portal side, 0xFFFF->0xFFFFFFFF
  exit widening) live here.
- WalkBuildingFactory + WalkBuildingRegistry: the production
  WalkBuilding build (drawing BSP with PORT nodes, degrade ladder,
  portal sides/stab lists, sort center, model frame) from the SAME
  LandBlockInfo the streaming build already fetches, under the
  factory's existing DAT lock - closing the gap where BuildingLoader
  drops every walk field at load.
- WalkLandscapeAssembler: the retail 51x51 viewer-centred grid
  (mid_radius 25) fed incrementally from landblock publish/retire;
  per-block z-slab (heightTable[max]+200 / [min]-1) computed
  worker-side in LandblockBuildFactory from the heights already in
  hand. O(1) SetViewer on same-block frames.
- WalkProductionFrameContext: the walk's frame contexts over
  CellVisibility + WalkBuildingRegistry with a generic
  inverse-view-projection ray caster (rays feed cross products only -
  scale-free) and the znear=0.1 CY plane.
- Publication: LandblockRenderPublisher owns both walk registries,
  publishing in the same AdvanceCompleteOne step as BuildingRegistry
  and retiring in RemoveBuildingRegistry - same commit, same
  retirement, no new ticket stage.

Conformance: ALL TEN oracle fixtures replay identically through the
PRODUCTION builders (WalkProductionWorldConformanceTests) - same
signatures as the test adapter, first run. Known gap documented for
FW3.2: far-tier landblocks carry no EnvCell transaction, so their
z-slab never reaches the assembler.

Suites: full Release build 0 warnings; Walk lane 186/1 skip;
hermetic 6,738/0 (+24); RuntimeDatAccessArchitectureTests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 13:19:22 +02:00

148 lines
5.8 KiB
C#

using System.Numerics;
using AcDream.App.Rendering.Walk;
namespace AcDream.App.Tests.Rendering.Walk;
/// <summary>Campaign FW3.1 — hermetic (no DAT) coverage of the production
/// landscape assembler: publish/retire lifecycle, the LOD ring pyramid, and
/// the viewer-recentre grid reflow. The DAT-backed equivalence to the FW1
/// test builder is proven by <c>WalkProductionWorldConformanceTests</c>
/// (Lane=InstalledDat); this class covers the incremental publish/retire
/// machinery that harness doesn't exercise (it publishes and calls
/// SetViewer exactly once per fixture).</summary>
public sealed class WalkLandscapeAssemblerTests
{
private const uint LandblockId = 0xA9B4FFFFu; // block (0xA9, 0xB4)
private const uint CameraCellId = 0xA9B40001u; // same block, outdoor landcell 1
private static int GridIndex(int gx, int gy) => gx * WalkLandscapeAssembler.GridWidth + gy;
private static int CenterIndex() =>
GridIndex(WalkLandscapeAssembler.MidRadius, WalkLandscapeAssembler.MidRadius);
[Fact]
public void PublishBeforeSetViewer_IsVisibleOnceSetViewerRuns()
{
var assembler = new WalkLandscapeAssembler();
assembler.PublishLandblock(LandblockId, maxZ: 100f, minZ: -5f, Array.Empty<WalkBuildingFactory.Entry>());
assembler.SetViewer(CameraCellId, Vector3.Zero);
WalkLandBlock? block = assembler.Landscape.Blocks[CenterIndex()];
Assert.NotNull(block);
Assert.Equal(8, block!.SideCellCount);
Assert.Equal(100f, block.MaxZ);
Assert.Equal(-5f, block.MinZ);
}
[Fact]
public void PublishAfterSetViewer_RefreshesTheAlreadyWindowedSlotImmediately()
{
var assembler = new WalkLandscapeAssembler();
assembler.SetViewer(CameraCellId, Vector3.Zero);
assembler.PublishLandblock(LandblockId, maxZ: 42f, minZ: 3f, Array.Empty<WalkBuildingFactory.Entry>());
Assert.Equal(42f, assembler.Landscape.Blocks[CenterIndex()]!.MaxZ);
}
[Fact]
public void RingPyramid_FarBlockDegradesSideCellCountAndNeverAttachesBuildings()
{
var assembler = new WalkLandscapeAssembler();
var building = new WalkBuildingFactory.Entry(
new WalkBuilding { PositionCellId = (LandblockId & 0xFFFF0000u) | 1u },
Matrix4x4.Identity, Matrix4x4.Identity);
assembler.PublishLandblock(LandblockId, 1f, 0f, new[] { building });
// Three blocks north of the camera's own block -> ring 3 -> SideCellCount 2.
const uint FarLandblockId = 0xA9B7FFFFu;
assembler.PublishLandblock(FarLandblockId, 1f, 0f, new[] { building });
assembler.SetViewer(CameraCellId, Vector3.Zero);
WalkLandBlock center = assembler.Landscape.Blocks[CenterIndex()]!;
WalkLandBlock far = assembler.Landscape.Blocks[
GridIndex(WalkLandscapeAssembler.MidRadius, WalkLandscapeAssembler.MidRadius + 3)]!;
Assert.Equal(8, center.SideCellCount);
Assert.Contains(center.CellBuildings, b => b is not null);
Assert.Equal(2, far.SideCellCount);
Assert.All(far.CellBuildings, Assert.Null);
}
[Fact]
public void RetireLandblock_NullsTheWindowedSlot()
{
var assembler = new WalkLandscapeAssembler();
assembler.PublishLandblock(LandblockId, 1f, 0f, Array.Empty<WalkBuildingFactory.Entry>());
assembler.SetViewer(CameraCellId, Vector3.Zero);
Assert.NotNull(assembler.Landscape.Blocks[CenterIndex()]);
assembler.RetireLandblock(LandblockId);
Assert.Null(assembler.Landscape.Blocks[CenterIndex()]);
}
[Fact]
public void RetireLandblock_UnpublishedLandblockIsANoOp()
{
var assembler = new WalkLandscapeAssembler();
assembler.RetireLandblock(LandblockId);
Assert.Null(assembler.Landscape.Blocks[CenterIndex()]);
}
[Fact]
public void SetViewer_RecentresTheWindowWhenTheCameraCrossesIntoAnotherBlock()
{
var assembler = new WalkLandscapeAssembler();
assembler.PublishLandblock(LandblockId, 7f, -1f, Array.Empty<WalkBuildingFactory.Entry>());
assembler.SetViewer(CameraCellId, Vector3.Zero);
Assert.NotNull(assembler.Landscape.Blocks[CenterIndex()]);
// Move the camera one block east; the published landblock should now
// sit one grid slot WEST of center instead of at center.
assembler.SetViewer(0xAAB40001u, Vector3.Zero);
Assert.Null(assembler.Landscape.Blocks[CenterIndex()]);
WalkLandBlock? shifted = assembler.Landscape.Blocks[
GridIndex(WalkLandscapeAssembler.MidRadius - 1, WalkLandscapeAssembler.MidRadius)];
Assert.NotNull(shifted);
Assert.Equal(7f, shifted!.MaxZ);
}
[Fact]
public void SetViewer_LandcellIndexDerivesViewerCellFromLowWord()
{
var assembler = new WalkLandscapeAssembler();
// Low word 10 -> landcell index 9 -> (9/8, 9%8) = (1, 1).
assembler.SetViewer(0xA9B4000Au, Vector3.Zero);
Assert.Equal(1, assembler.Landscape.ViewerCellX);
Assert.Equal(1, assembler.Landscape.ViewerCellY);
}
[Fact]
public void SetViewer_InteriorCameraDerivesViewerCellFromOrigin()
{
var assembler = new WalkLandscapeAssembler();
assembler.SetViewer(0xA9B40105u, new Vector3(50f, 74f, 0f));
Assert.Equal(2, assembler.Landscape.ViewerCellX); // floor(50 / 24) = 2
Assert.Equal(3, assembler.Landscape.ViewerCellY); // floor(74 / 24) = 3
}
[Fact]
public void SetViewer_SameBlockRepeatCallDoesNotClearAlreadyPublishedSlots()
{
var assembler = new WalkLandscapeAssembler();
assembler.PublishLandblock(LandblockId, 1f, 0f, Array.Empty<WalkBuildingFactory.Entry>());
assembler.SetViewer(CameraCellId, Vector3.Zero);
assembler.SetViewer(CameraCellId, new Vector3(5f, 5f, 0f));
Assert.NotNull(assembler.Landscape.Blocks[CenterIndex()]);
}
}