The offline degrade probe proved the mechanism: every Holtburg building carries PORT nodes ONLY in its level-0 GfxObj (out to ~24-48 m); every degraded level has zero. Retail walks the CURRENT degrade level BSP (part->gfxobj[deg_level]) - that is what limits look-in punches to the nearest buildings. WalkBuilding gains the degrade ladder + SelectDrawingBsp (band pick; UpdateViewerDistance hysteresis is a port TODO), the walk selects per viewer distance, the adapter builds per-level BSPs, and the stab-list load rule (CLandBlock::init_buildings @0052fd80: a full-res block loads exactly its buildings portal stab cells) replaces load-everything in the landscape builder. The sweep now shows clean rosters with all far-building punches gone; remaining deltas: the near buildings 001a/0022 (50 m/28 m center distance vs the 48 m band edge - sphere-adjusted distance/hysteresis to port) and the one ring-1 frustum boundary pair (aab50002/a9b3003c). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
142 lines
6.4 KiB
C#
142 lines
6.4 KiB
C#
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 3–4, 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;
|
||
|
||
// 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);
|
||
}
|
||
}
|