fix(phys): A6.P5 — unconditional outdoor expansion in CellTransit BFS
Retail's CObjCell::find_cell_list at acclient_2013_pseudo_c.txt:308742- 308869 walks vtable[0x80] on every cell in the array and adds portal- reachable cells unconditionally — without testing each portal plane against the sphere. Our exit-portal branch in FindTransitCellsSphere gated outdoor inclusion on sphere-plane overlap (exitOutside fired only when the sphere physically straddled the exit portal plane). That gate produced the cottage-door over-penetration bug verified in A6P5_BuildCellSetFromIndoorStart_ReachesDoorOutdoorCell: BFS from indoor cell 0xA9B4013F expanded to 0xA9B40150 (which has an exit portal) but the sphere — in 0xA9B4013F's volume — wasn't at 0xA9B40150's exit portal plane, so exitOutside stayed false and the door's outdoor cell 0xA9B40029 wasn't added to the cellSet. The cell-crossing tick's collision query missed the door and the sphere committed 0.27 m INTO the slab. Fix: exit portals contribute exitOutside=true by topology (OtherCellId == 0xFFFFu), not by sphere overlap. AddAllOutsideCells is deduped to once per BFS so the radial pattern is added exactly once when any BFS-visited cell has an exit portal. Conformance: A6P5_BuildCellSetFromIndoorStart_ReachesDoorOutdoorCell now passes. A6P5_BuildCellSetFromAlcove_AlsoReachesDoorOutdoorCell (regression guard for the previously-sometimes-working case) stays green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2a890e6bde
commit
3b1ae83931
1 changed files with 38 additions and 18 deletions
|
|
@ -94,22 +94,33 @@ public static class CellTransit
|
||||||
|
|
||||||
if (portal.OtherCellId == 0xFFFF)
|
if (portal.OtherCellId == 0xFFFF)
|
||||||
{
|
{
|
||||||
// Exit portal. Any path sphere straddling the plane triggers
|
// A6.P5 (2026-05-25): exit portals add outdoor cells
|
||||||
// the outdoor cell expansion.
|
// UNCONDITIONALLY, by topology — not by sphere-plane overlap.
|
||||||
for (int i = 0; i < sphereCount; i++)
|
//
|
||||||
{
|
// Retail's CObjCell::find_cell_list (acclient_2013_pseudo_c.txt
|
||||||
var sphere = worldSpheres[i];
|
// :308742-308869) walks vtable[0x80] on every cell already in
|
||||||
float rad = sphere.Radius + EPSILON;
|
// the array and adds reachable cells without testing the
|
||||||
var localCenter = Vector3.Transform(
|
// sphere against each portal plane. The straddle check we
|
||||||
sphere.Origin, currentCell.InverseWorldTransform);
|
// had here gated outdoor inclusion on the sphere physically
|
||||||
float dist = Vector3.Dot(localCenter, poly.Plane.Normal) + poly.Plane.D;
|
// overlapping the EXIT portal — which fails to fire when:
|
||||||
bool hit = dist > -rad && dist < rad;
|
// a) the sphere is in a SIBLING indoor cell that BFS-
|
||||||
if (hit)
|
// expanded to this one (sphere is geographically near
|
||||||
{
|
// the doorway region, just not at THIS cell's exit
|
||||||
exitOutside = true;
|
// portal plane); OR
|
||||||
break;
|
// b) the per-tick target moves the sphere across the
|
||||||
}
|
// portal plane on one tick but not the next, producing
|
||||||
}
|
// intermittent visibility from the same position.
|
||||||
|
//
|
||||||
|
// Pre-fix bug: cottage doors at outdoor cells were invisible
|
||||||
|
// from indoor cells during cell-crossing substeps (live
|
||||||
|
// capture 2026-05-25; over-penetration test in
|
||||||
|
// CellTransitTests.A6P5_BuildCellSetFromIndoorStart_...).
|
||||||
|
//
|
||||||
|
// Post-fix: any cell visited by BFS that has at least one
|
||||||
|
// exit portal contributes exitOutside=true regardless of
|
||||||
|
// sphere position. AddAllOutsideCells fires once per BFS
|
||||||
|
// (deduped in BuildCellSetAndPickContaining).
|
||||||
|
exitOutside = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -441,6 +452,7 @@ public static class CellTransit
|
||||||
pending.Enqueue(currentCellId);
|
pending.Enqueue(currentCellId);
|
||||||
visited.Add(currentCellId);
|
visited.Add(currentCellId);
|
||||||
int maxIterations = 16; // hard cap; portal graphs are small
|
int maxIterations = 16; // hard cap; portal graphs are small
|
||||||
|
bool outdoorAdded = false;
|
||||||
while (pending.Count > 0 && maxIterations-- > 0)
|
while (pending.Count > 0 && maxIterations-- > 0)
|
||||||
{
|
{
|
||||||
uint cellId = pending.Dequeue();
|
uint cellId = pending.Dequeue();
|
||||||
|
|
@ -461,10 +473,18 @@ public static class CellTransit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exitOutside)
|
// A6.P5 (2026-05-25): any BFS-visited cell with an exit
|
||||||
|
// portal triggers the outdoor-neighbourhood add — matches
|
||||||
|
// retail's CObjCell::find_cell_list at
|
||||||
|
// acclient_2013_pseudo_c.txt:308742-308869 which expands
|
||||||
|
// portal-reachable cells unconditionally via vtable[0x80].
|
||||||
|
// Dedupe to once per BFS — the radial pattern depends only
|
||||||
|
// on the seed cell + sphere XY, so repeated calls would
|
||||||
|
// be no-ops with extra HashSet overhead.
|
||||||
|
if (exitOutside && !outdoorAdded)
|
||||||
{
|
{
|
||||||
// Add neighbour outdoor cells too.
|
|
||||||
AddAllOutsideCells(worldSpheres, sphereCount, currentCellId, candidates);
|
AddAllOutsideCells(worldSpheres, sphereCount, currentCellId, candidates);
|
||||||
|
outdoorAdded = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue