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>
This commit is contained in:
parent
b95850defe
commit
b10ad662b0
14 changed files with 1711 additions and 2 deletions
|
|
@ -0,0 +1,75 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Walk;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering.Walk;
|
||||
|
||||
/// <summary>Campaign FW3.1 — hermetic (no DAT) coverage of the walk's
|
||||
/// production building registry: landblock-keyed publish/retire and the
|
||||
/// O(1) reverse index <see cref="WalkProductionFrameContext"/> relies on.</summary>
|
||||
public sealed class WalkBuildingRegistryTests
|
||||
{
|
||||
private static WalkBuildingFactory.Entry Entry(uint positionCellId) =>
|
||||
new(new WalkBuilding { PositionCellId = positionCellId }, Matrix4x4.Identity, Matrix4x4.Identity);
|
||||
|
||||
[Fact]
|
||||
public void Publish_MakesBuildingsFindableByLandblockAndByReference()
|
||||
{
|
||||
var registry = new WalkBuildingRegistry();
|
||||
WalkBuildingFactory.Entry entry = Entry(0xA9B40001u);
|
||||
|
||||
registry.Publish(0xA9B4FFFFu, new[] { entry });
|
||||
|
||||
Assert.Same(entry, Assert.Single(registry.GetBuildings(0xA9B40100u)));
|
||||
Assert.True(registry.TryGetEntry(entry.Building, out var found));
|
||||
Assert.Same(entry, found);
|
||||
Assert.Equal(1, registry.LandblockCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Publish_ReplacesThePreviousLandblockAndDropsItsReverseIndexEntries()
|
||||
{
|
||||
var registry = new WalkBuildingRegistry();
|
||||
WalkBuildingFactory.Entry first = Entry(0xA9B40001u);
|
||||
WalkBuildingFactory.Entry second = Entry(0xA9B40002u);
|
||||
registry.Publish(0xA9B4FFFFu, new[] { first });
|
||||
|
||||
registry.Publish(0xA9B4FFFFu, new[] { second });
|
||||
|
||||
Assert.Same(second, Assert.Single(registry.GetBuildings(0xA9B4FFFFu)));
|
||||
Assert.False(registry.TryGetEntry(first.Building, out _));
|
||||
Assert.True(registry.TryGetEntry(second.Building, out _));
|
||||
Assert.Equal(1, registry.LandblockCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Retire_RemovesTheLandblockAndItsReverseIndexEntries()
|
||||
{
|
||||
var registry = new WalkBuildingRegistry();
|
||||
WalkBuildingFactory.Entry entry = Entry(0xA9B40001u);
|
||||
registry.Publish(0xA9B4FFFFu, new[] { entry });
|
||||
|
||||
registry.Retire(0xA9B40100u); // any id sharing the landblock prefix
|
||||
|
||||
Assert.Empty(registry.GetBuildings(0xA9B4FFFFu));
|
||||
Assert.False(registry.TryGetEntry(entry.Building, out _));
|
||||
Assert.Equal(0, registry.LandblockCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Retire_UnknownLandblockIsANoOp()
|
||||
{
|
||||
var registry = new WalkBuildingRegistry();
|
||||
|
||||
registry.Retire(0xA9B4FFFFu);
|
||||
|
||||
Assert.Equal(0, registry.LandblockCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetBuildings_UnpublishedLandblockReturnsEmpty()
|
||||
{
|
||||
var registry = new WalkBuildingRegistry();
|
||||
|
||||
Assert.Empty(registry.GetBuildings(0xA9B4FFFFu));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
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()]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Walk;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering.Walk;
|
||||
|
||||
/// <summary>Campaign FW3.1 — hermetic (no DAT) coverage of the production
|
||||
/// frame context: cell resolution through <see cref="CellVisibility"/>,
|
||||
/// building resolution through <see cref="WalkBuildingRegistry"/>, and the
|
||||
/// CyPlane/ray-cast wiring. This proves the SEAM (does the context read the
|
||||
/// right registries the right way); the ten-fixture conformance gate proves
|
||||
/// the WORLD DATA those registries are fed is correct.</summary>
|
||||
public sealed class WalkProductionFrameContextTests
|
||||
{
|
||||
private static Matrix4x4 SimpleViewProjection() =>
|
||||
Matrix4x4.CreateLookAt(Vector3.Zero, Vector3.UnitY, Vector3.UnitZ)
|
||||
* Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 3f, 4f / 3f, 0.1f, 1000f);
|
||||
|
||||
[Fact]
|
||||
public void GetVisible_ResolvesThroughTheCommittedCellVisibilityRegistry()
|
||||
{
|
||||
var cellVisibility = new CellVisibility();
|
||||
var walkCell = new WalkCell { CellId = 0xA9B40100u };
|
||||
var loaded = new LoadedCell { CellId = 0xA9B40100u, Walk = walkCell };
|
||||
cellVisibility.CommitLandblock(0xA9B4FFFFu, new[] { loaded });
|
||||
var ctx = new WalkProductionFrameContext(
|
||||
cellVisibility, new WalkBuildingRegistry(), Vector3.Zero, Vector3.UnitY,
|
||||
SimpleViewProjection(), 1024f, 768f);
|
||||
|
||||
Assert.Same(walkCell, ctx.GetVisible(0xA9B40100u));
|
||||
Assert.Null(ctx.GetVisible(0xA9B40101u));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetVisible_ReturnsNullWhenTheCommittedCellHasNoWalkModel()
|
||||
{
|
||||
// A hand-built LoadedCell that never went through
|
||||
// EnvCellLandblockBuildBuilder.BuildVisibilityCell (test-only
|
||||
// shortcut some existing fixtures take) — Walk stays null.
|
||||
var cellVisibility = new CellVisibility();
|
||||
var loaded = new LoadedCell { CellId = 0xA9B40100u };
|
||||
cellVisibility.CommitLandblock(0xA9B4FFFFu, new[] { loaded });
|
||||
var ctx = new WalkProductionFrameContext(
|
||||
cellVisibility, new WalkBuildingRegistry(), Vector3.Zero, Vector3.UnitY,
|
||||
SimpleViewProjection(), 1024f, 768f);
|
||||
|
||||
Assert.Null(ctx.GetVisible(0xA9B40100u));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ObjectToClipAndViewpointIn_UseTheCellsOwnTransforms()
|
||||
{
|
||||
var cell = new WalkCell
|
||||
{
|
||||
CellId = 1,
|
||||
WorldTransform = Matrix4x4.CreateTranslation(10f, 0f, 0f),
|
||||
InverseWorldTransform = Matrix4x4.CreateTranslation(-10f, 0f, 0f),
|
||||
};
|
||||
Matrix4x4 vp = SimpleViewProjection();
|
||||
var ctx = new WalkProductionFrameContext(
|
||||
new CellVisibility(), new WalkBuildingRegistry(), new Vector3(10f, 0f, 0f), Vector3.UnitY,
|
||||
vp, 1024f, 768f);
|
||||
|
||||
Assert.Equal(cell.WorldTransform * vp, ctx.ObjectToClip(cell));
|
||||
Assert.Equal(Vector3.Zero, ctx.ViewpointIn(cell));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ViewpointInBuilding_ResolvesThroughWalkBuildingRegistry()
|
||||
{
|
||||
var registry = new WalkBuildingRegistry();
|
||||
var building = new WalkBuilding { PositionCellId = 1 };
|
||||
Matrix4x4 world = Matrix4x4.CreateTranslation(5f, 0f, 0f);
|
||||
Matrix4x4.Invert(world, out Matrix4x4 inverse);
|
||||
registry.Publish(0xA9B4FFFFu, new[] { new WalkBuildingFactory.Entry(building, world, inverse) });
|
||||
var ctx = new WalkProductionFrameContext(
|
||||
new CellVisibility(), registry, new Vector3(5f, 0f, 0f), Vector3.UnitY,
|
||||
SimpleViewProjection(), 1024f, 768f);
|
||||
|
||||
Assert.Equal(Vector3.Zero, ctx.ViewpointInBuilding(building));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ViewpointInBuilding_ThrowsWhenTheBuildingIsNotCommitted()
|
||||
{
|
||||
// Fail loud (the PV3 post-mortem rule): a walk/registry desync must
|
||||
// never resolve to a silently-skipped building.
|
||||
var ctx = new WalkProductionFrameContext(
|
||||
new CellVisibility(), new WalkBuildingRegistry(), Vector3.Zero, Vector3.UnitY,
|
||||
SimpleViewProjection(), 1024f, 768f);
|
||||
var unregistered = new WalkBuilding { PositionCellId = 1 };
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => ctx.ViewpointInBuilding(unregistered));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ViewerDistanceTo_MeasuresToTheBuildingsTransformedSortCenter()
|
||||
{
|
||||
var registry = new WalkBuildingRegistry();
|
||||
var building = new WalkBuilding { PositionCellId = 1, SortCenter = new Vector3(0f, 3f, 0f) };
|
||||
Matrix4x4 world = Matrix4x4.CreateTranslation(0f, 10f, 0f);
|
||||
Matrix4x4.Invert(world, out Matrix4x4 inverse);
|
||||
registry.Publish(0xA9B4FFFFu, new[] { new WalkBuildingFactory.Entry(building, world, inverse) });
|
||||
var ctx = new WalkProductionFrameContext(
|
||||
new CellVisibility(), registry, Vector3.Zero, Vector3.UnitY,
|
||||
SimpleViewProjection(), 1024f, 768f);
|
||||
|
||||
Assert.Equal(13f, ctx.ViewerDistanceTo(building));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CyPlane_MatchesTheRetailNearPlaneFormula()
|
||||
{
|
||||
Vector3 forward = Vector3.UnitY;
|
||||
var eye = new Vector3(0f, 5f, 0f);
|
||||
var ctx = new WalkProductionFrameContext(
|
||||
new CellVisibility(), new WalkBuildingRegistry(), eye, forward,
|
||||
SimpleViewProjection(), 1024f, 768f);
|
||||
|
||||
Assert.Equal(forward, ctx.CyPlane.Normal);
|
||||
Assert.Equal(-Vector3.Dot(eye, forward) - WalkProductionFrameContext.ZNear, ctx.CyPlane.D);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_RejectsANonInvertibleViewProjection()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new WalkProductionFrameContext(
|
||||
new CellVisibility(), new WalkBuildingRegistry(), Vector3.Zero, Vector3.UnitY,
|
||||
default, 1024f, 768f));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,321 @@
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Tests.Rendering;
|
||||
using AcDream.App.Rendering.Walk;
|
||||
using AcDream.Content;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Options;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering.Walk;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign FW3.1's load-bearing deliverable: the ten pose-stamped oracle
|
||||
/// fixtures (docs/research/2026-08-30-fw-walk-oracle/), replayed through the
|
||||
/// PRODUCTION world-data builders
|
||||
/// (<see cref="WalkCellFactory"/>/<see cref="WalkBuildingFactory"/>
|
||||
/// /<see cref="WalkLandscapeAssembler"/> — the same code
|
||||
/// <c>EnvCellLandblockBuildBuilder</c>/<c>LandblockBuildFactory</c> run at
|
||||
/// real landblock-build time, consuming ONLY the legal
|
||||
/// <see cref="IDatReaderWriter"/> seam), must reproduce the identical walk
|
||||
/// output <see cref="WalkTraceConformanceTests"/> already proves against the
|
||||
/// FW1 test adapter (<c>WalkWorldDatAdapter</c>/<c>WalkLandscapeDatBuilder</c>).
|
||||
///
|
||||
/// This class deliberately does NOT touch <see cref="WalkTraceConformanceTests"/>
|
||||
/// (frozen — must stay green untouched) but reuses its driver/signature
|
||||
/// helpers verbatim: <see cref="WalkTraceReplayContext"/>,
|
||||
/// <see cref="WalkOracleTrace"/>, <see cref="WalkOraclePose"/>,
|
||||
/// <see cref="WalkOracleFrame"/>. <see cref="WalkTraceReplayContext.Buildings"/>
|
||||
/// is typed against the TEST adapter's <c>WalkWorldDatAdapter.BuildingEntry</c>
|
||||
/// record — structurally identical to <see cref="WalkBuildingFactory.Entry"/>
|
||||
/// (same three fields) — so <see cref="BuildProductionWorld"/> below adapts
|
||||
/// one into the other rather than touching the shared context type.
|
||||
///
|
||||
/// <see cref="BuildProductionWorld"/> mirrors the FW1 harness's
|
||||
/// <c>WalkLandscapeDatBuilder.Build</c> loop structure and ring math
|
||||
/// EXACTLY (51×51 grid, <c>WalkLandscapeAssembler.MidRadius</c> = 25,
|
||||
/// buildings/stab-cells only at full resolution), but every DATA-BUILDING
|
||||
/// step below it — cells, buildings, the drawing BSP, z-slab — calls the
|
||||
/// PRODUCTION functions. That is the whole conformance claim: the grid/ring
|
||||
/// assembly is proven once (FW1's existing gate, via
|
||||
/// <see cref="WalkLandscapeAssembler"/>'s own port of that same math); this
|
||||
/// class proves the DATA those slots are filled with is identical to the
|
||||
/// FW1 test adapter's.
|
||||
/// </summary>
|
||||
[Trait("Lane", "InstalledDat")]
|
||||
public sealed class WalkProductionWorldConformanceTests
|
||||
{
|
||||
private sealed class Recorder : IWalkEventSink
|
||||
{
|
||||
public readonly List<WalkEvent> Events = new();
|
||||
public void Emit(in WalkEvent walkEvent) => Events.Add(walkEvent);
|
||||
}
|
||||
|
||||
private static DatCollection OpenDats()
|
||||
{
|
||||
string? datDir = CornerFloodReplayTests.ResolveDatDir();
|
||||
if (datDir is null)
|
||||
{
|
||||
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md.");
|
||||
}
|
||||
return new DatCollection(datDir!, DatAccessType.Read);
|
||||
}
|
||||
|
||||
/// <summary>Builds the walk's world data (landscape + interior cells +
|
||||
/// buildings) for a camera pose through the PRODUCTION builders, in the
|
||||
/// exact shape <see cref="WalkTraceReplayContext"/> expects. Mirrors
|
||||
/// <c>WalkLandscapeDatBuilder.Build</c>'s grid loop; every cell/building
|
||||
/// constructed inside it comes from <see cref="WalkCellFactory"/> /
|
||||
/// <see cref="WalkBuildingFactory"/>.</summary>
|
||||
private static (WalkLandscapeAssembler Assembler, Dictionary<uint, WalkCell> Cells,
|
||||
Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry> Buildings)
|
||||
BuildProductionWorld(IDatReaderWriter dats, uint cameraCellId, Vector3 cameraOrigin)
|
||||
{
|
||||
int cameraBlockX = (int)(cameraCellId >> 24);
|
||||
int cameraBlockY = (int)((cameraCellId >> 16) & 0xFF);
|
||||
var assembler = new WalkLandscapeAssembler();
|
||||
var cells = new Dictionary<uint, WalkCell>();
|
||||
var buildings = new Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry>();
|
||||
Region region = (Region)dats.Get<Region>(0x13000000u)!;
|
||||
float[] heightTable = region.LandDefs.LandHeightTable;
|
||||
|
||||
for (int gx = 0; gx < WalkLandscapeAssembler.GridWidth; gx++)
|
||||
{
|
||||
for (int gy = 0; gy < WalkLandscapeAssembler.GridWidth; gy++)
|
||||
{
|
||||
int blockX = cameraBlockX + gx - WalkLandscapeAssembler.MidRadius;
|
||||
int blockY = cameraBlockY + gy - WalkLandscapeAssembler.MidRadius;
|
||||
if (blockX < 0 || blockX > 0xFF || blockY < 0 || blockY > 0xFF)
|
||||
continue;
|
||||
uint landblockId = (uint)((blockX << 24) | (blockY << 16));
|
||||
if (dats.Get<LandBlock>(landblockId | 0xFFFFu) is not LandBlock landBlock)
|
||||
continue;
|
||||
|
||||
byte maxByte = 0, minByte = 255;
|
||||
foreach (byte h in landBlock.Height)
|
||||
{
|
||||
if (h > maxByte) maxByte = h;
|
||||
if (h < minByte) minByte = h;
|
||||
}
|
||||
float maxZ = heightTable[maxByte] + 200f;
|
||||
float minZ = heightTable[minByte] - 1f;
|
||||
|
||||
var blockOffset = new Vector3(
|
||||
(gx - WalkLandscapeAssembler.MidRadius) * WalkLandscape.BlockLength,
|
||||
(gy - WalkLandscapeAssembler.MidRadius) * WalkLandscape.BlockLength,
|
||||
0f);
|
||||
|
||||
var blockBuildings = new List<WalkBuildingFactory.Entry>();
|
||||
if (WalkLandscapeAssembler.SideCellCountForRing(WalkLandscapeAssembler.RingOf(gx, gy)) == 8)
|
||||
{
|
||||
LandBlockInfo? info = dats.Get<LandBlockInfo>(landblockId | 0xFFFEu);
|
||||
blockBuildings = WalkBuildingFactory.Build(dats, landblockId, info?.Buildings, blockOffset);
|
||||
foreach (WalkBuildingFactory.Entry entry in blockBuildings)
|
||||
{
|
||||
buildings[entry.Building] = new WalkWorldDatAdapter.BuildingEntry(
|
||||
entry.Building, entry.WorldTransform, entry.InverseWorldTransform);
|
||||
|
||||
// Retail's loaded-interior rule (CLandBlock::init_buildings
|
||||
// @0052fd80 -> add_to_stablist -> grab_visible_cells): a
|
||||
// full-res block loads exactly its buildings' portal stab
|
||||
// cells, matching WalkLandscapeDatBuilder's harness scope.
|
||||
foreach (WalkBldPortal portal in entry.Building.Portals)
|
||||
{
|
||||
if (portal.OtherCellId != 0xFFFFFFFFu && !cells.ContainsKey(portal.OtherCellId))
|
||||
{
|
||||
WalkCell? c = WalkCellFactory.BuildCell(dats, portal.OtherCellId, blockOffset);
|
||||
if (c is not null) cells[c.CellId] = c;
|
||||
}
|
||||
foreach (uint stab in portal.StabList)
|
||||
{
|
||||
if (cells.ContainsKey(stab)) continue;
|
||||
WalkCell? c = WalkCellFactory.BuildCell(dats, stab, blockOffset);
|
||||
if (c is not null) cells[c.CellId] = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assembler.PublishLandblock(landblockId, maxZ, minZ, blockBuildings);
|
||||
}
|
||||
}
|
||||
assembler.SetViewer(cameraCellId, cameraOrigin);
|
||||
return (assembler, cells, buildings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Street_outdoor_first_frame_diff()
|
||||
{
|
||||
IReadOnlyList<WalkOracleFrame> frames = WalkOracleTrace.Load("posed/holtburg-street-outdoor");
|
||||
Assert.NotEmpty(frames);
|
||||
using DatCollection dats = OpenDats();
|
||||
using var adapter = new DatCollectionAdapter(dats);
|
||||
WalkOracleFrame frame = frames[1];
|
||||
Assert.NotNull(frame.Pose);
|
||||
(WalkLandscapeAssembler assembler, Dictionary<uint, WalkCell> cells,
|
||||
Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry> buildings) =
|
||||
BuildProductionWorld(adapter, frame.Pose!.CellId, frame.Pose.Origin);
|
||||
var ctx = new WalkTraceReplayContext(frame.Pose, cells) { Buildings = buildings };
|
||||
var walk = new RetailFrameWalk();
|
||||
var recorder = new Recorder();
|
||||
|
||||
walk.WalkFrame(frame.Pose.CellId, null, assembler.Landscape, ctx, recorder);
|
||||
|
||||
string expected = WalkTraceReplayContext.Signature(frame);
|
||||
string actual = WalkTraceReplayContext.Signature(recorder.Events);
|
||||
Assert.True(
|
||||
expected == actual,
|
||||
$"production walk diverged from the FW1 test adapter\nEXPECTED: {expected}\nACTUAL: {actual}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Doorway_still_first_frame_diff()
|
||||
{
|
||||
IReadOnlyList<WalkOracleFrame> frames = WalkOracleTrace.Load("posed/holtburg-doorway-still");
|
||||
Assert.NotEmpty(frames);
|
||||
using DatCollection dats = OpenDats();
|
||||
using var adapter = new DatCollectionAdapter(dats);
|
||||
WalkOracleFrame frame = frames[1];
|
||||
Assert.NotNull(frame.Pose);
|
||||
(WalkLandscapeAssembler assembler, Dictionary<uint, WalkCell> cells,
|
||||
Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry> buildings) =
|
||||
BuildProductionWorld(adapter, frame.Pose!.CellId, frame.Pose.Origin);
|
||||
var ctx = new WalkTraceReplayContext(frame.Pose, cells) { Buildings = buildings };
|
||||
WalkCell camera = Assert.Contains(frame.Pose.CellId, cells);
|
||||
// Matches WalkTraceConformanceTests.Doorway_still_first_frame_diff:
|
||||
// this capture ran under cdb load with Render::deg_mul depressed to
|
||||
// the portless-arm threshold. Same environment pin, same reason.
|
||||
var walk = new RetailFrameWalk { DegradeMultiplier = 0f };
|
||||
var recorder = new Recorder();
|
||||
|
||||
walk.WalkFrame(frame.Pose.CellId, camera, assembler.Landscape, ctx, recorder);
|
||||
|
||||
string expected = WalkTraceReplayContext.Signature(frame);
|
||||
string actual = WalkTraceReplayContext.Signature(recorder.Events);
|
||||
Assert.True(
|
||||
expected == actual,
|
||||
$"production walk diverged from the FW1 test adapter\nEXPECTED: {expected}\nACTUAL: {actual}");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("posed/terrace-center")]
|
||||
[InlineData("posed/terrace-edge")]
|
||||
[InlineData("posed/cathedral-arrival")]
|
||||
public void Still_fixture_first_frame_reproduces_exactly(string fixture)
|
||||
{
|
||||
IReadOnlyList<WalkOracleFrame> frames = WalkOracleTrace.Load(fixture);
|
||||
Assert.NotEmpty(frames);
|
||||
using DatCollection dats = OpenDats();
|
||||
using var adapter = new DatCollectionAdapter(dats);
|
||||
WalkOracleFrame frame = frames[1];
|
||||
Assert.NotNull(frame.Pose);
|
||||
(WalkLandscapeAssembler assembler, Dictionary<uint, WalkCell> cells,
|
||||
Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry> buildings) =
|
||||
BuildProductionWorld(adapter, frame.Pose!.CellId, frame.Pose.Origin);
|
||||
var ctx = new WalkTraceReplayContext(frame.Pose, cells) { Buildings = buildings };
|
||||
WalkCell? camera = (frame.Pose.CellId & 0xFFFFu) >= 0x100
|
||||
? Assert.Contains(frame.Pose.CellId, cells)
|
||||
: null;
|
||||
var walk = new RetailFrameWalk();
|
||||
var recorder = new Recorder();
|
||||
|
||||
walk.WalkFrame(frame.Pose.CellId, camera, assembler.Landscape, ctx, recorder);
|
||||
|
||||
string expected = WalkTraceReplayContext.Signature(frame);
|
||||
string actual = WalkTraceReplayContext.Signature(recorder.Events);
|
||||
Assert.True(
|
||||
expected == actual,
|
||||
$"production walk diverged from the FW1 test adapter ({fixture})\nEXPECTED: {expected}\nACTUAL: {actual}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Foundry_entry_reproduces_every_frame_before_the_f67_order_segment()
|
||||
{
|
||||
// Same F67-F79 parked segment as WalkTraceConformanceTests (the
|
||||
// building-a9b40036-root-plane viewpoint question is a walk/ported-
|
||||
// algorithm question, not a world-data question — out of scope for
|
||||
// this class). Frames 1-66 must still reproduce exactly.
|
||||
MovingFixtureReplay("posed/foundry-entry", stopBeforeFrame: 67);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("posed/holtburg-walkout")]
|
||||
[InlineData("posed/holtburg-transitions")]
|
||||
[InlineData("posed/holtburg-walkabout")]
|
||||
public void Moving_fixture_reproduces_every_pairable_frame(string fixture)
|
||||
=> MovingFixtureReplay(fixture);
|
||||
|
||||
private void MovingFixtureReplay(string fixture, int stopBeforeFrame = int.MaxValue)
|
||||
{
|
||||
IReadOnlyList<WalkOracleFrame> frames = WalkOracleTrace.Load(fixture);
|
||||
Assert.True(frames.Count >= 3);
|
||||
using DatCollection dats = OpenDats();
|
||||
using var adapter = new DatCollectionAdapter(dats);
|
||||
WalkOraclePose anchor = frames[1].Pose!;
|
||||
(WalkLandscapeAssembler assembler, Dictionary<uint, WalkCell> cells,
|
||||
Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry> buildings) =
|
||||
BuildProductionWorld(adapter, anchor.CellId, anchor.Origin);
|
||||
var walk = new RetailFrameWalk();
|
||||
|
||||
for (int n = 1; n < frames.Count - 1; n++)
|
||||
{
|
||||
WalkOracleFrame frame = frames[n];
|
||||
if (frame.Number >= stopBeforeFrame) break;
|
||||
string expected = WalkTraceReplayContext.Signature(frame);
|
||||
string? firstActual = null;
|
||||
bool matched = false;
|
||||
foreach (WalkOraclePose pose in new[] { frames[n + 1].Pose!, frame.Pose! })
|
||||
{
|
||||
Assert.NotNull(pose);
|
||||
assembler.SetViewer(pose.CellId, pose.Origin);
|
||||
var ctx = new WalkTraceReplayContext(pose, cells) { Buildings = buildings };
|
||||
WalkCell? camera = null;
|
||||
if ((pose.CellId & 0xFFFFu) >= 0x100)
|
||||
{
|
||||
Assert.True(
|
||||
cells.TryGetValue(pose.CellId, out camera),
|
||||
$"frame {frame.Number}: interior camera cell {pose.CellId:x8} not loaded");
|
||||
}
|
||||
var recorder = new Recorder();
|
||||
walk.WalkFrame(pose.CellId, camera, assembler.Landscape, ctx, recorder);
|
||||
string actual = WalkTraceReplayContext.Signature(recorder.Events);
|
||||
firstActual ??= actual;
|
||||
if (actual == expected)
|
||||
{
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Assert.True(
|
||||
matched,
|
||||
$"frame {frame.Number} diverged under both adjacent poses ({fixture})\n"
|
||||
+ $"EXPECTED: {expected}\nACTUAL: {firstActual}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Foundry_deep_reproduces_every_complete_frame_exactly()
|
||||
{
|
||||
IReadOnlyList<WalkOracleFrame> frames = WalkOracleTrace.Load("posed/foundry-deep");
|
||||
Assert.NotEmpty(frames);
|
||||
using DatCollection dats = OpenDats();
|
||||
using var adapter = new DatCollectionAdapter(dats);
|
||||
Dictionary<uint, WalkCell> cells =
|
||||
WalkCellFactory.BuildInteriorCells(adapter, 0xA9B40000u, Vector3.Zero);
|
||||
var landscape = new WalkLandscape { MidWidth = 1, Blocks = new WalkLandBlock?[1] };
|
||||
|
||||
foreach (WalkOracleFrame frame in frames)
|
||||
{
|
||||
Assert.NotNull(frame.Pose);
|
||||
WalkCell camera = Assert.Contains(frame.Pose!.CellId, cells);
|
||||
var ctx = new WalkTraceReplayContext(frame.Pose, cells);
|
||||
var walk = new RetailFrameWalk();
|
||||
var recorder = new Recorder();
|
||||
|
||||
walk.WalkFrame(frame.Pose.CellId, camera, landscape, ctx, recorder);
|
||||
|
||||
Assert.Equal(
|
||||
WalkTraceReplayContext.Signature(frame),
|
||||
WalkTraceReplayContext.Signature(recorder.Events));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -89,6 +89,74 @@ public sealed class LandblockBuildFactoryTests
|
|||
Assert.Empty(result.Collisions.EnvCells);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildNear_PopulatesWalkZSlabUnconditionallyAndWalkBuildingsFromLandBlockInfo()
|
||||
{
|
||||
// Campaign FW3.1: EnvCellLandblockBuild.WalkMaxZ/WalkMinZ/WalkBuildings
|
||||
// must be populated by the production streaming build — hermetic
|
||||
// (no installed DAT needed), mirroring the RecordingDatProxy fixture
|
||||
// pattern the rest of this file already uses.
|
||||
var dat = CreateDat(out RecordingDatProxy proxy);
|
||||
AddNearFixture(proxy, LandblockId, environmentId: 1);
|
||||
var heights = new byte[81];
|
||||
Array.Fill(heights, (byte)10);
|
||||
heights[0] = 200; // one outlier byte -> a distinct maxByte from the rest.
|
||||
proxy.Add(LandblockId, new LandBlock { Id = LandblockId, Height = heights });
|
||||
proxy.Add(
|
||||
(LandblockId & 0xFFFF0000u) | 0xFFFEu,
|
||||
new LandBlockInfo
|
||||
{
|
||||
NumCells = 1,
|
||||
Buildings = new List<BuildingInfo>
|
||||
{
|
||||
new BuildingInfo
|
||||
{
|
||||
ModelId = 0x01234567u,
|
||||
Frame = new Frame
|
||||
{
|
||||
Origin = new Vector3(12f, 12f, 0f),
|
||||
Orientation = Quaternion.Identity,
|
||||
},
|
||||
Portals = new List<BuildingPortal>(),
|
||||
},
|
||||
},
|
||||
});
|
||||
var heightTable = new float[256];
|
||||
heightTable[10] = 5f;
|
||||
heightTable[200] = 40f;
|
||||
var factory = new LandblockBuildFactory(
|
||||
dat, TestPreparedCollisionSource.Instance, new object(), heightTable);
|
||||
|
||||
LandblockBuild? result = factory.Build(Request(LandblockStreamJobKind.LoadNear));
|
||||
|
||||
Assert.NotNull(result);
|
||||
var envCells = Assert.IsType<AcDream.App.Rendering.Wb.EnvCellLandblockBuild>(
|
||||
result.EnvCells);
|
||||
Assert.Equal(240f, envCells.WalkMaxZ); // heightTable[200] + 200
|
||||
Assert.Equal(4f, envCells.WalkMinZ); // heightTable[10] - 1
|
||||
AcDream.App.Rendering.Walk.WalkBuildingFactory.Entry buildingEntry =
|
||||
Assert.Single(envCells.WalkBuildings);
|
||||
// origin (12,12) -> cellX=cellY=0 -> low word 0*8+0+1 = 1.
|
||||
Assert.Equal((LandblockId & 0xFFFF0000u) | 1u, buildingEntry.Building.PositionCellId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildFar_NeverPopulatesWalkDataBecauseEnvCellsIsNull()
|
||||
{
|
||||
// Documents the known FW3.1 scope gap: far-tier (LoadFar) landblocks
|
||||
// carry no EnvCellLandblockBuild transaction at all (terrain-only),
|
||||
// so their z-slab/buildings never reach WalkLandscapeAssembler until
|
||||
// a follow-up wires the far-tier path too.
|
||||
var dat = CreateDat(out RecordingDatProxy proxy);
|
||||
proxy.Add(LandblockId, new LandBlock { Id = LandblockId });
|
||||
var factory = Factory(dat, new object());
|
||||
|
||||
LandblockBuild? result = factory.Build(Request(LandblockStreamJobKind.LoadFar));
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Null(result.EnvCells);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NearPreparedFaultRejectsWholeGenerationWhileFarNeverReadsCollision()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue