fix(phys): #106 — outdoor membership crosses landblock boundaries (LandDefs global-lcoord port)
The player's outdoor cell froze at the last in-block cell the moment they
walked over a landblock boundary (10,449-frame playerCell freeze in the
2026-06-09 capture; whole neighbouring-block interiors unenterable, plus
the running-distortion from the stale render anchor). Root cause: the
add_all_outside_cells port clamped BOTH the candidate proposal and the
find_cell_list containing-cell pick to the current landblock's 8x8 grid,
in a frame that silently assumed the current block sits at world origin.
One step over the line -> zero candidates -> FindCellSet returns
currentCellId forever.
Retail has no such clamp. Its cell math runs in a GLOBAL landcell grid
(lcoord 0..2039 spanning the map): get_outside_lcoord = blockid_to_lcoord
+ floor(blockLocalPos/24) with no bounds besides the map edge, and
lcoord_to_gid re-derives the landblock id from the lcoord's upper bits —
crossings are inherent, never special-cased.
The fix, decomp-cited throughout:
- New AcDream.Core.Physics.LandDefs: in_bounds (pc:68509),
blockid_to_lcoord (pc:68520), inbound_valid_cellid (pc:163438),
gid_to_lcoord (pc:163500), lcoord_to_gid (pc:171859),
get_outside_lcoord (pc:438690), adjust_to_outside (pc:438719).
Cross-checked against ACE LandDefs.cs; three artifacts documented and
avoided: BN's int8_t mis-render of block_y, BN's dropped 192f
BlockLength constant, and ACE add_cell_block's "FIXME!" same-block
guard (an ACE divergence, not retail).
- CellTransit.AddAllOutsideCells rewritten as the faithful sphere
variant (pc:317499 @0x00533630): adjust_to_outside re-seats the
(cell, position) pair cross-block, check_add_cell_boundary (pc:317229)
adds up to 3 neighbours by global lcoord, add_outside_cell (pc:317056)
has no same-block filter. adjust_to_outside failure breaks the sphere
loop (pc:533699 verbatim).
- BuildCellSetAndPickContaining: the outdoor containing-cell pick is now
the global XY-column under the sphere centre (AdjustToOutside), not
the [0,8)-clamped current-prefix reconstruction. Interior-wins order
and current-cell-first hysteresis unchanged.
- World->block-local frame conversion via the landblock origin already
registered in CellGraph (new TryGetTerrainOrigin); Zero fallback
preserves the legacy anchor-block assumption for unregistered terrain.
- Cross-landblock building entry comes free: the candidate snapshot now
contains neighbour-block landcells, so GetBuilding/CheckBuildingTransit
fire for cottages across the line (the capture's one failing entry).
Investigated FIRST per the pickup brief: the b3ce505 #98 stopgap gate is
definitively exonerated — it is a collision-object query gate that fires
only for indoor primary cells; no membership path touches
ShadowObjectRegistry.
Tests: 31 new (25 LandDefs conformance incl. capture-geometry goldens
0xA9B40031 -> 0xA9B30038/0xA9B30034 and the northbound return; 4
AddAllOutsideCells cross-block; 3 FindCellSet membership goldens incl.
the non-anchor-frame origin conversion). Full suite: 294+218+420 green;
Core 1369 green + the 4 pre-existing door/#99-era failures + 1 skip
(unchanged from baseline).
Pseudocode + artifact notes:
docs/research/2026-06-09-landdefs-outside-cells-pseudocode.md.
Remaining acceptance: live boundary walk with ACDREAM_PROBE_CELL=1
(ISSUES.md #106).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
12fb408972
commit
7078264291
11 changed files with 813 additions and 88 deletions
|
|
@ -170,110 +170,112 @@ public static class CellTransit
|
|||
|
||||
/// <summary>
|
||||
/// Outdoor neighbour expansion. Ported from
|
||||
/// <c>CLandCell::add_all_outside_cells</c> (sphere variant) per the
|
||||
/// pseudocode doc §"LandCell.add_all_outside_cells (sphere variant)".
|
||||
/// <c>CLandCell::add_all_outside_cells</c> (sphere variant,
|
||||
/// pc:317499 @0x00533630) per
|
||||
/// <c>docs/research/2026-06-09-landdefs-outside-cells-pseudocode.md</c>.
|
||||
///
|
||||
/// <para>
|
||||
/// The 24×24m landcell grid: a landblock is 8×8 cells. Cell index
|
||||
/// within a landblock is computed from local X/Y mod 24. The sphere
|
||||
/// adds the primary cell plus up to 3 neighbours when the radius
|
||||
/// reaches a cell boundary.
|
||||
/// Retail runs this in the GLOBAL landcell grid (<see cref="LandDefs"/>
|
||||
/// lcoords, 0..2039 across the whole map): <c>adjust_to_outside</c> re-seats
|
||||
/// the (cell, position) pair onto the landcell actually under the sphere —
|
||||
/// crossing landblock boundaries when <c>floor(local/24)</c> leaves the
|
||||
/// current block's 8×8 grid — and <c>check_add_cell_boundary</c> adds up to
|
||||
/// 3 neighbour cells (strict >/< against the sphere radius), each id
|
||||
/// re-derived from its own global lcoord. Issue #106: the pre-fix port
|
||||
/// clamped everything to the current landblock's grid, so the candidate set
|
||||
/// emptied the moment the player stepped over a boundary and membership
|
||||
/// froze on the last in-block cell.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <see cref="worldSphereCenter"/> is in the landblock-local coord
|
||||
/// space the rest of the engine uses (X/Y in [0, 192]; landblock
|
||||
/// world origin is at the streaming center, so all landblock-local
|
||||
/// positions are also world positions for the player's landblock).
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// A6.P4 door fix (2026-05-24): pre-fix this function subtracted the
|
||||
/// landblock's "absolute" world origin (lbX=0xA9*192=32448) from the
|
||||
/// sphere position, which made sense only if sphere coords were the
|
||||
/// absolute world position (32580). But production has used
|
||||
/// landblock-local coords since Phase A.1 (streaming-center landblock
|
||||
/// at world origin, so lbOffset for the center is (0,0); see
|
||||
/// <c>GameWindow.BuildInteriorEntitiesForStreaming</c>'s lbOffset
|
||||
/// formula). With landblock-local sphere coords, the old subtraction
|
||||
/// produced <c>localX = 132.36 - 32448 = -32316</c> → <c>gridX = -1346</c>
|
||||
/// → out-of-range → early return → ZERO outdoor cells added. For
|
||||
/// indoor primary cells (where issue #98 gates the GetNearbyObjects
|
||||
/// outdoor radial sweep) this meant the cottage door's outdoor cell
|
||||
/// 0xA9B40029 never reached <c>portalReachableCells</c>, the door's
|
||||
/// BSP was never queried, and the player walked through unimpeded —
|
||||
/// the user-reported Holtburg-door walkthrough bug. The fix:
|
||||
/// treat <c>worldSphereCenter</c> as landblock-local directly, no
|
||||
/// landblock-world-origin subtraction. This matches retail's
|
||||
/// <c>CLandCell::add_all_outside_cells</c> which uses the per-cell
|
||||
/// 6-byte position struct (landblock-relative).
|
||||
/// <paramref name="worldSphereCenter"/> is in the floating world frame
|
||||
/// (anchor landblock at origin — the convention every physics caller uses);
|
||||
/// <paramref name="currentBlockOrigin"/> is the current cell's landblock
|
||||
/// world origin (SW corner; <see cref="World.Cells.CellGraph.TryGetTerrainOrigin"/>),
|
||||
/// which converts it to retail's block-local frame. Pass
|
||||
/// <see cref="Vector3.Zero"/> when the current block IS the anchor — the
|
||||
/// pre-#106 behavior, and what the A6.P4 (2026-05-24) "landblock-local
|
||||
/// coords" convention actually meant.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static void AddAllOutsideCells(
|
||||
/// <returns>
|
||||
/// False when <c>adjust_to_outside</c> rejects the position (map edge /
|
||||
/// invalid cell id) — retail breaks out of the sphere loop on that.
|
||||
/// </returns>
|
||||
public static bool AddAllOutsideCells(
|
||||
Vector3 worldSphereCenter,
|
||||
float sphereRadius,
|
||||
uint currentCellId,
|
||||
Vector3 currentBlockOrigin,
|
||||
ICollection<uint> candidates)
|
||||
{
|
||||
const float CellSize = 24f;
|
||||
// Retail's position is block-local to the current cell's landblock.
|
||||
var center = worldSphereCenter - currentBlockOrigin;
|
||||
|
||||
uint lbPrefix = currentCellId & 0xFFFF0000u;
|
||||
uint cellId = currentCellId;
|
||||
if (!LandDefs.AdjustToOutside(ref cellId, ref center))
|
||||
return false;
|
||||
if (!LandDefs.GidToLcoord(cellId, out int lx, out int ly))
|
||||
return false;
|
||||
|
||||
float localX = worldSphereCenter.X;
|
||||
float localY = worldSphereCenter.Y;
|
||||
AddOutsideCell(candidates, lx, ly);
|
||||
|
||||
float cellLocalX = localX % CellSize;
|
||||
float cellLocalY = localY % CellSize;
|
||||
// check_add_cell_boundary (pc:317229 @0x00533260): the point within the
|
||||
// 24 m cell, from the adjust_to_outside-normalized block-local center
|
||||
// (always [0, 192) post-adjust; floor-mod for safety). Strict >/< —
|
||||
// a sphere exactly tangent to a boundary does NOT add the neighbour.
|
||||
float pointX = center.X - MathF.Floor(center.X / LandDefs.CellLength) * LandDefs.CellLength;
|
||||
float pointY = center.Y - MathF.Floor(center.Y / LandDefs.CellLength) * LandDefs.CellLength;
|
||||
float minRad = sphereRadius;
|
||||
float maxRad = CellSize - sphereRadius;
|
||||
float maxRad = LandDefs.CellLength - sphereRadius;
|
||||
|
||||
int gridX = (int)(localX / CellSize);
|
||||
int gridY = (int)(localY / CellSize);
|
||||
if (gridX < 0 || gridX >= 8 || gridY < 0 || gridY >= 8) return;
|
||||
|
||||
AddOutsideCell(candidates, lbPrefix, gridX, gridY);
|
||||
|
||||
if (cellLocalX > maxRad)
|
||||
if (pointX > maxRad)
|
||||
{
|
||||
AddOutsideCell(candidates, lbPrefix, gridX + 1, gridY);
|
||||
if (cellLocalY > maxRad) AddOutsideCell(candidates, lbPrefix, gridX + 1, gridY + 1);
|
||||
if (cellLocalY < minRad) AddOutsideCell(candidates, lbPrefix, gridX + 1, gridY - 1);
|
||||
AddOutsideCell(candidates, lx + 1, ly);
|
||||
if (pointY > maxRad) AddOutsideCell(candidates, lx + 1, ly + 1);
|
||||
if (pointY < minRad) AddOutsideCell(candidates, lx + 1, ly - 1);
|
||||
}
|
||||
if (cellLocalX < minRad)
|
||||
if (pointX < minRad)
|
||||
{
|
||||
AddOutsideCell(candidates, lbPrefix, gridX - 1, gridY);
|
||||
if (cellLocalY > maxRad) AddOutsideCell(candidates, lbPrefix, gridX - 1, gridY + 1);
|
||||
if (cellLocalY < minRad) AddOutsideCell(candidates, lbPrefix, gridX - 1, gridY - 1);
|
||||
AddOutsideCell(candidates, lx - 1, ly);
|
||||
if (pointY > maxRad) AddOutsideCell(candidates, lx - 1, ly + 1);
|
||||
if (pointY < minRad) AddOutsideCell(candidates, lx - 1, ly - 1);
|
||||
}
|
||||
if (cellLocalY > maxRad) AddOutsideCell(candidates, lbPrefix, gridX, gridY + 1);
|
||||
if (cellLocalY < minRad) AddOutsideCell(candidates, lbPrefix, gridX, gridY - 1);
|
||||
if (pointY > maxRad) AddOutsideCell(candidates, lx, ly + 1);
|
||||
if (pointY < minRad) AddOutsideCell(candidates, lx, ly - 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multi-sphere outdoor expansion. Retail's sphere variant loops every
|
||||
/// path sphere and adds the outdoor landcells touched by any of them.
|
||||
/// path sphere and adds the outdoor landcells touched by any of them;
|
||||
/// an <c>adjust_to_outside</c> failure BREAKS the loop (pc:533699).
|
||||
/// </summary>
|
||||
public static void AddAllOutsideCells(
|
||||
IReadOnlyList<Sphere> worldSpheres,
|
||||
int numSpheres,
|
||||
uint currentCellId,
|
||||
Vector3 currentBlockOrigin,
|
||||
ICollection<uint> candidates)
|
||||
{
|
||||
int sphereCount = EffectiveSphereCount(worldSpheres, numSpheres);
|
||||
for (int i = 0; i < sphereCount; i++)
|
||||
{
|
||||
var sphere = worldSpheres[i];
|
||||
AddAllOutsideCells(sphere.Origin, sphere.Radius, currentCellId, candidates);
|
||||
if (!AddAllOutsideCells(sphere.Origin, sphere.Radius, currentCellId, currentBlockOrigin, candidates))
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddOutsideCell(ICollection<uint> candidates, uint lbPrefix, int gridX, int gridY)
|
||||
private static void AddOutsideCell(ICollection<uint> candidates, int lx, int ly)
|
||||
{
|
||||
if (gridX < 0 || gridX >= 8 || gridY < 0 || gridY >= 8) return;
|
||||
|
||||
// Cell index within landblock: row-major (X * 8 + Y) + 1.
|
||||
uint low = (uint)(gridX * 8 + gridY + 1);
|
||||
candidates.Add(lbPrefix | low);
|
||||
// CLandCell::add_outside_cell (pc:317056 @0x00532ec0): map-bounds check,
|
||||
// then lcoord_to_gid — NO same-block filter (ACE's add_cell_block
|
||||
// "FIXME!" guard is an ACE divergence, not retail). The block id is
|
||||
// re-derived from the global lcoord, so neighbour-landblock cells come
|
||||
// out with the neighbour's prefix.
|
||||
uint gid = LandDefs.LcoordToGid(lx, ly);
|
||||
if (gid != 0u) candidates.Add(gid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -504,7 +506,12 @@ public static class CellTransit
|
|||
Vector3 worldSphereCenter = worldSpheres[0].Origin;
|
||||
float sphereRadius = worldSpheres[0].Radius;
|
||||
uint currentLow = currentCellId & 0xFFFFu;
|
||||
uint lbPrefix = currentCellId & 0xFFFF0000u;
|
||||
|
||||
// #106: the current block's world origin converts the world-frame sphere
|
||||
// coords into retail's block-local frame for the LandDefs lcoord math.
|
||||
// Unregistered terrain (tests; pre-stream) falls back to Vector3.Zero —
|
||||
// the legacy anchor-block assumption (world frame == block-local frame).
|
||||
cache.CellGraph.TryGetTerrainOrigin(currentCellId, out var blockOrigin);
|
||||
|
||||
if (currentLow >= 0x0100u)
|
||||
{
|
||||
|
|
@ -539,7 +546,7 @@ public static class CellTransit
|
|||
// pick order and interior-wins is preserved.
|
||||
if (exitOutside && !outdoorAdded)
|
||||
{
|
||||
AddAllOutsideCells(worldSpheres, sphereCount, currentCellId, candidates);
|
||||
AddAllOutsideCells(worldSpheres, sphereCount, currentCellId, blockOrigin, candidates);
|
||||
outdoorAdded = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -549,7 +556,7 @@ public static class CellTransit
|
|||
// Outdoor seed: expand neighbour landcells (added first), then check each
|
||||
// for a building stab whose portals cross into an interior EnvCell.
|
||||
// (Stage 2 will make building entry intrinsic and remove CheckBuildingTransit.)
|
||||
AddAllOutsideCells(worldSpheres, sphereCount, currentCellId, candidates);
|
||||
AddAllOutsideCells(worldSpheres, sphereCount, currentCellId, blockOrigin, candidates);
|
||||
|
||||
var landcellSnapshot = new List<uint>(candidates.OrderedIds);
|
||||
foreach (uint landcellId in landcellSnapshot)
|
||||
|
|
@ -571,6 +578,21 @@ public static class CellTransit
|
|||
// (Replaces the 5ca2f44 current-first pre-check, which approximated this for
|
||||
// the indoor-current case only; the ordered array now delivers it for every
|
||||
// seed by construction.)
|
||||
//
|
||||
// #106: the outdoor containing cell is the GLOBAL XY-column under the sphere
|
||||
// centre (LandDefs.AdjustToOutside from the current block's frame — retail
|
||||
// subtracts get_block_offset per candidate before point_in_cell, pc:308804;
|
||||
// landcells are disjoint columns so identity-compare is equivalent). The
|
||||
// pre-fix [0,8)-clamped, current-prefix-only computation could never match a
|
||||
// neighbour-block cell, freezing membership at landblock boundaries.
|
||||
uint containingOutdoorId = 0u;
|
||||
{
|
||||
var pickPos = worldSphereCenter - blockOrigin;
|
||||
uint pickCell = currentCellId;
|
||||
if (LandDefs.AdjustToOutside(ref pickCell, ref pickPos))
|
||||
containingOutdoorId = pickCell;
|
||||
}
|
||||
|
||||
uint outdoorResult = 0u;
|
||||
foreach (uint candId in candidates.OrderedIds)
|
||||
{
|
||||
|
|
@ -583,20 +605,14 @@ public static class CellTransit
|
|||
if (BSPQuery.PointInsideCellBsp(cand.CellBSP.Root, local))
|
||||
return candId; // interior-wins, stop (pseudo_c:308819)
|
||||
}
|
||||
else if (outdoorResult == 0u)
|
||||
else if (outdoorResult == 0u && containingOutdoorId != 0u)
|
||||
{
|
||||
// Outdoor candidate — CLandCell::point_in_cell is the XY-column the
|
||||
// sphere is over (acdream landcells have no BSP point_in_cell; the
|
||||
// documented adaptation). Record as the running result but DO NOT
|
||||
// break — an interior cell later in the array can still win.
|
||||
int gx = (int)(worldSphereCenter.X / 24f);
|
||||
int gy = (int)(worldSphereCenter.Y / 24f);
|
||||
if (gx >= 0 && gx < 8 && gy >= 0 && gy < 8)
|
||||
{
|
||||
uint outdoorId = lbPrefix | (uint)(gx * 8 + gy + 1);
|
||||
if (candId == outdoorId)
|
||||
outdoorResult = candId;
|
||||
}
|
||||
if (candId == containingOutdoorId)
|
||||
outdoorResult = candId;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
147
src/AcDream.Core/Physics/LandDefs.cs
Normal file
147
src/AcDream.Core/Physics/LandDefs.cs
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.Core.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>LandDefs</c> outdoor-cell coordinate math (issue #106). All functions
|
||||
/// operate on the GLOBAL landcell coordinate space (<c>lcoord</c>, 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
|
||||
/// <c>docs/research/2026-06-09-landdefs-outside-cells-pseudocode.md</c>; cross-checked
|
||||
/// against ACE <c>Physics/Common/LandDefs.cs</c>.
|
||||
///
|
||||
/// <para>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.</para>
|
||||
/// </summary>
|
||||
public static class LandDefs
|
||||
{
|
||||
/// <summary>24 m landcell side (retail <c>square_length</c>).</summary>
|
||||
public const float CellLength = 24f;
|
||||
|
||||
/// <summary>192 m landblock side.</summary>
|
||||
public const float BlockLength = 192f;
|
||||
|
||||
/// <summary>Map-wide lcoord bound: 255 blocks × 8 cells (retail <c>0x7F8</c>).</summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// <c>LandDefs::in_bounds</c> (pc:68509, @0x0043d650): both lcoord axes inside
|
||||
/// the map.
|
||||
/// </summary>
|
||||
public static bool InBounds(int lx, int ly)
|
||||
=> lx >= 0 && ly >= 0 && lx < LandLength && ly < LandLength;
|
||||
|
||||
/// <summary>
|
||||
/// <c>LandDefs::blockid_to_lcoord</c> (pc:68520, @0x0043d680): the lcoord of a
|
||||
/// landblock's (0,0) cell. Block bytes are ZERO-extended — the decomp's
|
||||
/// <c>int8_t</c> cast on block_y is a Binary Ninja mis-render (ACE
|
||||
/// LandDefs.cs:169 confirms plain masks).
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>LandDefs::inbound_valid_cellid</c> (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).
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>LandDefs::gid_to_lcoord</c> (pc:163500, @0x00497a90): a full OUTDOOR cell
|
||||
/// id to its global lcoord. Fails for indoor ids (low ≥ 0x100).
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>LandDefs::lcoord_to_gid</c> (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).
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>LandDefs::get_outside_lcoord</c> (pc:438690, @0x005a9b00): global lcoord
|
||||
/// of the landcell under <paramref name="blockLocalPos"/>, expressed relative to
|
||||
/// <paramref name="cellId"/>'s block. <c>floor(pos/24)</c> may be negative or
|
||||
/// ≥ 8 — that IS the landblock crossing; the only rejection is the map edge.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>LandDefs::adjust_to_outside</c> (pc:438719, @0x005a9bc0): re-seat
|
||||
/// (<paramref name="cellId"/>, <paramref name="blockLocalPos"/>) 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 (<c>pos -= floor(pos/192)·192</c>; the decomp's <c>0f</c> divisor is a
|
||||
/// BN artifact — ACE LandDefs.cs:140 confirms BlockLength). On failure the cell
|
||||
/// id is zeroed (retail behavior).
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
|
@ -416,7 +416,7 @@ public sealed class ShadowObjectRegistry
|
|||
/// outdoor cell ids. <see cref="CellTransit.FindCellSet"/> already adds
|
||||
/// outdoor cells to the candidate set when the sphere straddles an
|
||||
/// indoor cell's exit portal (<c>OtherCellId=0xFFFF</c>) via
|
||||
/// <see cref="CellTransit.AddAllOutsideCells(System.Numerics.Vector3, float, uint, System.Collections.Generic.HashSet{uint})"/>.
|
||||
/// <see cref="CellTransit.AddAllOutsideCells(System.Numerics.Vector3, float, uint, System.Numerics.Vector3, System.Collections.Generic.ICollection{uint})"/>.
|
||||
/// Pre-slice-1, the explicit
|
||||
/// "skip outdoor ids" filter combined with #98's indoor-primary gate
|
||||
/// meant doors registered at outdoor cells (default <c>cellScope=0</c>
|
||||
|
|
|
|||
|
|
@ -27,6 +27,25 @@ public sealed class CellGraph
|
|||
public void RegisterTerrain(uint landblockPrefix, TerrainSurface terrain, Vector3 worldOrigin)
|
||||
=> _terrain[landblockPrefix & 0xFFFF0000u] = (terrain, worldOrigin);
|
||||
|
||||
/// <summary>
|
||||
/// World origin (SW corner) of the landblock containing <paramref name="id"/>,
|
||||
/// as registered by <see cref="RegisterTerrain"/>. Issue #106: converts the
|
||||
/// floating-world-frame sphere coords into retail's block-local frame for the
|
||||
/// <see cref="LandDefs"/> lcoord math. False (origin zero) when the landblock
|
||||
/// has no registered terrain — callers fall back to the legacy anchor-block
|
||||
/// assumption (world frame == block-local frame).
|
||||
/// </summary>
|
||||
public bool TryGetTerrainOrigin(uint id, out Vector3 origin)
|
||||
{
|
||||
if (_terrain.TryGetValue(id & 0xFFFF0000u, out var t))
|
||||
{
|
||||
origin = t.Origin;
|
||||
return true;
|
||||
}
|
||||
origin = Vector3.Zero;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void RemoveLandblock(uint landblockPrefix)
|
||||
{
|
||||
uint lb = landblockPrefix & 0xFFFF0000u;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue