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>
131 lines
5.6 KiB
C#
131 lines
5.6 KiB
C#
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));
|
|
}
|
|
}
|