using System.Numerics;
namespace AcDream.App.Rendering.Walk;
///
/// Campaign FW3.1 — the production sibling of the FW1 conformance harness's
/// WalkLandscapeDatBuilder
/// (tests/AcDream.App.Tests/Rendering/Walk/WalkLandscapeDatBuilder.cs): owns
/// retail's viewer-centred block grid (LScape mid_radius = 25 → a
/// 51×51 window, recon 2026-08-30) and its LOD ring pyramid
/// ().
///
/// Unlike the test builder — which re-scans the whole grid from a
/// DatCollection on every call — this class is fed INCREMENTALLY from
/// landblock commit/retire (/
/// ), the same lifecycle
/// Wb.BuildingRegistry and CellVisibility already follow. Each
/// landblock's z-slab and building placements are computed ONCE, on the
/// streaming worker thread, at landblock-build time
/// (LandblockBuildFactory/WalkBuildingFactory) — publishing
/// here is a pure dictionary write, no DAT or GfxObj work.
///
/// recentres the grid on the camera's block, porting
/// the test builder's viewer-cell recompute; it goes further by also
/// reflowing the 51×51 window when the camera crosses into a different
/// landblock (the test builder never needs this — every moving fixture stays
/// within its anchor block). Reflow only touches already-published data (a
/// dictionary lookup per slot); no DAT access ever happens here.
///
/// No frame currently calls or reads
/// — FW3.2 wires RetailFrameWalk into the
/// render loop. This class exists now so that wiring is additive, and so the
/// FW3.1 conformance gate can exercise the SAME assembly code path
/// production will drive.
///
public sealed class WalkLandscapeAssembler
{
/// Retail LScape mid_radius (recon 2026-08-30) — NOT
/// acdream's streaming radius (two-tier N1/N2), which is smaller. Blocks
/// outside the streamed window simply stay unpublished (null slots).
public const int MidRadius = 25;
public const int GridWidth = MidRadius * 2 + 1;
private sealed class BlockData
{
public required float MaxZ;
public required float MinZ;
public required IReadOnlyList Buildings;
}
private readonly Dictionary<(int X, int Y), BlockData> _blocks = new();
private int _viewerBlockX = int.MinValue;
private int _viewerBlockY = int.MinValue;
public WalkLandscape Landscape { get; } = new()
{
MidWidth = GridWidth,
Blocks = new WalkLandBlock?[GridWidth * GridWidth],
ViewerBlockX = MidRadius,
ViewerBlockY = MidRadius,
};
/// Landblock commit
/// (LandblockRenderPublisher.AdvanceCompleteOne's
/// BuildingRegistryCommitted step): register/replace this
/// landblock's z-slab and building placements. If the landblock falls
/// within the CURRENT window, the visible slot refreshes immediately.
public void PublishLandblock(
uint landblockId, float maxZ, float minZ,
IReadOnlyList buildings)
{
(int bx, int by) = BlockCoords(landblockId);
_blocks[(bx, by)] = new BlockData { MaxZ = maxZ, MinZ = minZ, Buildings = buildings };
RefreshSlotIfWindowed(bx, by);
}
/// Landblock retirement (LandblockRenderPublisher
/// .RemoveBuildingRegistry): drop this landblock's data. Safe to call
/// on a landblock that never published (no-op).
public void RetireLandblock(uint landblockId)
{
(int bx, int by) = BlockCoords(landblockId);
_blocks.Remove((bx, by));
RefreshSlotIfWindowed(bx, by);
}
/// Recentres the grid on the camera's block and updates the
/// sub-block cell offsets (retail's viewer recentre —
/// WalkLandscapeDatBuilder.SetViewer's port). When the camera's
/// block hasn't changed since the last call, this only updates
/// /
/// — zero allocation, matching the test builder's lightweight path. A
/// block crossing additionally reflows the 51×51 window from already-
/// published data (a bounded, allocation-free dictionary-lookup pass —
/// no DAT/GfxObj work, which already happened at publish time).
public void SetViewer(uint cameraCellId, Vector3 cameraOrigin)
{
int cameraBlockX = (int)(cameraCellId >> 24);
int cameraBlockY = (int)((cameraCellId >> 16) & 0xFF);
if (cameraBlockX != _viewerBlockX || cameraBlockY != _viewerBlockY)
{
_viewerBlockX = cameraBlockX;
_viewerBlockY = cameraBlockY;
RebuildWindow();
}
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: derive the outside-projected landcell from
// the camera origin, matching the test builder's fallback
// (Position::get_outside_cell_id via SmartBox::RenderNormalMode's
// seen_outside arm).
Landscape.ViewerCellX = Math.Clamp((int)MathF.Floor(cameraOrigin.X / 24f), 0, 7);
Landscape.ViewerCellY = Math.Clamp((int)MathF.Floor(cameraOrigin.Y / 24f), 0, 7);
}
}
/// ring <= 1 ? 8 : ring == 2 ? 4 : ring <= 4 ? 2 : 1
/// — the live-observed resolution pyramid (recon 2026-08-30): 8×8 in the
/// 3×3 core, 4×4 at ring 2, 2×2 at rings 3–4, 1×1 beyond. Buildings only
/// attach at full resolution (ring ≤ 1) — the traces show no BLD beyond
/// ±1 block.
internal static int SideCellCountForRing(int ring)
=> ring <= 1 ? 8 : ring == 2 ? 4 : ring <= 4 ? 2 : 1;
internal static int RingOf(int gridX, int gridY)
=> Math.Max(Math.Abs(gridX - MidRadius), Math.Abs(gridY - MidRadius));
private void RebuildWindow()
{
for (int gx = 0; gx < GridWidth; gx++)
{
for (int gy = 0; gy < GridWidth; gy++)
{
int bx = _viewerBlockX + gx - MidRadius;
int by = _viewerBlockY + gy - MidRadius;
Landscape.Blocks[gx * GridWidth + gy] = BuildSlot(bx, by, gx, gy);
}
}
}
private void RefreshSlotIfWindowed(int bx, int by)
{
if (_viewerBlockX == int.MinValue)
return; // SetViewer has never run — no window to refresh yet.
int gx = bx - _viewerBlockX + MidRadius;
int gy = by - _viewerBlockY + MidRadius;
if (gx < 0 || gx >= GridWidth || gy < 0 || gy >= GridWidth)
return;
Landscape.Blocks[gx * GridWidth + gy] = BuildSlot(bx, by, gx, gy);
}
private WalkLandBlock? BuildSlot(int bx, int by, int gx, int gy)
{
if (bx < 0 || bx > 0xFF || by < 0 || by > 0xFF)
return null;
if (!_blocks.TryGetValue((bx, by), out BlockData? data))
return null;
int sideCellCount = SideCellCountForRing(RingOf(gx, gy));
var block = new WalkLandBlock
{
// Additive (Campaign FW3.2b-1): same (bx<<24 | by<<16) encoding
// BlockCoords decodes below — see WalkLandBlock.LandblockId.
LandblockId = (uint)bx << 24 | (uint)by << 16,
SideCellCount = sideCellCount,
MaxZ = data.MaxZ,
MinZ = data.MinZ,
};
block.EnsureCellArrays();
if (sideCellCount == 8)
{
foreach (WalkBuildingFactory.Entry entry in data.Buildings)
{
int cellIndex = (int)(entry.Building.PositionCellId & 0xFFFFu) - 1;
if (cellIndex >= 0 && cellIndex < 64)
block.CellBuildings[cellIndex] = entry.Building;
}
}
return block;
}
private static (int X, int Y) BlockCoords(uint landblockId)
=> ((int)((landblockId >> 24) & 0xFFu), (int)((landblockId >> 16) & 0xFFu));
}