using System.Numerics; using AcDream.App.Rendering.Walk; using DatReaderWriter; using DatReaderWriter.DBObjs; namespace AcDream.App.Tests.Rendering.Walk; /// /// 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 /// (CLandBlock::calc_lighting-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. /// public static class WalkLandscapeDatBuilder { public sealed record BuiltWorld( WalkLandscape Landscape, Dictionary Cells, Dictionary Buildings); /// 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). public const int MidRadius = 25; private static int SideCellCountForRing(int ring) => ring <= 1 ? 8 : ring == 2 ? 4 : ring <= 4 ? 2 : 1; /// Per-frame viewer placement for moving replays (the camera /// stays within the anchor block in every moving fixture). 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(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(); var buildings = new Dictionary(); 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(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); } }