namespace AcDream.App.Rendering.Walk; /// /// Campaign FW1 — retail's landscape draw-order machinery, ported from the /// named 2013 decomp (model: docs/research/2026-08-30-fw-walk-pseudocode.md §3). /// /// Retail enumerates viewer-centered Chebyshev rings with one shared 8-slot /// coefficient pattern at BOTH grid levels: LScape::get_block_order /// @0x00504c50 builds the landblock draw list NEAR-TO-FAR (viewer block at /// index 0; LScape::draw @0x00506330 then walks it BACKWARDS = /// far-to-near), and CLandBlock::calc_sq_draw_order @0x0052f4a0 fills /// the per-block cell array BACKWARDS from the closest cell (forward walk = /// far-to-near). The coefficient tables were byte-decoded from the /// PDB-paired binary (block tables VA 0x0081cc6c..0x0081cd2b, cell tables /// VA 0x0081df88..0x0081e047 — the two sets are IDENTICAL). /// public static class LandWalkOrder { // The shared 8-slot ring pattern. For ring r, step s (0..r-1), slot k // emits offset (dx, dy) = (XStep[k]*s + XRing[k]*r + XConst[k], // YStep[k]*s + YRing[k]*r + YConst[k]). // Expanded: (-s,+r) (-r,-s) (+s,-r) (+r,+s) (+s+1,+r) (-r,+s+1) // (-s-1,-r) (+r,-s-1) — tiles each ring's 8r slots exactly once. // Byte-decoded 2026-08-30; do not "simplify" the visit order — it decides // draw order between equidistant blocks/cells and the oracle traces pin it. private static readonly int[] XConst = [0, 0, 0, 0, 1, 0, -1, 0]; private static readonly int[] XRing = [0, -1, 0, 1, 0, -1, 0, 1]; private static readonly int[] XStep = [-1, 0, 1, 0, 1, 0, -1, 0]; private static readonly int[] YConst = [0, 0, 0, 0, 0, 1, 0, -1]; private static readonly int[] YStep = [0, -1, 0, 1, 0, 1, 0, -1]; private static readonly int[] YRing = [1, 0, -1, 0, 1, 0, -1, 0]; /// /// LScape::get_block_order @0x00504c50: fills /// with grid slots (x * width + y) near-to-far — /// the viewer's slot first, then rings outward, skipping out-of-bounds /// slots. Returns the number of slots written (= width² when the viewer /// is inside the grid). The caller draws by walking the result BACKWARDS. /// public static int GetBlockOrder(int viewerX, int viewerY, int width, Span order) { int count = 0; order[count++] = viewerX * width + viewerY; int maxRing = MaxRingTo(viewerX, viewerY, width, width); for (int ring = 1; ring <= maxRing; ring++) { for (int step = 0; step < ring; step++) { for (int slot = 0; slot < 8; slot++) { int x = XStep[slot] * step + XRing[slot] * ring + XConst[slot] + viewerX; int y = YStep[slot] * step + YRing[slot] * ring + YConst[slot] + viewerY; if (x >= 0 && x < width && y >= 0 && y < width) order[count++] = x * width + y; } } } return count; } /// /// CLandBlock::calc_sq_draw_order @0x0052f4a0 (ring half): fills /// with cell slots (x * side + y) so that a /// FORWARD walk visits cells far-to-near — the closest cell is written to /// the LAST index and rings fill backwards from there. The array is /// exactly filled (retail's --k reaches 0). /// public static void FillCellOrderFarToNear(int closestX, int closestY, int side, Span order) { int k = side * side; order[--k] = closestX * side + closestY; int maxRing = MaxRingTo(closestX, closestY, side, side); for (int ring = 1; ring <= maxRing; ring++) { for (int step = 0; step < ring; step++) { for (int slot = 0; slot < 8; slot++) { int x = XStep[slot] * step + XRing[slot] * ring + XConst[slot] + closestX; int y = YStep[slot] * step + YRing[slot] * ring + YConst[slot] + closestY; if (x >= 0 && x < side && y >= 0 && y < side) order[--k] = x * side + y; } } } } /// /// LandDefs::get_dir @0x005a9aa0: compass direction of a block at /// grid offset (dx, dy) from the viewer's block. Lcoord y grows northward. /// public static LandDirection GetDirection(int dx, int dy) { if (dx < 0) { if (dy < 0) return LandDirection.SouthWest; return dy > 0 ? LandDirection.NorthWest : LandDirection.West; } if (dx == 0) { if (dy < 0) return LandDirection.South; return dy > 0 ? LandDirection.North : LandDirection.InViewerBlock; } if (dy < 0) return LandDirection.SouthEast; return dy > 0 ? LandDirection.NorthEast : LandDirection.East; } /// /// CLandBlock::calc_sq_draw_order @0x0052f4a0 (direction switch, /// jump table @0x0052f8e0): the block's cell nearest the viewer, given the /// block's compass direction from the viewer. For the viewer's own block /// the viewer's cell coordinate (cell & 7 per axis) is scaled by /// 8/side; for other blocks the facing edge/corner is closest. /// public static (int X, int Y) ClosestCell( LandDirection dir, int viewerSqX, int viewerSqY, int side) { int scale = 8 / side; return dir switch { LandDirection.InViewerBlock => (viewerSqX / scale, viewerSqY / scale), LandDirection.North => (viewerSqX / scale, 0), LandDirection.South => (viewerSqX / scale, side - 1), LandDirection.East => (0, viewerSqY / scale), LandDirection.West => (side - 1, viewerSqY / scale), LandDirection.NorthWest => (side - 1, 0), LandDirection.SouthWest => (side - 1, side - 1), LandDirection.NorthEast => (0, 0), LandDirection.SouthEast => (0, side - 1), _ => throw new ArgumentOutOfRangeException(nameof(dir)), }; } // get_block_order's branchy max computation @0x00504ca0-0x00504cba: // the largest Chebyshev distance from (x, y) to any grid corner — // max(x, y, width-1-x, height-1-y, ...) — which guarantees the ring sweep // reaches every in-bounds slot. private static int MaxRingTo(int x, int y, int width, int height) { int max = x; if (y > max) max = y; if (width - 1 - x > max) max = width - 1 - x; if (height - 1 - y > max) max = height - 1 - y; return max; } } /// /// LandDefs::Direction (retail enum, values used raw by the /// calc_sq_draw_order jump table @0x0052f8e0). /// public enum LandDirection { InViewerBlock = 0, North = 1, South = 2, East = 3, West = 4, NorthWest = 5, SouthWest = 6, NorthEast = 7, SouthEast = 8, }