From 3b1ae83931ab7f614f9f24fbd80e83da8e69610f Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 25 May 2026 12:53:31 +0200 Subject: [PATCH] =?UTF-8?q?fix(phys):=20A6.P5=20=E2=80=94=20unconditional?= =?UTF-8?q?=20outdoor=20expansion=20in=20CellTransit=20BFS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/AcDream.Core/Physics/CellTransit.cs | 56 +++++++++++++++++-------- 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/src/AcDream.Core/Physics/CellTransit.cs b/src/AcDream.Core/Physics/CellTransit.cs index bdacceb..6629f94 100644 --- a/src/AcDream.Core/Physics/CellTransit.cs +++ b/src/AcDream.Core/Physics/CellTransit.cs @@ -94,22 +94,33 @@ public static class CellTransit if (portal.OtherCellId == 0xFFFF) { - // Exit portal. Any path sphere straddling the plane triggers - // the outdoor cell expansion. - for (int i = 0; i < sphereCount; i++) - { - var sphere = worldSpheres[i]; - float rad = sphere.Radius + EPSILON; - var localCenter = Vector3.Transform( - sphere.Origin, currentCell.InverseWorldTransform); - float dist = Vector3.Dot(localCenter, poly.Plane.Normal) + poly.Plane.D; - bool hit = dist > -rad && dist < rad; - if (hit) - { - exitOutside = true; - break; - } - } + // A6.P5 (2026-05-25): exit portals add outdoor cells + // UNCONDITIONALLY, by topology — not by sphere-plane overlap. + // + // Retail's CObjCell::find_cell_list (acclient_2013_pseudo_c.txt + // :308742-308869) walks vtable[0x80] on every cell already in + // the array and adds reachable cells without testing the + // sphere against each portal plane. The straddle check we + // had here gated outdoor inclusion on the sphere physically + // overlapping the EXIT portal — which fails to fire when: + // a) the sphere is in a SIBLING indoor cell that BFS- + // expanded to this one (sphere is geographically near + // the doorway region, just not at THIS cell's exit + // portal plane); OR + // 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; } @@ -441,6 +452,7 @@ public static class CellTransit pending.Enqueue(currentCellId); visited.Add(currentCellId); int maxIterations = 16; // hard cap; portal graphs are small + bool outdoorAdded = false; while (pending.Count > 0 && maxIterations-- > 0) { 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); + outdoorAdded = true; } } }