T6 (BR-7) C2: BuildShadowCellSet - the registration-side portal flood
Verbatim port of CObjCell::find_cell_list (Ghidra 0x0052b4e0, pc:308742)
as invoked by calc_cross_cells / calc_cross_cells_static (0x00515230 /
0x00515160) - the flood retail runs at SHADOW REGISTRATION time, minus
the containing-cell pick (registration passes a null out-cell):
- Seed: indoor -> exactly that one cell (added even when unloaded, like
retail's null-pointer add_cell; the walk is then skipped per the
0052b576 seed-pointer gate); outdoor -> block-crossing
AddAllOutsideCells.
- Growing-array walk: indoor cells via FindTransitCellsSphere
(sphere-vs-neighbor-BSP admission; exterior straddle -> outside cells
once per walk, retail CELLARRAY.added_outside); outdoor cells via the
CLandCell leg (0x00533800) = add_all_outside_cells (same once-guard)
+ the building bridge CSortCell -> CBuildingObj ->
check_building_transit (0x00534060/0x006b5230/0x0052c5d0) - how an
outdoor-positioned door reaches the vestibule's shadow list at
registration. No XY grid, no visibility lists (the spec's
VisibleCellIds rule was REFUTED by the WF1 verification).
- Static prune (do_not_load_cells, 0052b66e): indoor-seeded statics keep
only {seed} + seed.stab_list (VisibleCellIds) - also strips outdoor
cells, matching retail (interior statics never shadow into landcells;
outdoor spheres reach them through their own array's building bridge).
11 unit tests: seeds (indoor/outdoor/unloaded), neighbor-BSP admission,
building bridge incl. the C1 negative-portal-id gate, exterior straddle
on/off, static prune drop/keep, zero-sphere no-op.
Consumed by C3 (ShadowObjectRegistry rewrite).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
6ec4cde9a4
commit
abf36e2743
2 changed files with 448 additions and 0 deletions
|
|
@ -441,6 +441,155 @@ public static class CellTransit
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// BR-7 / A6.P4 (2026-06-11). Registration-side cell-set builder — the
|
||||
/// sphere-overlap portal flood retail runs at SHADOW REGISTRATION time.
|
||||
/// Verbatim port of <c>CObjCell::find_cell_list</c> (Ghidra 0x0052b4e0,
|
||||
/// pc:308742) as invoked by <c>CPhysicsObj::calc_cross_cells</c> /
|
||||
/// <c>calc_cross_cells_static</c> (Ghidra 0x00515230 / 0x00515160):
|
||||
/// the seed + growing-array walk, WITHOUT the containing-cell pick
|
||||
/// (registration passes a null out-cell).
|
||||
///
|
||||
/// <para>Shape, all Ghidra-verified (wf1-interior-collision.md):</para>
|
||||
/// <list type="number">
|
||||
/// <item>Seed: indoor id (low16 ≥ 0x100) → exactly that one cell
|
||||
/// (0052b563), added EVEN IF unloaded (retail add_cell()s the id
|
||||
/// with a null pointer); outdoor id → the block-crossing
|
||||
/// <see cref="AddAllOutsideCells(IReadOnlyList{Sphere}, int, uint, Vector3, ICollection{uint})"/>
|
||||
/// (0052b53f).</item>
|
||||
/// <item>Growing-array walk (0052b576-0052b5ab), gated on the seed
|
||||
/// being LOADED: each array cell's <c>find_transit_cells</c>
|
||||
/// (vtable+0x80). Indoor cells →
|
||||
/// <see cref="FindTransitCellsSphere(PhysicsDataCache, CellPhysics, uint, IReadOnlyList{Sphere}, int, ICollection{uint}, out bool, out bool)"/>
|
||||
/// (sphere-vs-neighbor-BSP gates; exterior straddle → outside
|
||||
/// cells, once per walk like retail's CELLARRAY.added_outside).
|
||||
/// Outdoor cells → <c>CLandCell::find_transit_cells</c>
|
||||
/// (0x00533800) = add_all_outside_cells (same once-guard) +
|
||||
/// the building bridge <c>CSortCell → CBuildingObj →
|
||||
/// CEnvCell::check_building_transit</c>
|
||||
/// (0x00534060/0x006b5230/0x0052c5d0) — how an outdoor-seeded
|
||||
/// door reaches the vestibule's shadow list. Unloaded indoor
|
||||
/// cells in the array are not walked (0052b58e null check).</item>
|
||||
/// <item>Static prune (<paramref name="isStatic"/>, retail
|
||||
/// <c>do_not_load_cells</c>, 0052b66e): when the seed is indoor,
|
||||
/// flood results are pruned to {seed} ∪ seed.stab_list
|
||||
/// (<see cref="CellPhysics.VisibleCellIds"/>) — placement of
|
||||
/// statics must not force-load cells. Note this also strips
|
||||
/// outdoor cells (they are never in an EnvCell stab list):
|
||||
/// retail interior statics never shadow into landcells; an
|
||||
/// outdoor sphere reaches them via its OWN array's
|
||||
/// check_building_transit instead.</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <para>Flood spheres: the object's REAL collision footprint — retail
|
||||
/// globalizes the CylSpheres (low_pt → world, cyl radius, cap 10;
|
||||
/// overload 0x0052b9f0), falling back to the part-array sorting sphere.
|
||||
/// Callers map ShadowShape lists via
|
||||
/// <see cref="ShadowShapeBuilder"/>-derived helpers (see
|
||||
/// ShadowObjectRegistry).</para>
|
||||
/// </summary>
|
||||
public static IReadOnlyList<uint> BuildShadowCellSet(
|
||||
PhysicsDataCache cache,
|
||||
uint seedCellId,
|
||||
IReadOnlyList<Sphere> worldSpheres,
|
||||
int numSpheres,
|
||||
bool isStatic)
|
||||
{
|
||||
var candidates = new CellArray();
|
||||
int sphereCount = EffectiveSphereCount(worldSpheres, numSpheres);
|
||||
if (seedCellId == 0 || sphereCount == 0)
|
||||
return candidates.OrderedIds;
|
||||
|
||||
uint seedLow = seedCellId & 0xFFFFu;
|
||||
// #106 frame convention: LandDefs lcoord math runs block-local;
|
||||
// TryGetTerrainOrigin supplies the seed block's world origin
|
||||
// (Zero fallback = legacy anchor-frame, same as the transit path).
|
||||
cache.CellGraph.TryGetTerrainOrigin(seedCellId, out var blockOrigin);
|
||||
|
||||
bool seedLoaded;
|
||||
if (seedLow >= 0x0100u)
|
||||
{
|
||||
candidates.Add(seedCellId);
|
||||
// Retail adds the unloaded seed by id (null cell pointer) and
|
||||
// skips the walk (gate on arg4 != 0 at 0052b576) — the object
|
||||
// stays registered under just its claimed cell until the cell
|
||||
// hydrates and the registration is re-run (CObjCell::init_objects
|
||||
// → recalc_cross_cells, Ghidra 0x0052b420/0x00515a30; our
|
||||
// equivalent is ShadowObjectRegistry's re-flood hook).
|
||||
seedLoaded = cache.GetCellStruct(seedCellId) is not null;
|
||||
}
|
||||
else
|
||||
{
|
||||
AddAllOutsideCells(worldSpheres, sphereCount, seedCellId, blockOrigin, candidates);
|
||||
// Outdoor seeds always walk: retail's null-CLandCell case is
|
||||
// "landblock not loaded at all", where our per-cell building
|
||||
// lookups below come back null anyway (documented adaptation).
|
||||
seedLoaded = true;
|
||||
}
|
||||
|
||||
if (seedLoaded)
|
||||
{
|
||||
bool outdoorAdded = seedLow < 0x0100u; // retail CELLARRAY.added_outside
|
||||
for (int i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
uint cellId = candidates.OrderedIds[i];
|
||||
if ((cellId & 0xFFFFu) >= 0x0100u)
|
||||
{
|
||||
var cell = cache.GetCellStruct(cellId);
|
||||
if (cell is null) continue;
|
||||
|
||||
FindTransitCellsSphere(
|
||||
cache, cell, cellId, worldSpheres, sphereCount,
|
||||
candidates, out bool exitStraddle, out _);
|
||||
|
||||
if (exitStraddle && !outdoorAdded)
|
||||
{
|
||||
AddAllOutsideCells(worldSpheres, sphereCount, seedCellId, blockOrigin, candidates);
|
||||
outdoorAdded = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// CLandCell::find_transit_cells (0x00533800):
|
||||
// add_all_outside_cells (added_outside-guarded) then the
|
||||
// building bridge for the landcell's building, if any.
|
||||
if (!outdoorAdded)
|
||||
{
|
||||
AddAllOutsideCells(worldSpheres, sphereCount, seedCellId, blockOrigin, candidates);
|
||||
outdoorAdded = true;
|
||||
}
|
||||
|
||||
var building = cache.GetBuilding(cellId);
|
||||
if (building is not null)
|
||||
CheckBuildingTransit(cache, building, worldSpheres, sphereCount, candidates, out _);
|
||||
}
|
||||
}
|
||||
|
||||
// Static prune (do_not_load_cells, 0052b66e): indoor-seeded
|
||||
// statics keep only {seed} ∪ seed.stab_list.
|
||||
if (isStatic && seedLow >= 0x0100u)
|
||||
{
|
||||
var seedCell = cache.GetCellStruct(seedCellId);
|
||||
if (seedCell is not null)
|
||||
{
|
||||
var keep = new List<uint>(candidates.Count);
|
||||
foreach (uint id in candidates.OrderedIds)
|
||||
{
|
||||
if (id == seedCellId || seedCell.VisibleCellIds.Contains(id))
|
||||
keep.Add(id);
|
||||
}
|
||||
if (keep.Count != candidates.Count)
|
||||
{
|
||||
candidates.Clear();
|
||||
foreach (uint id in keep) candidates.Add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return candidates.OrderedIds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verbatim port of <c>CEnvCell::find_visible_child_cell</c>
|
||||
/// (<c>acclient_2013_pseudo_c.txt:311397</c>). Returns the cell whose cell-BSP
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue