diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkLandscapeDatBuilder.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkLandscapeDatBuilder.cs new file mode 100644 index 00000000..8f8aab54 --- /dev/null +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkLandscapeDatBuilder.cs @@ -0,0 +1,120 @@ +using System.Numerics; +using AcDream.App.Rendering.Walk; +using DatReaderWriter; +using DatReaderWriter.DBObjs; + +namespace AcDream.App.Tests.Rendering.Walk; + +/// +/// Campaign FW1 — assembles the replay world for outdoor/landscape +/// fixtures: retail's 11×11 viewer-centered block grid (mid_radius 5) in +/// CAMERA-BLOCK-LOCAL coordinates (the same space the pose origin and the +/// per-block corner math use), each block with its retail z-slab +/// (CLandBlock::calc_lighting-adjacent unpack @0x0052f1d0: +/// max_zval = heightTable[maxByte] + 200, min_zval = heightTable[minByte] +/// − 1), buildings placed at their landcell indices with landblock-local +/// transforms offset by the block delta, and every neighbor block's +/// interior cells merged into one registry. +/// +public static class WalkLandscapeDatBuilder +{ + public sealed record BuiltWorld( + WalkLandscape Landscape, + Dictionary Cells, + Dictionary Buildings); + + /// The capture client's live landscape grid (dumped 2026-08-30: + /// LScape mid_radius=25 → a 51×51 grid — the owner's landscape-radius + /// setting) with the resolution pyramid observed live: side_cell_count 8 + /// in the 3×3 core, 4 at ring 2, 2 at rings 3–4, 1 beyond. Buildings + /// attach only to full-res blocks (the traces show no BLD beyond ±1). + public const int MidRadius = 25; + + private static int SideCellCountForRing(int ring) + => ring <= 1 ? 8 : ring == 2 ? 4 : ring <= 4 ? 2 : 1; + + public static BuiltWorld Build(DatCollection dats, uint cameraCellId) + { + Region region = (Region)dats.Get(0x13000000u)!; + float[] heightTable = region.LandDefs.LandHeightTable; + + int cameraBlockX = (int)(cameraCellId >> 24); + int cameraBlockY = (int)((cameraCellId >> 16) & 0xFF); + const int midRadius = MidRadius; + const int midWidth = midRadius * 2 + 1; + + var landscape = new WalkLandscape + { + MidWidth = midWidth, + Blocks = new WalkLandBlock?[midWidth * midWidth], + ViewerBlockX = midRadius, + ViewerBlockY = midRadius, + }; + uint low = cameraCellId & 0xFFFFu; + if (low >= 1 && low <= 0x40) + { + int cellIndex = (int)low - 1; + landscape.ViewerCellX = cellIndex / 8; + landscape.ViewerCellY = cellIndex % 8; + } + + var cells = new Dictionary(); + var buildings = new Dictionary(); + + for (int gx = 0; gx < midWidth; gx++) + { + for (int gy = 0; gy < midWidth; gy++) + { + int blockX = cameraBlockX + gx - midRadius; + int blockY = cameraBlockY + gy - midRadius; + if (blockX < 0 || blockX > 0xFF || blockY < 0 || blockY > 0xFF) + continue; + uint landblockId = (uint)((blockX << 24) | (blockY << 16)); + if (dats.Get(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; + } + int ring = Math.Max(Math.Abs(gx - midRadius), Math.Abs(gy - midRadius)); + int sideCellCount = SideCellCountForRing(ring); + var block = new WalkLandBlock + { + SideCellCount = sideCellCount, + MaxZ = heightTable[maxByte] + 200f, + MinZ = heightTable[minByte] - 1f, + }; + block.EnsureCellArrays(); + + var blockOffset = new Vector3( + (gx - midRadius) * WalkLandscape.BlockLength, + (gy - midRadius) * WalkLandscape.BlockLength, + 0f); + + if (sideCellCount == 8) + { + foreach (WalkWorldDatAdapter.BuildingEntry entry + in WalkWorldDatAdapter.BuildBuildings(dats, landblockId)) + { + Matrix4x4 world = entry.WorldTransform + * Matrix4x4.CreateTranslation(blockOffset); + Matrix4x4.Invert(world, out Matrix4x4 inverse); + var placed = new WalkWorldDatAdapter.BuildingEntry( + entry.Building, world, inverse); + buildings[entry.Building] = placed; + int cellIndex = (int)(entry.Building.PositionCellId & 0xFFFFu) - 1; + if (cellIndex >= 0 && cellIndex < 64) + block.CellBuildings[cellIndex] = entry.Building; + } + WalkWorldDatAdapter.BuildInteriorCells( + dats, landblockId, blockOffset, cells); + } + landscape.Blocks[gx * midWidth + gy] = block; + } + } + return new BuiltWorld(landscape, cells, buildings); + } +} diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.cs index 8c064669..fde4de27 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.cs @@ -32,6 +32,37 @@ public sealed class WalkTraceConformanceTests return new DatCollection(datDir!, DatAccessType.Read); } + [Fact(Skip = "FW1 adjudication in progress: the walk over-culls six traced " + + "buildings (incl. block a9b3) and the look-in floods differ — see the " + + "campaign memory. Re-enable as the outdoor gate once adjudicated.")] + public void Street_outdoor_first_frame_diff() + { + // The first landscape-involving conformance case: diff-first (the + // assert prints both signatures on mismatch for adjudication). + IReadOnlyList frames = + WalkOracleTrace.Load("posed/holtburg-street-outdoor"); + Assert.NotEmpty(frames); + using DatCollection dats = OpenDats(); + WalkOracleFrame frame = frames[1]; // frame 2: pose settled (marker timing) + Assert.NotNull(frame.Pose); + WalkLandscapeDatBuilder.BuiltWorld world = + WalkLandscapeDatBuilder.Build(dats, frame.Pose!.CellId); + var ctx = new WalkTraceReplayContext(frame.Pose, world.Cells) + { + Buildings = world.Buildings, + }; + var walk = new RetailFrameWalk(); + var recorder = new Recorder(); + + walk.WalkFrame(frame.Pose.CellId, null, world.Landscape, ctx, recorder); + + string expected = WalkTraceReplayContext.Signature(frame); + string actual = WalkTraceReplayContext.Signature(recorder.Events); + Assert.True( + expected == actual, + $"walk diverged from retail\nEXPECTED: {expected}\nACTUAL: {actual}"); + } + [Fact] public void Foundry_deep_reproduces_every_complete_frame_exactly() { diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceReplay.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceReplay.cs index 76778d58..7cf72809 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceReplay.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceReplay.cs @@ -72,6 +72,14 @@ public sealed class WalkTraceReplayContext : IWalkFrameContext, IRetailFrameWalk CyPlane = new WalkPlane(forward, -Vector3.Dot(pose.Origin, forward) - 0.1f); } + /// Building placements (camera-block-local) for the landscape + /// fixtures; empty for the interior-only ones. + public Dictionary Buildings { get; set; } + = new(); + + private Vector2[] _activeViewVerts = new Vector2[32]; + private int _activeViewVertCount; + public Vector3 ViewpointIn(WalkCell cell) => Vector3.Transform(WorldViewpoint, cell.InverseWorldTransform); @@ -85,13 +93,33 @@ public sealed class WalkTraceReplayContext : IWalkFrameContext, IRetailFrameWalk public float ViewportHeight { get; } public WalkPlane CyPlane { get; } public IWalkFrameContext CellContext => this; - public void SetActiveView(WalkPortalView views, int index) { } - public Vector3 ViewpointInBuilding(WalkBuilding building) => WorldViewpoint; + public void SetActiveView(WalkPortalView views, int index) + { + WalkViewPoly poly = views.View.Polys[index]; + if (_activeViewVerts.Length < poly.VertexCount) + _activeViewVerts = new Vector2[poly.VertexCount]; + for (int k = 0; k < poly.VertexCount; k++) + _activeViewVerts[k] = views.View.Vertices[poly.VertexIndex + k].Point; + _activeViewVertCount = poly.VertexCount; + } + + public Vector3 ViewpointInBuilding(WalkBuilding building) + => Vector3.Transform(WorldViewpoint, Buildings[building].InverseWorldTransform); public int ClipBuildingPolygon( WalkBuilding building, WalkPolygon polygon, int side, Span output) - => 0; // building look-ins join the conformance surface with the landscape fixtures + { + Matrix4x4 objectToClip = Buildings[building].WorldTransform * _viewProjection; + Span projected = stackalloc WalkScreenPoint[polygon.Vertices.Length]; + for (int i = 0; i < polygon.Vertices.Length; i++) + projected[i] = WalkScreenClip.TransformToScreen( + polygon.Vertices[i], objectToClip, ViewportWidth, ViewportHeight); + if (side != 0) + projected.Reverse(); + return WalkScreenClip.ClipAgainstView( + projected, _activeViewVerts.AsSpan(0, _activeViewVertCount), output); + } // ---- signatures for comparing walk output to oracle frames ---- diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapter.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapter.cs index db7d6bd8..a8393cff 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapter.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapter.cs @@ -32,7 +32,14 @@ using AcDream.App.Rendering.Walk; /// public static class WalkWorldDatAdapter { + /// One built building plus its landblock-local placement. + public sealed record BuildingEntry( + WalkBuilding Building, Matrix4x4 WorldTransform, Matrix4x4 InverseWorldTransform); + public static WalkCell? BuildCell(DatCollection dats, uint cellId) + => BuildCell(dats, cellId, Vector3.Zero); + + public static WalkCell? BuildCell(DatCollection dats, uint cellId, Vector3 blockOffset) { if (dats.Get(cellId) is not EnvCell envCell) return null; @@ -67,9 +74,9 @@ public static class WalkWorldDatAdapter Matrix4x4 worldTransform = Matrix4x4.CreateFromQuaternion(envCell.Position.Orientation) * Matrix4x4.CreateTranslation( - envCell.Position.Origin.X, - envCell.Position.Origin.Y, - envCell.Position.Origin.Z); + envCell.Position.Origin.X + blockOffset.X, + envCell.Position.Origin.Y + blockOffset.Y, + envCell.Position.Origin.Z + blockOffset.Z); Matrix4x4.Invert(worldTransform, out Matrix4x4 inverse); return new WalkCell { @@ -84,12 +91,17 @@ public static class WalkWorldDatAdapter public static Dictionary BuildInteriorCells( DatCollection dats, uint landblockId) + => BuildInteriorCells(dats, landblockId, Vector3.Zero, null); + + public static Dictionary BuildInteriorCells( + DatCollection dats, uint landblockId, Vector3 blockOffset, + Dictionary? into) { uint lbMask = landblockId & 0xFFFF0000u; - var cells = new Dictionary(); + Dictionary cells = into ?? new Dictionary(); for (uint low = 0x0100; low <= 0xFFFD; low++) { - WalkCell? cell = BuildCell(dats, lbMask | low); + WalkCell? cell = BuildCell(dats, lbMask | low, blockOffset); if (cell is null) break; // interior cells are contiguous from 0x0100 cells[cell.CellId] = cell; } @@ -97,11 +109,12 @@ public static class WalkWorldDatAdapter } /// Buildings of one landblock, with their walk event ids - /// (containing landcell), portal records, and drawing-BSP portal trees. - public static List BuildBuildings(DatCollection dats, uint landblockId) + /// (containing landcell), portal records, drawing-BSP portal trees, and + /// landblock-local placements. + public static List BuildBuildings(DatCollection dats, uint landblockId) { uint lbMask = landblockId & 0xFFFF0000u; - var result = new List(); + var result = new List(); if (dats.Get(lbMask | 0xFFFEu) is not LandBlockInfo info || info.Buildings is null) { @@ -138,12 +151,19 @@ public static class WalkWorldDatAdapter if (dats.Get(buildingInfo.ModelId) is GfxObj gfxObj) bsp = ConvertDrawingBsp(gfxObj, gfxObj.DrawingBSP?.Root); - result.Add(new WalkBuilding - { - PositionCellId = positionCellId, - Portals = portals, - DrawingBsp = bsp, - }); + Matrix4x4 worldTransform = + Matrix4x4.CreateFromQuaternion(buildingInfo.Frame.Orientation) + * Matrix4x4.CreateTranslation(origin); + Matrix4x4.Invert(worldTransform, out Matrix4x4 inverse); + result.Add(new BuildingEntry( + new WalkBuilding + { + PositionCellId = positionCellId, + Portals = portals, + DrawingBsp = bsp, + }, + worldTransform, + inverse)); } return result; } diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapterTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapterTests.cs index 38fb831a..ce777f60 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapterTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapterTests.cs @@ -60,8 +60,9 @@ public sealed class WalkWorldDatAdapterTests { using DatCollection dats = OpenDats(); - List buildings = WalkWorldDatAdapter.BuildBuildings(dats, landblockId); - var roster = buildings.Select(b => b.PositionCellId).ToHashSet(); + List buildings = + WalkWorldDatAdapter.BuildBuildings(dats, landblockId); + var roster = buildings.Select(e => e.Building.PositionCellId).ToHashSet(); foreach (uint traced in tracedBuildingIds) Assert.Contains(traced, roster); @@ -72,9 +73,10 @@ public sealed class WalkWorldDatAdapterTests { using DatCollection dats = OpenDats(); - List buildings = WalkWorldDatAdapter.BuildBuildings(dats, 0xF4180000u); + List buildings = + WalkWorldDatAdapter.BuildBuildings(dats, 0xF4180000u); WalkBuilding sanctuary = Assert.Single( - buildings, b => b.PositionCellId == 0xF418000Au); + buildings, e => e.Building.PositionCellId == 0xF418000Au).Building; Assert.NotEmpty(sanctuary.Portals); Assert.All(sanctuary.Portals, p => Assert.NotEmpty(p.StabList));