acdream/tests/AcDream.App.Tests/Rendering/Walk/WalkLandscapeDatBuilder.cs
Erik e3f0c6a1a1 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>
2026-08-30 10:36:09 +02:00

120 lines
5.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}
}