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>
109 lines
4.3 KiB
C#
109 lines
4.3 KiB
C#
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using AcDream.Core.Physics;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Tests.Physics;
|
|
|
|
public class CellTransitAddAllOutsideCellsTests
|
|
{
|
|
[Fact]
|
|
public void SphereWellInsideCell_AddsOneCell()
|
|
{
|
|
// A6.P4 (2026-05-24): coords are LANDBLOCK-LOCAL (X/Y in [0, 192]).
|
|
// #106 (2026-06-09): the convention is now explicit — coords are
|
|
// world-frame and currentBlockOrigin converts them to block-local;
|
|
// Zero origin == "current block is the anchor", the same inputs as
|
|
// before. Player at landblock-local (12, 12, 0) → cell (0,0) in
|
|
// landblock 0xA9B40000.
|
|
var candidates = new HashSet<uint>();
|
|
CellTransit.AddAllOutsideCells(
|
|
worldSphereCenter: new Vector3(12f, 12f, 0f),
|
|
sphereRadius: 0.5f,
|
|
currentCellId: 0xA9B40001u,
|
|
currentBlockOrigin: Vector3.Zero,
|
|
candidates: candidates);
|
|
|
|
Assert.Single(candidates);
|
|
Assert.Contains(0xA9B40001u, candidates);
|
|
}
|
|
|
|
[Fact]
|
|
public void SphereAtCellEastBoundary_AddsTwoCells()
|
|
{
|
|
// Player at (23.6, 12, 0) — near +X edge of cell (0,0). Sphere
|
|
// reaches to local X = 23.6 + 0.5 = 24.1 → cell (1,0) added.
|
|
var candidates = new HashSet<uint>();
|
|
CellTransit.AddAllOutsideCells(
|
|
worldSphereCenter: new Vector3(23.6f, 12f, 0f),
|
|
sphereRadius: 0.5f,
|
|
currentCellId: 0xA9B40001u,
|
|
currentBlockOrigin: Vector3.Zero,
|
|
candidates: candidates);
|
|
|
|
Assert.Equal(2, candidates.Count);
|
|
Assert.Contains(0xA9B40001u, candidates);
|
|
// Cell (1,0): low-16 id = 1 * 8 + 0 + 1 = 9 → 0x0009.
|
|
Assert.Contains(0xA9B40009u, candidates);
|
|
}
|
|
|
|
// ── #106: landblock crossings (retail add_all_outside_cells has no
|
|
// same-block clamp — LandDefs global lcoords) ─────────────────────
|
|
|
|
[Fact]
|
|
public void SphereJustSouthOfBlockBoundary_AddsBothBlocks()
|
|
{
|
|
// 0.2 m south of A9B4's southern edge: adjust_to_outside re-seats to
|
|
// A9B3 row 7 (cell 0xA9B30038 under x=150), and the boundary check
|
|
// (point.Y = 23.8 > maxRad 23.5) adds the A9B4 cell back as the +Y
|
|
// neighbour. Pre-#106 this returned ZERO candidates (the freeze).
|
|
var candidates = new HashSet<uint>();
|
|
CellTransit.AddAllOutsideCells(
|
|
worldSphereCenter: new Vector3(150f, -0.2f, 0f),
|
|
sphereRadius: 0.5f,
|
|
currentCellId: 0xA9B40031u,
|
|
currentBlockOrigin: Vector3.Zero,
|
|
candidates: candidates);
|
|
|
|
Assert.Contains(0xA9B30038u, candidates); // containing (south block)
|
|
Assert.Contains(0xA9B40031u, candidates); // +Y neighbour (home block)
|
|
Assert.Equal(2, candidates.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void SphereDeepInNeighbourBlock_AddsNeighbourCellOnly()
|
|
{
|
|
// The #106 capture geometry: ~109.65 m south of A9B4's origin =
|
|
// ~82 m into A9B3 → cell row 3 (floor(-109.65/24) = -5 → ly 1435).
|
|
var candidates = new HashSet<uint>();
|
|
CellTransit.AddAllOutsideCells(
|
|
worldSphereCenter: new Vector3(150f, -109.65f, 0f),
|
|
sphereRadius: 0.5f,
|
|
currentCellId: 0xA9B40031u,
|
|
currentBlockOrigin: Vector3.Zero,
|
|
candidates: candidates);
|
|
|
|
Assert.Contains(0xA9B30034u, candidates);
|
|
Assert.DoesNotContain(0xA9B40031u, candidates);
|
|
}
|
|
|
|
[Fact]
|
|
public void NonAnchorBlockOrigin_ConvertsWorldFrame()
|
|
{
|
|
// Player's current cell is in A9B3 (one block south of the anchor,
|
|
// world origin (0, -192)). World y = -0.2 is block-local y = 191.8 —
|
|
// still inside A9B3 row 7, with the boundary check adding the A9B4
|
|
// cell to the north. Exercises the origin conversion that the
|
|
// pre-#106 code silently skipped (world frame assumed block-local).
|
|
var candidates = new HashSet<uint>();
|
|
CellTransit.AddAllOutsideCells(
|
|
worldSphereCenter: new Vector3(150f, -0.2f, 0f),
|
|
sphereRadius: 0.5f,
|
|
currentCellId: 0xA9B30038u,
|
|
currentBlockOrigin: new Vector3(0f, -192f, 0f),
|
|
candidates: candidates);
|
|
|
|
Assert.Contains(0xA9B30038u, candidates);
|
|
Assert.Contains(0xA9B40031u, candidates);
|
|
}
|
|
}
|