using System; using System.Numerics; namespace AcDream.Core.Physics; /// /// Retail LandDefs outdoor-cell coordinate math (issue #106). All functions /// operate on the GLOBAL landcell coordinate space (lcoord, one unit per 24 m /// cell, range [0, 0x7F8) across the whole map) — landblock crossings are inherent /// in the math, never special-cased. Ported per /// docs/research/2026-06-09-landdefs-outside-cells-pseudocode.md; cross-checked /// against ACE Physics/Common/LandDefs.cs. /// /// Frame note: retail positions are landblock-local ([0, 192) per axis, /// relative to the cell id's block). acdream physics positions are in the floating /// world frame (anchor landblock at origin) — callers convert via the current /// block's registered world origin BEFORE calling in here. /// public static class LandDefs { /// 24 m landcell side (retail square_length). public const float CellLength = 24f; /// 192 m landblock side. public const float BlockLength = 192f; /// Map-wide lcoord bound: 255 blocks × 8 cells (retail 0x7F8). public const int LandLength = 0x7F8; // Retail PhysicsGlobals::EPSILON as used by adjust_to_outside's coordinate // snap (decomp :438719 shows the literal 0.000199999995f). private const float Epsilon = 0.000199999995f; /// /// LandDefs::in_bounds (pc:68509, @0x0043d650): both lcoord axes inside /// the map. /// public static bool InBounds(int lx, int ly) => lx >= 0 && ly >= 0 && lx < LandLength && ly < LandLength; /// /// LandDefs::blockid_to_lcoord (pc:68520, @0x0043d680): the lcoord of a /// landblock's (0,0) cell. Block bytes are ZERO-extended — the decomp's /// int8_t cast on block_y is a Binary Ninja mis-render (ACE /// LandDefs.cs:169 confirms plain masks). /// public static bool BlockIdToLcoord(uint cellId, out int lx, out int ly) { if (cellId == 0u) { lx = 0; ly = 0; return false; } lx = (int)((cellId >> 24) & 0xFFu) << 3; ly = (int)((cellId >> 16) & 0xFFu) << 3; return InBounds(lx, ly); } /// /// LandDefs::inbound_valid_cellid (pc:163438, @0x004979a0): low word in /// a valid range (landcell 1..0x40, envcell 0x100..0xFFFD, or the 0xFFFF block /// sentinel) and the block lcoord inside the map. Retail checks BOTH axes (ACE /// checks only X — an ACE divergence, not copied). /// public static bool InboundValidCellId(uint cellId) { if (!CellLowInRange(cellId & 0xFFFFu)) return false; int lx = (int)((cellId >> 24) & 0xFFu) << 3; int ly = (int)((cellId >> 16) & 0xFFu) << 3; return InBounds(lx, ly); } /// /// LandDefs::gid_to_lcoord (pc:163500, @0x00497a90): a full OUTDOOR cell /// id to its global lcoord. Fails for indoor ids (low ≥ 0x100). /// public static bool GidToLcoord(uint cellId, out int lx, out int ly) { lx = 0; ly = 0; if (!InboundValidCellId(cellId)) return false; uint low = cellId & 0xFFFFu; if (low >= 0x100u) return false; // outdoor only lx = ((int)((cellId >> 24) & 0xFFu) << 3) + (int)((low - 1u) >> 3); ly = ((int)((cellId >> 16) & 0xFFu) << 3) + (int)((low - 1u) & 7u); return InBounds(lx, ly); } /// /// LandDefs::lcoord_to_gid (pc:171859, @0x004a19a0): a global lcoord to /// the full outdoor cell id — the block id is RE-DERIVED from the lcoord's /// upper bits, so a neighbour-block lcoord yields the neighbour's cell id. /// Returns 0 when out of map bounds (retail behavior). /// public static uint LcoordToGid(int lx, int ly) { if (!InBounds(lx, ly)) return 0u; uint low = (uint)((ly & 7) + ((lx & 7) << 3) + 1); uint block = (uint)(((lx >> 3) << 8) | (ly >> 3)); return (block << 16) | low; } /// /// LandDefs::get_outside_lcoord (pc:438690, @0x005a9b00): global lcoord /// of the landcell under , expressed relative to /// 's block. floor(pos/24) may be negative or /// ≥ 8 — that IS the landblock crossing; the only rejection is the map edge. /// public static bool GetOutsideLcoord(uint cellId, Vector3 blockLocalPos, out int lx, out int ly) { lx = 0; ly = 0; if (!CellLowInRange(cellId & 0xFFFFu)) return false; BlockIdToLcoord(cellId, out lx, out ly); lx += (int)MathF.Floor(blockLocalPos.X / CellLength); ly += (int)MathF.Floor(blockLocalPos.Y / CellLength); return InBounds(lx, ly); } /// /// LandDefs::adjust_to_outside (pc:438719, @0x005a9bc0): re-seat /// (, ) onto the outdoor /// landcell actually under the point. On success the cell id may belong to a /// NEIGHBOUR landblock and the position is re-based into that block's local /// frame (pos -= floor(pos/192)·192; the decomp's 0f divisor is a /// BN artifact — ACE LandDefs.cs:140 confirms BlockLength). On failure the cell /// id is zeroed (retail behavior). /// public static bool AdjustToOutside(ref uint cellId, ref Vector3 blockLocalPos) { if (CellLowInRange(cellId & 0xFFFFu)) { if (MathF.Abs(blockLocalPos.X) < Epsilon) blockLocalPos.X = 0f; if (MathF.Abs(blockLocalPos.Y) < Epsilon) blockLocalPos.Y = 0f; if (GetOutsideLcoord(cellId, blockLocalPos, out int lx, out int ly)) { cellId = LcoordToGid(lx, ly); blockLocalPos.X -= MathF.Floor(blockLocalPos.X / BlockLength) * BlockLength; blockLocalPos.Y -= MathF.Floor(blockLocalPos.Y / BlockLength) * BlockLength; return true; } } cellId = 0u; return false; } // Retail cell_in_range: landcell, envcell, or the block sentinel. private static bool CellLowInRange(uint low) => low is (>= 1u and <= 0x40u) or (>= 0x100u and <= 0xFFFDu) or 0xFFFFu; }