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>
148 lines
5.8 KiB
C#
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()]);
|
|
}
|
|
}
|