acdream/tests/AcDream.App.Tests/Rendering/Walk/WalkLandscapeDatBuilder.cs
Erik 66f9e0d459 feat(render) Campaign FW1: SIX OF TEN FIXTURES FULLY CONFORMANT
Three final pins complete the still-fixture set: (1) the CELL portal
side decode is the INVERSE of the 0x2 bit (uniform with the building
convention; the doorway-still flood proved it - ov=2 n=3 exact, and
foundry-deep stays green); (2) interior frames key the landscape order
off the OUTSIDE-projected landcell (get_outside_cell_id - derived from
the camera origin); (3) the outdoor pview has draw_landscape=FALSE so
look-in floods discard exit portals - the ov=0 pattern of every traced
look-in. CONFORMANT: foundry-deep (every frame), doorway-still,
street-outdoor, terrace-center, terrace-edge (the #456 acceptance
pose), cathedral-arrival - full frames identical to retail. The moving
four diverge only at punch-edge frames (walkabout F9, foundry-entry
F67) - pose-timing sensitivity parked in the driver Skip note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 11:07:29 +02:00

169 lines
7.7 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;
/// <summary>Per-frame viewer placement for moving replays (the camera
/// stays within the anchor block in every moving fixture).</summary>
public static void SetViewer(WalkLandscape landscape, uint cameraCellId, Vector3 cameraOrigin)
{
uint low = cameraCellId & 0xFFFFu;
if (low >= 1 && low <= 0x40)
{
int cellIndex = (int)low - 1;
landscape.ViewerCellX = cellIndex / 8;
landscape.ViewerCellY = cellIndex % 8;
}
else
{
landscape.ViewerCellX = Math.Clamp((int)MathF.Floor(cameraOrigin.X / 24f), 0, 7);
landscape.ViewerCellY = Math.Clamp((int)MathF.Floor(cameraOrigin.Y / 24f), 0, 7);
}
}
public static BuiltWorld Build(DatCollection dats, uint cameraCellId, Vector3 cameraOrigin = default)
{
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;
}
else
{
// Interior camera: retail keys the landscape order off the
// OUTSIDE-projected landcell (Position::get_outside_cell_id via
// SmartBox::RenderNormalMode seen_outside arm) - derive it from
// the camera origin (block-local, 24 m cells).
landscape.ViewerCellX = Math.Clamp((int)MathF.Floor(cameraOrigin.X / 24f), 0, 7);
landscape.ViewerCellY = Math.Clamp((int)MathF.Floor(cameraOrigin.Y / 24f), 0, 7);
}
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;
// 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 — NOT every interior. The flood halts at
// unloaded cells (GetVisible null), which is what
// limits retail's look-in punches to nearby doorways.
foreach (WalkBldPortal portal in entry.Building.Portals)
{
if (portal.OtherCellId != 0xFFFFFFFFu
&& !cells.ContainsKey(portal.OtherCellId))
{
WalkCell? c = WalkWorldDatAdapter.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 = WalkWorldDatAdapter.BuildCell(
dats, stab, blockOffset);
if (c is not null) cells[c.CellId] = c;
}
}
}
}
landscape.Blocks[gx * midWidth + gy] = block;
}
}
return new BuiltWorld(landscape, cells, buildings);
}
}