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 4a851b5..8b31333 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 @@ -142,27 +142,50 @@ 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) +## What's next (revised AGAIN — corner test PASSED, bug is state-related) -**Investigate sphere-vs-corner collision behavior** at the alcove -east wall → cottage north wall meeting point at world (133.5, 17.10). +**Corner-slide hypothesis: FALSIFIED.** `CornerSlide_AlcoveEastToCottageNorth_ShouldBlock` +test runs cottage GfxObj + cell 0x0150 BSP both registered. Places +sphere at (132.95, 16.8, 94) inside alcove near east wall. Walks +Y +50 times at 0.05 m/tick. **Sphere stays put at (132.95, 16.8) for all +50 ticks with cn=(0.71, -0.71, 0)** — the corner normal between +alcove east wall and cottage north wall. **The corner handling works +correctly in the harness.** -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 +So production's walkthrough is **a STATE difference**, not a geometric +or collision-detection bug. The harness's sphere can't reach +X=133.655 inside the cottage geometry. Production's sphere does +reach it somehow. -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. +Differences between harness and production: +- Harness uses identity walkable polygon (big quad). Production uses + real cell walkable polys (small, with edges). +- Harness has stub landblock terrain at Z=-1000. Production has real + terrain. +- Harness uses fresh body each tick. Production has accumulated state + from many prior ticks (velocity, contact plane history, etc.). +- Harness uses sphereRadius=0.48 + sphereHeight=1.20 exactly. Production + matches but might have different stepUp / stepDown. -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. +**Next-session apparatus**: replay the EXACT captured tick 2586's body +state through the corner-blocking test setup. Tick 2586 was where +sphere went from indoor cell 0x0150 to outdoor cell 0x0029 at +PrevPy=17.586, Py=17.586 (no Y motion) with X=134.022 (way past alcove +east wall). That tick is the smoking-gun "how did sphere get to X=134 +inside alcove" event. Load its body state into the harness, replay +the call, see what the engine reports about getting to that position. + +If the harness blocks (sphere can't reach X=134), then production has +state we're not capturing — probably accumulated push/depenetration +across many earlier ticks. If the harness reproduces sphere at X=134, +the bug is in the specific body state at that moment. + +The cleanest path forward is **cdb attach to retail** as the original +handoff recommended. Inspect what retail does FRAME-BY-FRAME at the +same doorway approach. If retail walks the user inside cottage at +off-center approach EXACTLY like we do — the bug isn't a bug, and +we should accept the behavior. If retail blocks cleanly — diff +retail's body state evolution vs ours to find the divergence. ## OLD (superseded) "what's next" candidates diff --git a/tests/AcDream.Core.Tests/Physics/DoorBugTrajectoryReplayTests.cs b/tests/AcDream.Core.Tests/Physics/DoorBugTrajectoryReplayTests.cs index 3cd469e..c6fcb91 100644 --- a/tests/AcDream.Core.Tests/Physics/DoorBugTrajectoryReplayTests.cs +++ b/tests/AcDream.Core.Tests/Physics/DoorBugTrajectoryReplayTests.cs @@ -441,6 +441,145 @@ public class DoorBugTrajectoryReplayTests $"wall is now blocking as expected — the #98 gate fix landed."); } + /// + /// A6.P4 corner-slide hypothesis (2026-05-25 late) — reproduces the + /// inside-out walkthrough at unit-test speed. Builds engine with + /// BOTH cottage GfxObj 0x01000A2B (which contains the north exterior + /// wall east of doorway at X=[133.5, 136.3], Y=17.10) AND cell + /// 0xA9B40150's BSP (alcove east wall at X=133.5, Y=[16.5, 17.1]). + /// Sphere starts inside alcove sliding against east wall, then + /// walks NORTH. If harness slides sphere past the corner at + /// (133.5, 17.10) to end up at X > 133.5 Y > 17.10, bug reproduced. + /// + [Fact] + public void CornerSlide_AlcoveEastToCottageNorth_ShouldBlock() + { + var datDir = ResolveDatDir(); + if (datDir is null) return; + + var (engine, cache) = BuildFaithfulDoorEngine(datDir); + + // 1. Register cottage GfxObj (contains the north exterior wall). + const uint CottageGfxId = 0x01000A2Bu; + const uint CottageEntityId = 0x00A9B479u; + var cottageFixturePath = Path.Combine(SolutionRoot(), + "tests", "AcDream.Core.Tests", "Fixtures", "issue98", + "0x01000A2B.gfxobj.json"); + 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); + + // 2. Load cell 0xA9B40150 BSP into cache (the alcove walls). + const uint AlcoveCellId = 0xA9B40150u; + using (var dats = new DatCollection(datDir, DatAccessType.Read)) + { + var envCell = dats.Get(AlcoveCellId); + Assert.NotNull(envCell); + var environment = dats.Get( + 0x0D000000u | envCell!.EnvironmentId); + Assert.NotNull(environment); + Assert.True(environment!.Cells.TryGetValue(envCell.CellStructure, out var cellStruct)); + + var cellOriginWorld = envCell.Position.Origin; + var cellTransform = + Matrix4x4.CreateFromQuaternion(envCell.Position.Orientation) * + Matrix4x4.CreateTranslation(cellOriginWorld); + cache.CacheCellStruct(AlcoveCellId, envCell, cellStruct!, cellTransform); + } + Assert.NotNull(cache.GetCellStruct(AlcoveCellId)); + + // 3. Sphere setup: inside alcove, near east wall. + // Alcove east wall at world X=133.5, Y=[16.5, 17.1]. Sphere at + // X=132.95 (sphere east edge 133.43 just west of wall), Y=16.8 + // (inside alcove Y range). + var currentPos = new Vector3(132.95f, 16.8f, 94f); + + // Walk sphere in +Y direction (toward cottage exterior north wall). + // Repeat several ticks with small steps to mimic walk-speed motion. + Vector3 pos = currentPos; + uint cellId = AlcoveCellId; + bool isOnGround = true; + + var body = new PhysicsBody + { + Position = pos, + Orientation = Quaternion.Identity, + ContactPlaneValid = true, + ContactPlane = new System.Numerics.Plane(0f, 0f, 1f, -94f), + ContactPlaneCellId = cellId, + WalkablePolygonValid = true, + WalkablePlane = new System.Numerics.Plane(0f, 0f, 1f, -94f), + WalkableVertices = new[] + { + new Vector3(120f, 10f, 94f), + new Vector3(145f, 10f, 94f), + new Vector3(145f, 30f, 94f), + new Vector3(120f, 30f, 94f), + }, + WalkableUp = Vector3.UnitZ, + TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable, + }; + + Console.WriteLine($"Start: pos=({pos.X:F3},{pos.Y:F3},{pos.Z:F3}) cell=0x{cellId:X8}"); + for (int t = 1; t <= 50; t++) + { + var target = pos + new Vector3(0f, 0.05f, 0f); // walk speed + var result = engine.ResolveWithTransition( + currentPos: pos, + targetPos: target, + cellId: cellId, + sphereRadius: 0.48f, + sphereHeight: 1.20f, + stepUpHeight: 0.60f, + stepDownHeight: 1.5f, + isOnGround: isOnGround, + body: body, + moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide, + movingEntityId: DoorEntityId + 1); + + pos = result.Position; + cellId = result.CellId; + isOnGround = result.IsOnGround; + body.Position = pos; + + if (t % 5 == 0 || result.CollisionNormalValid) + { + Console.WriteLine(string.Format(System.Globalization.CultureInfo.InvariantCulture, + "t={0,2} pos=({1:F3},{2:F3},{3:F3}) cell=0x{4:X8} cnValid={5} cn=({6:F2},{7:F2},{8:F2})", + t, pos.X, pos.Y, pos.Z, cellId, + result.CollisionNormalValid, + result.CollisionNormal.X, result.CollisionNormal.Y, result.CollisionNormal.Z)); + } + + // Stop if sphere has clearly walked through the wall. + if (pos.Y > 18f) break; + } + + Console.WriteLine($"Final pos: ({pos.X:F3},{pos.Y:F3},{pos.Z:F3}) cell=0x{cellId:X8}"); + + // Document expected: sphere should stop at sphere center Y = + // 17.10 - 0.48 = 16.62 (cottage north wall + sphere reach). + // Bug: sphere slides past corner and exits north. + Assert.True(pos.Y < 17.20f, + $"BUG REPRODUCTION: sphere walked from inside alcove to Y={pos.Y:F3} " + + $"(past cottage north wall at Y=17.10). Cottage wall should have blocked " + + $"sphere at Y ≈ 16.62 (wall - sphere reach). If this assertion FAILS, " + + $"the corner handling at (X=133.5, Y=17.10) is letting sphere slide past."); + } + /// /// Diagnostic: dump cottage GfxObj 0x01000A2B polygons near world /// position (133.655, 17.59, 94.5) — the sphere position where the