# LandDefs + add_all_outside_cells — retail pseudocode (issue #106) **Date:** 2026-06-09. **Purpose:** ground the #106 fix (outdoor membership freezes at landblock boundaries) in the retail decomp. The pre-fix acdream port of `AddAllOutsideCells` clamped candidates to the current landblock's 8×8 grid; retail has NO such clamp — its cell math runs in a **global** landcell coordinate space (`lcoord`, 0..2039) spanning the entire map, so landblock crossings are inherent. **Sources:** `docs/research/named-retail/acclient_2013_pseudo_c.txt` (primary); `references/ACE/Source/ACE.Server/Physics/Common/LandDefs.cs` + `LandCell.cs` (cross-check, in the MAIN repo checkout — this worktree only vendors WorldBuilder). --- ## 1. The coordinate model - A **landblock id** packs the map grid into the high word of a cell id: bits 31–24 = `block_x` (east–west), bits 23–16 = `block_y` (north–south). - An **outdoor cell** low word is `1 + cell_x*8 + cell_y` (`cell_x`,`cell_y` ∈ 0..7, 24 m cells, row-major with X outer). - A **global lcoord** is `(block_x*8 + cell_x, block_y*8 + cell_y)` — a flat map-wide cell grid, valid range `[0, 0x7F8)` = `[0, 2040)` (255 blocks × 8). - Positions in retail `Position` structs are **block-local** (`[0,192)` per axis). acdream divergence to bridge: physics positions are in a **floating world frame** anchored at the login/teleport landblock (origin = that block's SW corner; `GameWindow.ApplyLoadedTerrainLocked` registers each block's origin as `((lbX − centerX)·192, (lbY − centerY)·192)`). Convert world → current-block-local by subtracting the current block's registered origin (`CellGraph._terrain` stores it), then retail's math applies verbatim. ## 2. LandDefs functions (verbatim-equivalent pseudocode) ### in_bounds (pc:68509, @0x0043d650) ``` in_bounds(x, y) := x >= 0 && y >= 0 && x < 0x7F8 && y < 0x7F8 ``` ### blockid_to_lcoord (pc:68520, @0x0043d680) ``` blockid_to_lcoord(cellId, out lx, out ly): if cellId == 0: return false lx = ((cellId >> 24) & 0xFF) * 8 // decomp form: (cellId >> 21) & 0x7F8 ly = ((cellId >> 16) & 0xFF) * 8 // decomp form: (cellId >> 13) & 0x7F8 return in_bounds(lx, ly) ``` ⚠️ **BN artifact:** the decomp renders the Y extraction as `(int8_t)(cellId >> 16) << 3` — a signed cast that would explode for block_y ≥ 0x80 (e.g. 0xB4). ACE's `blockid_to_lcoord` (LandDefs.cs:169) confirms plain zero-extended masks. Use unsigned. ### inbound_valid_cellid (pc:163438, @0x004979a0) ``` inbound_valid_cellid(cellId): low = cellId & 0xFFFF if !(low in [1,0x40] || low in [0x100,0xFFFD] || low == 0xFFFF): return false lx = (cellId >> 21) & 0x7F8; ly = (cellId >> 13) & 0x7F8 return in_bounds(lx, ly) ``` ⚠️ **ACE divergence:** ACE's `inbound_valid_cellid` (LandDefs.cs:241) checks only `block_x`. Retail checks both axes. Port retail. ### gid_to_lcoord (pc:163500, @0x00497a90) — outdoor cells only ``` gid_to_lcoord(cellId, out lx, out ly): if !inbound_valid_cellid(cellId): return false if (cellId & 0xFFFF) >= 0x100: return false // outdoor only lx = block_x*8 + ((low−1) >> 3) ly = block_y*8 + ((low−1) & 7) return in_bounds(lx, ly) ``` ### lcoord_to_gid (pc:171859, @0x004a19a0) ``` lcoord_to_gid(lx, ly): if !in_bounds(lx, ly): return 0 low = (ly & 7) + (lx & 7)*8 + 1 block = ((lx >> 3) << 8) | (ly >> 3) // decomp: ((lx & ~7) << 5) | (ly >> 3) return (block << 16) | low ``` Cross-block is transparent: `lx = 1439` → `block_y = 179 (0xB3)` even when the input block was 0xB4. ### get_outside_lcoord (pc:438690, @0x005a9b00) ``` get_outside_lcoord(cellId, blockLocalPos, out lx, out ly): if cellId low16 not in {1..0x40, 0x100..0xFFFD, 0xFFFF}: return false blockid_to_lcoord(cellId, out lx, out ly) lx += floor(pos.x / 24); ly += floor(pos.y / 24) // floor() — negative-safe return in_bounds(lx, ly) ``` `floor(pos/24)` may be negative or ≥ 8 — **this is the landblock crossing**. No clamp. ### adjust_to_outside (pc:438719, @0x005a9bc0) ``` adjust_to_outside(ref cellId, ref blockLocalPos): low = cellId & 0xFFFF if low in valid ranges (as above): if |pos.x| < 0.000199999995: pos.x = 0 // retail EPSILON snap if |pos.y| < 0.000199999995: pos.y = 0 if get_outside_lcoord(cellId, pos, out lx, out ly): cellId = lcoord_to_gid(lx, ly) pos.x -= floor(pos.x / 192) * 192 // re-normalize to NEW block pos.y -= floor(pos.y / 192) * 192 return true cellId = 0 return false ``` ⚠️ **BN artifact:** the decomp shows `floor(pos.x / 0f) * 0f` — the constant was dropped. ACE (LandDefs.cs:140-141) confirms `BlockLength = 192`. ## 3. CLandCell::add_all_outside_cells — sphere variant (pc:317499, @0x00533630) ``` add_all_outside_cells(position, numSphere, spheres[], cellArray): if cellArray.added_outside: return cellArray.added_outside = 1 if numSphere == 0: cellId = position.objcell_id; pos = position.frame.origin if adjust_to_outside(ref cellId, ref pos) && gid_to_lcoord(cellId, out lx, ly): add_outside_cell(cellArray, lx, ly) return for each sphere (center copied): cellId = position.objcell_id if !adjust_to_outside(ref cellId, ref center): BREAK // break, not continue if gid_to_lcoord(cellId, out lx, out ly): add_outside_cell(cellArray, lx, ly) point.x = center.x mod 24; point.y = center.y mod 24 // center is [0,192) post-adjust check_add_cell_boundary(cellArray, point, lx, ly, minRad=radius, maxRad=24−radius) ``` ### add_outside_cell (pc:317056, @0x00532ec0) ``` add_outside_cell(cellArray, lx, ly): if in_bounds(lx, ly): cellArray.add_cell(lcoord_to_gid(lx, ly), LScape::get_landcell(...)) ``` **NO same-block filter.** ⚠️ ACE's `add_cell_block` (LandCell.cs:198) inserts `if (id >> 16 != cellID >> 16) continue;` annotated `// FIXME!` — an ACE divergence that drops cross-landblock cells. Do NOT copy it. ### check_add_cell_boundary (pc:317229, @0x00533260) ``` check_add_cell_boundary(cellArray, point, lx, ly, minRad, maxRad): // strict > / < if point.x > maxRad: add_outside_cell(lx+1, ly) if point.y > maxRad: add_outside_cell(lx+1, ly+1) if point.y < minRad: add_outside_cell(lx+1, ly−1) if point.x < minRad: add_outside_cell(lx−1, ly) if point.y > maxRad: add_outside_cell(lx−1, ly+1) if point.y < minRad: add_outside_cell(lx−1, ly−1) if point.y > maxRad: add_outside_cell(lx, ly+1) if point.y < minRad: add_outside_cell(lx, ly−1) ``` A sphere exactly tangent to a 24 m boundary does NOT add the neighbour (strict comparisons — matches ACE). ## 4. The containing-cell pick (CObjCell::find_cell_list, pc:308742-308869, @0x0052b4e0) After the transit set is built, retail iterates candidates IN ORDER; for each it subtracts `LandDefs::get_block_offset(position.block, candidate.block)` from the sphere center (cross-block-aware) and calls the cell's `point_in_cell` (vtable +0x84). Any containing cell sets the running result; an INTERIOR containing cell breaks immediately (interior-wins); outdoor cells are disjoint columns so at most one contains the point. acdream adaptation (landcells have no BSP `point_in_cell`): the containing outdoor cell is `adjust_to_outside(currentCellId, blockLocal(center))` — the global XY-column id — compared by identity against candidates. Pre-#106 this was computed with a `[0,8)` grid clamp under the current block's prefix, which is what froze membership at boundaries. ## 5. Worked goldens (from the #106 capture geometry) Anchor A9B4 at world origin; A9B3 is one block SOUTH (origin (0, −192)). | Input | Result | |---|---| | `blockid_to_lcoord(0xA9B40031)` | (1352, 1440) | | `gid_to_lcoord(0xA9B40031)` (low 0x31 = 49 → cell 6,0) | (1358, 1440) | | `lcoord_to_gid(1358, 1440)` | 0xA9B40031 (roundtrip) | | `lcoord_to_gid(1358, 1439)` | **0xA9B30038** (block_y 179, cell 6,7) | | `adjust_to_outside(0xA9B40031, (150, −1))` | cellId **0xA9B30038**, pos (150, **191**) | | `adjust_to_outside(0xA9B40031, (150, −109.65))` | cellId **0xA9B30034** (floor(−109.65/24) = −5 → ly 1435), pos (150, 82.35) | | `adjust_to_outside(0xA9B30038, (150, 193))` from A9B3 frame | cellId **0xA9B40031**, pos (150, 1) — northbound return |