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