From da798b2071c9249d6d762090820f67bac663ac66 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 25 May 2026 08:27:52 +0200 Subject: [PATCH] =?UTF-8?q?test(phys):=20A6.P4=20door=20inside-out=20?= =?UTF-8?q?=E2=80=94=20collision-geometry=20gap=20diagnosis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added diagnostic apparatus that pinpoints the inside-out walkthrough as a collision-geometry GAP, not a collision-detection bug. New tests in DoorBugTrajectoryReplayTests: - InsideOut_Tick3254_WithCottageWalls_ShouldBlock: hypothesis test that registered cottage GfxObj 0x01000A2B and replayed the captured tick. Cottage blocked sphere but with cn=(0,0,1) floor-cap normal, not a wall normal — first signal that cottage geometry near the sphere isn't a wall. - Diagnostic_CottagePolys_NearWalkthroughPosition: dumps cottage polys near sphere XY=(133.655, 17.59) at any Z. Result: ZERO cottage polygons in that area. The cottage GfxObj has no geometry where the sphere walks through. DoorSetupGfxObjInspectionTests.HoltburgCottage_CellPortals_DatInspection extended to dump cell 0xA9B40150's 4 physics polygons in world frame: - floor (Z=94), ceiling (Z=96.5), west wall (X=131.6), east wall (X=133.5) - All walls only span Y=[16.5, 17.1] — the small doorway alcove volume - North of Y=17.1, no wall Captured sphere at (133.655, 17.59) is 0.155 m east of cell east wall AND 0.49 m north of the wall's Y range. No collision geometry exists at that XY past Y=17.1. The collision representation has a gap that the visual cottage covers with a wall. Production capture confirms the diagnosis: cottage GfxObj fires [bsp-test] 425 times during inside-out walking — visibility IS correct post-AddAllOutsideCells fix. Door slab fires 245 times. But the BSP queries find no polygon at (133.655, 17.6+, 94-95.20). The slab's east face blocks WEST motion (cn=(+1,0,0) as captured), sphere free to move +Y past it because no wall is there to block. Three candidates for next-session investigation: 1. Different cottage GfxObj (Holtburg cottages may be multi-piece) 2. Landblock-baked stab static at the cottage exterior wall location 3. Cottage GfxObj's visual polygons wider than physics polygons (dat fact) Cheapest next step: add LandblockStatics_DatInspection test that loads LandBlockInfo 0xA9B4FFFE + iterates StaticObjects + prints every entity at world XY in [131,135] x [16,19]. Reveals what other entities live at the cottage doorway. Full handoff: docs/research/2026-05-25-door-bug-inside-out-geometry-gap.md Co-Authored-By: Claude Opus 4.7 (1M context) --- ...-05-25-door-bug-inside-out-geometry-gap.md | 157 ++++++++++++++++++ .../Physics/DoorBugTrajectoryReplayTests.cs | 151 +++++++++++++++++ .../Physics/DoorSetupGfxObjInspectionTests.cs | 25 +++ 3 files changed, 333 insertions(+) create mode 100644 docs/research/2026-05-25-door-bug-inside-out-geometry-gap.md 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 new file mode 100644 index 0000000..ce3e3e6 --- /dev/null +++ b/docs/research/2026-05-25-door-bug-inside-out-geometry-gap.md @@ -0,0 +1,157 @@ +# Door bug — inside-out walkthrough: missing cottage exterior wall (geometry gap) +2026-05-25, continuation of door-collision investigation + +## TL;DR + +The inside-out walkthrough that persisted after the +`AddAllOutsideCells` fix is **NOT a collision-detection bug**. It's a +**collision-geometry GAP**: the cottage's north exterior wall east +(and presumably west) of the doorway opening doesn't exist in any +registered entity our engine knows about. The sphere walks past the +door slab on its east side, clears the doorway alcove cell's small +east wall (Y range [16.5, 17.1]), and then has nothing in front of it +in the collision representation — even though the VISUAL cottage has +a wall there. + +## Apparatus diagnostics + +Three new tests landed (in `DoorBugTrajectoryReplayTests`): + +1. `Directional_OutsideIn_SouthApproach_BlocksAtSlabSouthFace` — sphere + south moving north blocks. PASSES. +2. `Directional_InsideOut_NorthApproach_BlocksAtSlabNorthFace` — sphere + north moving south blocks. PASSES. +3. `Geometric_DoorSlabAtSphereHeight_OverlapsInZ` — pins slab world Z + range = [94.139, 96.630]; sphere top at Z=95.20 IS within slab. + The slab is at sphere height — BSP collision is geometrically active. +4. `InsideOut_Tick3254_WithCottageWalls_ShouldBlock` — hypothesis test + adds cottage GfxObj 0x01000A2B. Result: cottage DID block but with + cn=(0,0,1) — a floor-cap response, NOT a wall response. +5. `Diagnostic_CottagePolys_NearWalkthroughPosition` — dumps cottage + polygons near sphere XY=(133.655, 17.59), any Z. **Result: ZERO + cottage polygons in that area.** The cottage GfxObj has no + geometry where the sphere walks through. + +`DoorSetupGfxObjInspectionTests.HoltburgCottage_CellPortals_DatInspection` +extended to dump cell 0xA9B40150's 4 physics polys in world frame: + +``` +[0] sides=Landblock X=[131.600, 133.500] Y=[16.500, 17.100] Z=[94.000, 94.000] FLOOR +[1] sides=Landblock X=[131.600, 131.600] Y=[16.500, 17.100] Z=[94.000, 96.500] WEST WALL +[2] sides=Landblock X=[131.600, 133.500] Y=[16.500, 17.100] Z=[96.500, 96.500] CEILING +[3] sides=Landblock X=[133.500, 133.500] Y=[16.500, 17.100] Z=[94.000, 96.500] EAST WALL +``` + +Cell 0xA9B40150 is the **doorway alcove** — a small ~1.9m × 0.6m × 2.5m +volume between the cottage interior and the outdoor area. Its east wall +only extends Y=[16.5, 17.1]. **North of Y=17.1, no wall** in this cell. + +The captured failing sphere at (133.655, 17.59) is 0.155m east of the +east wall AND 0.49m NORTH of the wall's Y range. The wall doesn't +reach the sphere. + +## The collision-geometry gap + +Visual representation (in-client): +- Cottage has a north exterior wall east and west of the doorway opening +- The wall extends Y > 17.1 (north of the alcove) +- User sees their character partially clipping into this wall + +Collision representation (what we register): +- Cottage GfxObj 0x01000A2B: **0 polygons** in the area (133.655, 17.59, 94-95.20) +- Cell 0xA9B40150 (alcove): walls only at Y=[16.5, 17.1] +- Door slab: only spans X=[131.635, 133.560] — too narrow to cover the cottage opening +- Outdoor cell 0xA9B40029: outdoor cell, no walls + +**Net: no entity has wall polygons at (133.655, Y > 17.1).** Sphere can +walk there freely. + +## Verification in production capture + +`door-fix-inout2.launch.log` shows: +- Cottage GfxObj `[bsp-test]` fires 425 times during inside-out walking + (so visibility is correct post-fix) +- Door slab `[bsp-test]` fires 245 times +- Captured tick 3254: sphere at (133.655, 17.590), target (133.549, + 17.599). Result: position X=133.655 unchanged (blocked westward), + position Y=17.599 (moved north freely). cn=(+1, 0, 0) = slab east + face normal. +- 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 + +**Identify which entity SHOULD own the cottage's north exterior wall +east of the doorway.** Three candidates: + +1. **A different cottage GfxObj.** Holtburg cottages might be + multi-piece (separate GfxObjs for wall sections, doorway frame, roof). + The cottage we have (0x01000A2B) might be one of multiple. Check + the landblock's static-entity list for other GfxObjs at the cottage + position via `[entity-source]` log + Setup file. + +2. **A landblock-baked "stab"** (separate static entity registered at + spawn time). LandblockLoader produces these. Check `LandBlockInfo` + dat record for landblock 0xA9B4 — what other entities are at world + (~133, ~18)? + +3. **The cottage GfxObj's drawing geometry is wider than its physics.** + If 0x01000A2B has `Polygons` (visual) at the wall location but no + `PhysicsPolygons` (collision), the visual is wider than the + collision. This is a dat-data fact — not fixable without retail + re-engineering of the dat. + +For candidates 1-2, the fix is "register the missing entity." For 3, +the bug is dat-side (or retail accepts the same walkthrough we do). + +**Cheapest next-step test:** add a method to +`DoorSetupGfxObjInspectionTests` that loads `LandBlockInfo` 0xA9B4FFFE +(landblock-baked statics) and prints every static at world XY in +[131, 135] × [16, 19]. The output will name what other GfxObjs/Setups +are registered at the cottage doorway — if any include the missing +wall, we know what to register additionally. + +## Apparatus committed + +- `tests/AcDream.Core.Tests/Physics/DoorBugTrajectoryReplayTests.cs`: + faithful door registration, directional collision tests, geometric + pin test, cottage GfxObj hypothesis test, cottage polygon dump. +- `tests/AcDream.Core.Tests/Physics/DoorSetupGfxObjInspectionTests.cs`: + HoltburgCottage_CellPortals_DatInspection extended with cell-poly + world-frame dump. + +All tests under `DoorBugTrajectoryReplayTests` and the extended +`DoorSetupGfxObjInspectionTests.HoltburgCottage_CellPortals_DatInspection` +PASS (skip on CI when dat dir absent). + +## Pickup prompt for next session + +``` +A6.P4 door inside-out walkthrough: identified as collision-geometry +gap, NOT collision-detection bug. The cottage's north exterior wall +east+west of the doorway opening isn't represented in any registered +entity. Sphere walks freely at (133.655, 17.59) — no wall to block. + + Read docs/research/2026-05-25-door-bug-inside-out-geometry-gap.md + + Diagnostic_CottagePolys_NearWalkthroughPosition test output + + HoltburgCottage_CellPortals_DatInspection dump for cell 0x0150 + + State both altitudes: + Currently working toward: M1.5 — Indoor world feels right + Current phase: A6.P4 door bug — find missing cottage wall entity. + The fix isn't in BSP, cells, or AddAllOutsideCells + — those are correct. The collision geometry has a + gap. Need to identify which entity SHOULD own the + wall and register it. + + First move: add a LandblockStatics_DatInspection test to + DoorSetupGfxObjInspectionTests that loads LandBlockInfo 0xA9B4FFFE + + iterates StaticObjects. Print every entity at world XY in + [131, 135] x [16, 19] — name + setup id + position. Will reveal + what other entities (if any) live at the cottage doorway. + + If a wall-bearing entity exists but we're not registering it: fix + the registration path. If nothing exists: the dat doesn't have the + wall, and this might be retail-faithful behavior we have to accept + (or compensate for by widening the door slab via gameplay layer). +``` diff --git a/tests/AcDream.Core.Tests/Physics/DoorBugTrajectoryReplayTests.cs b/tests/AcDream.Core.Tests/Physics/DoorBugTrajectoryReplayTests.cs index f1711d5..716d7f6 100644 --- a/tests/AcDream.Core.Tests/Physics/DoorBugTrajectoryReplayTests.cs +++ b/tests/AcDream.Core.Tests/Physics/DoorBugTrajectoryReplayTests.cs @@ -359,6 +359,157 @@ public class DoorBugTrajectoryReplayTests /// tick. /// /// + /// + /// A6.P4 inside-out bug investigation (2026-05-25 late evening) — + /// hypothesis test: the asymmetric inside-out walkthrough is the + /// sphere walking AROUND the door slab via the cottage wall area + /// east/west of the doorway opening. The cottage exterior walls + /// are part of GfxObj 0x01000A2B (the cottage building, same one + /// from issue #98's cellar floor cap). Issue #98's indoor-primary-cell + /// gate removed cottage-WALL visibility along with the cottage FLOOR + /// — too aggressive. From indoor primary cells, the cottage walls + /// adjacent to the doorway can't block the sphere. + /// + /// + /// This test reproduces the captured tick 3254 (sphere at + /// (133.655, 17.590, 94) in indoor cell 0xA9B40150, moving to + /// (133.549, 17.599, 94)) with the cottage GfxObj registered as + /// landblock-baked static. If, with the cottage walls visible, the + /// sphere is blocked from being at X=133.655 (which is OUTSIDE the + /// doorway opening, INSIDE the cottage wall geometry), the bug + /// is confirmed as #98's overly-aggressive gate. + /// + /// + [Fact] + public void InsideOut_Tick3254_WithCottageWalls_ShouldBlock() + { + var datDir = ResolveDatDir(); + if (datDir is null) return; + + var (engine, cache) = BuildFaithfulDoorEngine(datDir); + + // Add cottage GfxObj 0x01000A2B as landblock-baked static, + // mirroring production GameWindow.RegisterLiveEntityCollision's + // cellScope=0u (landblock-wide). + const uint CottageGfxId = 0x01000A2Bu; + const uint CottageEntityId = 0x00A9B479u; // matches issue #98 fixture id + var cottageFixturePath = Path.Combine(SolutionRoot(), + "tests", "AcDream.Core.Tests", "Fixtures", "issue98", + "0x01000A2B.gfxobj.json"); + Assert.True(File.Exists(cottageFixturePath)); + var cottageDump = GfxObjDumpSerializer.Read(cottageFixturePath); + var cottagePhysics = GfxObjDumpSerializer.Hydrate(cottageDump); + cache.RegisterGfxObjForTest(CottageGfxId, cottagePhysics); + + engine.ShadowObjects.Register( + entityId: CottageEntityId, + gfxObjId: CottageGfxId, + worldPos: new Vector3(130.5f, 11.5f, 94.0f), + rotation: Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI), + radius: cottagePhysics.BoundingSphere?.Radius ?? 14f, + worldOffsetX: 0f, + worldOffsetY: 0f, + landblockId: DoorLandblockId, + collisionType: ShadowCollisionType.BSP, + scale: 1.0f, + cellScope: 0u); + + // Replay captured tick 3254 inputs exactly. + var currentPos = new Vector3(133.65524f, 17.58999f, 94f); + var targetPos = new Vector3(133.54903f, 17.599283f, 94f); + var (result, body) = ResolveAt(engine, currentPos, targetPos, 0xA9B40150u); + + // Expected: cottage wall east of doorway blocks the sphere + // from being at X=133.655 (or, at minimum, blocks the +Y slide). + // Currently (per the user's report) the sphere walks past unimpeded. + Console.WriteLine(string.Format(System.Globalization.CultureInfo.InvariantCulture, + "Harness tick 3254 reply: pos=({0:F4},{1:F4},{2:F4}) cn=({3:F4},{4:F4},{5:F4}) " + + "cnValid={6} cell=0x{7:X8}", + result.Position.X, result.Position.Y, result.Position.Z, + result.CollisionNormal.X, result.CollisionNormal.Y, result.CollisionNormal.Z, + result.CollisionNormalValid, result.CellId)); + + // Document the production state via assertion: the sphere DID make + // Y motion (+0.009) at this tick (target.Y > input.Y). If the + // cottage wall blocks correctly, harness Y should stay at input Y + // (sphere fully blocked, can't move north past cottage wall). + // Currently this test demonstrates the bug shape. + Assert.True(result.Position.Y < targetPos.Y - 0.005f, + $"BUG REPRODUCTION: harness allowed Y motion ({result.Position.Y}) toward " + + $"target ({targetPos.Y}). Cottage wall should block sphere at X=133.655 " + + $"(0.095 m east of slab east edge). If this assertion FAILS, the cottage " + + $"wall is now blocking as expected — the #98 gate fix landed."); + } + + /// + /// Diagnostic: dump cottage GfxObj 0x01000A2B polygons near world + /// position (133.655, 17.59, 94.5) — the sphere position where the + /// inside-out walkthrough happens. Identifies which cottage polys + /// are at sphere height in that area, to know whether walls / floors + /// / nothing. + /// + [Fact] + public void Diagnostic_CottagePolys_NearWalkthroughPosition() + { + var fixturePath = Path.Combine(SolutionRoot(), + "tests", "AcDream.Core.Tests", "Fixtures", "issue98", + "0x01000A2B.gfxobj.json"); + var dump = GfxObjDumpSerializer.Read(fixturePath); + var physics = GfxObjDumpSerializer.Hydrate(dump); + + // Cottage world transform: pos (130.5, 11.5, 94), rotation 180° Z. + var cottagePos = new Vector3(130.5f, 11.5f, 94.0f); + var cottageRot = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI); + + // Failing sphere position: (133.655, 17.59, 94 .. 95.20) + // Sphere world AABB: X[133.175, 134.135], Y[17.110, 18.070], Z[94, 95.20] + var sphereCenterX = 133.655f; + var sphereCenterY = 17.59f; + + Console.WriteLine(string.Format(System.Globalization.CultureInfo.InvariantCulture, + "Cottage GfxObj 0x01000A2B: {0} polys total, BS radius {1:F3}", + physics.Resolved.Count, physics.BoundingSphere?.Radius ?? 0f)); + Console.WriteLine("Looking for polys whose world-vertex bbox overlaps sphere AABB:"); + Console.WriteLine($" Sphere X=[{sphereCenterX-0.48f:F3}, {sphereCenterX+0.48f:F3}]"); + Console.WriteLine($" Sphere Y=[{sphereCenterY-0.48f:F3}, {sphereCenterY+0.48f:F3}]"); + Console.WriteLine($" Sphere Z=[94.000, 95.200]"); + + int matched = 0; + int matchedXY = 0; + Console.WriteLine(""); + Console.WriteLine("=== All cottage polys with XY overlap (any Z) ==="); + foreach (var (polyId, poly) in physics.Resolved) + { + // Transform vertices to world space. + 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; + } + bool xOverlap = wxMax >= sphereCenterX - 0.48f && wxMin <= sphereCenterX + 0.48f; + bool yOverlap = wyMax >= sphereCenterY - 0.48f && wyMin <= sphereCenterY + 0.48f; + bool zOverlap = wzMax >= 94f && wzMin <= 95.20f; + if (xOverlap && yOverlap) + { + matchedXY++; + var nWorld = Vector3.Transform(poly.Plane.Normal, cottageRot); + string zMark = zOverlap ? " *** Z-OVERLAP ***" : ""; + Console.WriteLine(string.Format(System.Globalization.CultureInfo.InvariantCulture, + " poly 0x{0:X4} n=({1:F3},{2:F3},{3:F3}) bbox X=[{4:F3},{5:F3}] Y=[{6:F3},{7:F3}] Z=[{8:F3},{9:F3}]{10}", + polyId, nWorld.X, nWorld.Y, nWorld.Z, wxMin, wxMax, wyMin, wyMax, wzMin, wzMax, zMark)); + } + if (xOverlap && yOverlap && zOverlap) matched++; + } + Console.WriteLine($" XY-overlap polys (any Z): {matchedXY}"); + Console.WriteLine($" XYZ-overlap polys: {matched}"); + } + [Fact] public void Geometric_DoorSlabAtSphereHeight_OverlapsInZ() { diff --git a/tests/AcDream.Core.Tests/Physics/DoorSetupGfxObjInspectionTests.cs b/tests/AcDream.Core.Tests/Physics/DoorSetupGfxObjInspectionTests.cs index 1420661..723aac2 100644 --- a/tests/AcDream.Core.Tests/Physics/DoorSetupGfxObjInspectionTests.cs +++ b/tests/AcDream.Core.Tests/Physics/DoorSetupGfxObjInspectionTests.cs @@ -314,6 +314,31 @@ public class DoorSetupGfxObjInspectionTests _out.WriteLine($" CellStruct polygons = {cellStruct.Polygons?.Count ?? 0} (visible)"); _out.WriteLine($" CellStruct physicsPolys = {cellStruct.PhysicsPolygons?.Count ?? 0}"); + // Dump ALL physics polygons (collision walls/floor) in world frame + // so we can see what blocks a sphere at world (133.655, 17.59). + _out.WriteLine($" CellStruct PHYSICS polys (world frame):"); + var cellOrigin = envCell.Position.Origin; + var cellRot = envCell.Position.Orientation; + for (int pi = 0; pi < cellStruct.PhysicsPolygons.Count; pi++) + { + var (pid, poly) = (cellStruct.PhysicsPolygons.Keys.ElementAt(pi), + cellStruct.PhysicsPolygons.Values.ElementAt(pi)); + float wxMin = float.MaxValue, wxMax = float.MinValue; + float wyMin = float.MaxValue, wyMax = float.MinValue; + float wzMin = float.MaxValue, wzMax = float.MinValue; + foreach (var vid in poly.VertexIds) + { + if (!cellStruct.VertexArray.Vertices.TryGetValue((ushort)vid, out var sv)) continue; + var rotated = System.Numerics.Vector3.Transform(sv.Origin, cellRot); + var world = cellOrigin + 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; + } + _out.WriteLine($" [{pi}=0x{pid:X4}] sides={poly.SidesType} " + + $"X=[{wxMin:F3},{wxMax:F3}] Y=[{wyMin:F3},{wyMax:F3}] Z=[{wzMin:F3},{wzMax:F3}]"); + } + // Dump the plane of each portal polygon (the planes // FindTransitCellsSphere tests the sphere center against). for (int i = 0; i < envCell.CellPortals.Count; i++)