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";