feat(render) Campaign FW1: the landscape replay world + first outdoor diff

WalkLandscapeDatBuilder assembles the replay grid with the LIVE-dumped
retail landscape model (LScape mid_radius=25 -> 51x51; the resolution
pyramid observed on the capture client: side_cell_count 8 in the 3x3
core, 4 at ring 2, 2 at rings 3-4, 1 beyond; buildings attach only to
full-res blocks - matching the traces roster), retail z-slabs
(heightTable[max]+200 / [min]-1 per CLandBlock unpack @0052f297), and
camera-block-local coordinates. The replay context gains the building
half (placements, active-view install, building-polygon clip). The
street-outdoor conformance diff now reaches real adjudication: the walk
over-culls six traced buildings (jagged boundary - not a clean frustum
edge) and the look-in floods differ; the driving test is parked Skip
while the visibility map is instrumented.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-30 10:36:09 +02:00
parent e8df809c40
commit e3f0c6a1a1
5 changed files with 222 additions and 21 deletions

View file

@ -0,0 +1,120 @@
using System.Numerics;
using AcDream.App.Rendering.Walk;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
namespace AcDream.App.Tests.Rendering.Walk;
/// <summary>
/// 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
/// (<c>CLandBlock::calc_lighting</c>-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.
/// </summary>
public static class WalkLandscapeDatBuilder
{
public sealed record BuiltWorld(
WalkLandscape Landscape,
Dictionary<uint, WalkCell> Cells,
Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry> Buildings);
/// <summary>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 34, 1 beyond. Buildings
/// attach only to full-res blocks (the traces show no BLD beyond ±1).</summary>
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<Region>(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<uint, WalkCell>();
var buildings = new Dictionary<WalkBuilding, WalkWorldDatAdapter.BuildingEntry>();
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<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;
}
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);
}
}