diff --git a/docs/research/2026-05-25-door-bug-inside-out-geometry-gap.md b/docs/research/2026-05-25-door-bug-inside-out-geometry-gap.md
index ce3e3e6..4a851b5 100644
--- a/docs/research/2026-05-25-door-bug-inside-out-geometry-gap.md
+++ b/docs/research/2026-05-25-door-bug-inside-out-geometry-gap.md
@@ -79,7 +79,92 @@ walk there freely.
- The slab east face blocks WEST motion correctly. The sphere is FREE
to move north because no geometry covers (133.655, Y > 17.1).
-## What's next
+## UPDATE (2026-05-25 evening): the wall EXISTS, but isn't blocking
+
+Continued investigation with a wider polygon search in
+`Diagnostic_CottagePolys_NearWalkthroughPosition` revealed the cottage
+DOES have the missing wall:
+
+```
+poly 0x0032 n=(0.00, +1.00, 0.00) X=[133.50, 136.30] Y=[17.10, 17.10] Z=[94.00, 97.00]
+poly 0x0033 n=(0.00, +1.00, 0.00) X=[133.50, 136.30] Y=[17.10, 17.10] Z=[94.00, 97.00]
+```
+
+(Plus symmetric polys 0x0030, 0x0031, 0x0034, 0x0035 covering X<131.6,
+0x0037, 0x0038, 0x003A, 0x003B above the doorway lintel.)
+
+The cottage's north exterior wall east of doorway IS at world (X=[133.5,
+136.3], Y=17.10, Z=[94, 97]), normal +Y. **This wall SHOULD block sphere
+at X=133.655 (sphere west edge at 133.175 ≤ wall X range, sphere south
+edge at 17.110 ≤ wall Y).**
+
+The new question: WHY isn't the wall blocking in production?
+
+Sphere at world (133.655, 17.59) at the captured failing tick:
+- Sphere XY: X=[133.175, 134.135], Y=[17.110, 18.070]
+- Sphere overlaps wall in X (133.175..134.135 vs 133.5..136.3) by 0.635m
+- Sphere south edge at Y=17.110 ALIGNS with wall at Y=17.10 (0.010m past)
+- Sphere CENTER at Y=17.59 is 0.49m north of wall
+- Distance from sphere center to wall plane: 0.49m. Sphere radius 0.48m.
+- |dist| (0.49) ≈ radius (0.48). Sphere is JUST grazing the wall plane.
+
+At this exact tick the sphere CENTER is 0.49m north of wall; sphere
+south edge is 0.01m north of wall. Sphere is BARELY past the wall.
+
+So this tick isn't where the walkthrough happens. The walkthrough is
+EARLIER — when sphere center Y went from 17.58 (just past wall by reach)
+to 17.59. The crossing must have allowed the sphere through.
+
+OR: the sphere never actually crossed the wall — it walked around it.
+Cottage wall east of doorway is X=[133.5, 136.3]. Sphere at X=133.655
+is barely in the wall's X range. If sphere came from X < 133.5 (where
+no east wall exists) and shifted east while sliding along the slab,
+it could end up at X > 133.5 having NEVER crossed the wall plane.
+
+Cell transit data confirms: tick 1549 outdoor→indoor at X=132.859,
+tick 2586 indoor→outdoor at X=134.022 (way past wall east edge).
+**The sphere reached X=134.022 inside cottage geometry somehow.**
+
+Sphere fitting through doorway opening requires center X in
+[131.6+0.48, 133.5-0.48] = [132.08, 133.02]. Tight. The user's
+off-center test (~50cm east) puts sphere at edge of opening or
+past. Sphere is sliding against the slab east face (cn=(+1,0,0))
+which gradually pushes it east. Eventually sphere center exceeds
+X=133.5 — past the cottage east wall's start. From that position,
+sphere can move north WITHOUT crossing the wall plane (sphere
+center already north of Y=17.10 from prior sliding).
+
+**This may be retail-faithful behavior** OR a bug in sphere-vs-corner
+collision. The corner where alcove east wall (X=133.5, Y=[16.5,17.1])
+meets cottage north wall (X=[133.5,136.3], Y=17.10) is a degenerate
+edge. Sphere sliding along the alcove east wall (moving +Y) reaches
+the corner at (133.5, 17.10) — should encounter the cottage wall
+and be stopped. If our engine handles the corner transition
+incorrectly, sphere slides past.
+
+## What's next (revised)
+
+**Investigate sphere-vs-corner collision behavior** at the alcove
+east wall → cottage north wall meeting point at world (133.5, 17.10).
+
+Apparatus to write:
+- Load cottage GfxObj + cell 0x0150 BSP into harness
+- Place sphere at (133.0, 16.8, 94) (inside alcove, near east wall)
+- Walk sphere +Y in small increments
+- Expected: sphere stops at Y=17.10-0.48 = 16.62 if east wall blocks
+ OR: sphere slides along corner staying in alcove
+- Captured: sphere ends up at (133.655, 17.59) — sliding past corner
+
+If harness reproduces "sphere slides past corner", the bug is in
+the engine's corner-handling. Read BSPQuery for the sphere-vs-edge
+case. Retail oracle: CTransition::find_obj_collisions corner
+handling at acclient_2013_pseudo_c.txt.
+
+If harness BLOCKS at corner (no sliding past), the bug is something
+else — maybe cell 0x0150 BSP isn't being queried in production from
+some sphere position.
+
+## OLD (superseded) "what's next" candidates
**Identify which entity SHOULD own the cottage's north exterior wall
east of the doorway.** Three candidates:
diff --git a/tests/AcDream.Core.Tests/Physics/DoorBugTrajectoryReplayTests.cs b/tests/AcDream.Core.Tests/Physics/DoorBugTrajectoryReplayTests.cs
index 716d7f6..3cd469e 100644
--- a/tests/AcDream.Core.Tests/Physics/DoorBugTrajectoryReplayTests.cs
+++ b/tests/AcDream.Core.Tests/Physics/DoorBugTrajectoryReplayTests.cs
@@ -474,10 +474,39 @@ public class DoorBugTrajectoryReplayTests
Console.WriteLine($" Sphere Y=[{sphereCenterY-0.48f:F3}, {sphereCenterY+0.48f:F3}]");
Console.WriteLine($" Sphere Z=[94.000, 95.200]");
+ // Also dump ALL polys with vertices near sphere XY (loose: 3m window)
+ // so we can see what wall geometry the cottage HAS in the area.
+ Console.WriteLine("");
+ Console.WriteLine("=== Cottage polys with bbox extending into (X in [130,138], Y in [13,21]) ===");
+ int nearXYCount = 0;
+ foreach (var (polyId, poly) in physics.Resolved)
+ {
+ float wxMin = float.MaxValue, wxMax = float.MinValue;
+ float wyMin = float.MaxValue, wyMax = float.MinValue;
+ float wzMin = float.MaxValue, wzMax = float.MinValue;
+ foreach (var v in poly.Vertices)
+ {
+ var rotated = Vector3.Transform(v, cottageRot);
+ var world = cottagePos + rotated;
+ if (world.X < wxMin) wxMin = world.X; if (world.X > wxMax) wxMax = world.X;
+ if (world.Y < wyMin) wyMin = world.Y; if (world.Y > wyMax) wyMax = world.Y;
+ if (world.Z < wzMin) wzMin = world.Z; if (world.Z > wzMax) wzMax = world.Z;
+ }
+ // Wide search window.
+ if (wxMax < 130 || wxMin > 138) continue;
+ if (wyMax < 13 || wyMin > 21) continue;
+ nearXYCount++;
+ var nWorld = Vector3.Transform(poly.Plane.Normal, cottageRot);
+ Console.WriteLine(string.Format(System.Globalization.CultureInfo.InvariantCulture,
+ " poly 0x{0:X4} n=({1:F2},{2:F2},{3:F2}) X=[{4:F2},{5:F2}] Y=[{6:F2},{7:F2}] Z=[{8:F2},{9:F2}]",
+ polyId, nWorld.X, nWorld.Y, nWorld.Z, wxMin, wxMax, wyMin, wyMax, wzMin, wzMax));
+ }
+ Console.WriteLine($" Total: {nearXYCount}");
+
int matched = 0;
int matchedXY = 0;
Console.WriteLine("");
- Console.WriteLine("=== All cottage polys with XY overlap (any Z) ===");
+ Console.WriteLine("=== Tight: All cottage polys with XY overlap of sphere AABB (any Z) ===");
foreach (var (polyId, poly) in physics.Resolved)
{
// Transform vertices to world space.
diff --git a/tests/AcDream.Core.Tests/Physics/DoorSetupGfxObjInspectionTests.cs b/tests/AcDream.Core.Tests/Physics/DoorSetupGfxObjInspectionTests.cs
index 723aac2..0b18715 100644
--- a/tests/AcDream.Core.Tests/Physics/DoorSetupGfxObjInspectionTests.cs
+++ b/tests/AcDream.Core.Tests/Physics/DoorSetupGfxObjInspectionTests.cs
@@ -262,6 +262,80 @@ public class DoorSetupGfxObjInspectionTests
}
}
+ ///
+ /// A6.P4 door inside-out (2026-05-25 late) — inspects LandBlockInfo
+ /// 0xA9B4FFFE to identify all static entities at the Holtburg
+ /// cottage doorway area. The captured walkthrough has sphere at
+ /// world (133.655, 17.59) — no cottage GfxObj polys exist there.
+ /// Maybe a different entity (stab object, second cottage GfxObj,
+ /// building wall sub-piece) lives at that XY.
+ ///
+ [Fact]
+ public void HoltburgLandblockStatics_DatInspection()
+ {
+ var datDir = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR")
+ ?? Path.Combine(Env.GetFolderPath(Env.SpecialFolder.UserProfile),
+ "Documents", "Asheron's Call");
+ if (!Directory.Exists(datDir))
+ {
+ _out.WriteLine($"SKIP: dat directory not found at {datDir}");
+ return;
+ }
+
+ using var dats = new DatCollection(datDir, DatAccessType.Read);
+
+ // Landblock 0xA9B4 — the captured Holtburg cottage area.
+ // LandBlockInfo id = (landblockId & 0xFFFF0000) | 0xFFFE
+ var lbInfo = dats.Get(0xA9B4FFFEu);
+ if (lbInfo is null)
+ {
+ _out.WriteLine("LandBlockInfo 0xA9B4FFFE: NULL");
+ return;
+ }
+
+ _out.WriteLine($"=== LandBlockInfo 0xA9B4FFFE ===");
+ _out.WriteLine($" NumCells = {lbInfo.NumCells}");
+ _out.WriteLine($" Objects = {lbInfo.Objects.Count} (landblock stabs)");
+ _out.WriteLine($" Buildings = {lbInfo.Buildings.Count}");
+
+ // The captured walkthrough sphere position.
+ const float SphereX = 133.655f;
+ const float SphereY = 17.59f;
+ const float Window = 4f; // search window
+
+ _out.WriteLine("");
+ _out.WriteLine($"=== Stabs (Objects) within {Window} m of sphere XY ({SphereX:F2}, {SphereY:F2}) ===");
+ int stabHits = 0;
+ for (int i = 0; i < lbInfo.Objects.Count; i++)
+ {
+ var stab = lbInfo.Objects[i];
+ float dx = stab.Frame.Origin.X - SphereX;
+ float dy = stab.Frame.Origin.Y - SphereY;
+ float dist = MathF.Sqrt(dx * dx + dy * dy);
+ if (dist > Window) continue;
+ stabHits++;
+ _out.WriteLine($" [{i}] id=0x{stab.Id:X8} pos=({stab.Frame.Origin.X:F3},{stab.Frame.Origin.Y:F3},{stab.Frame.Origin.Z:F3}) dist={dist:F3}");
+ }
+ _out.WriteLine($" Total stab hits: {stabHits}");
+
+ _out.WriteLine("");
+ _out.WriteLine($"=== ALL Buildings (sorted by distance to sphere) ===");
+ var buildings = lbInfo.Buildings
+ .Select((b, i) => new {
+ Idx = i, B = b,
+ Dist = MathF.Sqrt(
+ (b.Frame.Origin.X - SphereX) * (b.Frame.Origin.X - SphereX) +
+ (b.Frame.Origin.Y - SphereY) * (b.Frame.Origin.Y - SphereY))
+ })
+ .OrderBy(x => x.Dist)
+ .Take(6)
+ .ToList();
+ foreach (var x in buildings)
+ {
+ _out.WriteLine($" [{x.Idx}] modelId=0x{x.B.ModelId:X8} pos=({x.B.Frame.Origin.X:F3},{x.B.Frame.Origin.Y:F3},{x.B.Frame.Origin.Z:F3}) dist={x.Dist:F3} portals={x.B.Portals.Count} numLeaves={x.B.NumLeaves}");
+ }
+ }
+
private void InspectCell(DatCollection dats, uint cellId)
{
string typeLabel = (cellId & 0xFFFFu) >= 0x0100u ? "indoor" : "outdoor";