diff --git a/docs/research/2026-05-24-door-collision-task7-shipped-but-bug-remains.md b/docs/research/2026-05-24-door-collision-task7-shipped-but-bug-remains.md new file mode 100644 index 0000000..d348122 --- /dev/null +++ b/docs/research/2026-05-24-door-collision-task7-shipped-but-bug-remains.md @@ -0,0 +1,247 @@ +# Door collision — Task 7 shipped, partial fix, deeper bug remains + +**Date:** 2026-05-24 (evening, continuation of door collision investigation) +**Branch:** `claude/strange-albattani-3fc83c` +**Status:** A6.P4 architecture is correct. Multi-part registration works. +The Holtburg door bug PARTIALLY fixed — center blocks, but off-center +and inside-out still walk through. Root cause is downstream in the +engine's grounded BSP collision path (Path 5 + step-up), NOT in the +multi-part registration we just shipped. + +--- + +## TL;DR + +**Three commits shipped** (composable foundation): + +| SHA | Title | What it does | +|---|---|---| +| `e1d94d7` | dat-inspection test | Confirmed door part `0x010044B5` has full 1.9×0.26×2.5 m BSP slab (6 Landblock polys). Hypothesis A from prior handoff was wrong. | +| `3b7dc46` | `GetNearbyObjects` dedup fix | Changed `HashSet` (entityId) → `HashSet`. Multi-part shapes no longer silently dropped. | +| `ca9341c` | Task 7 live wiring | `RegisterLiveEntityCollision` uses `ShadowShapeBuilder.FromSetup` + `RegisterMultiPart`. Doors now register cyl+bsp instead of just cyl. | + +**Live verification (visual user test):** + +| Scenario | Result | +|---|---| +| Dead center, walk into closed door (outside) | ✅ Blocks | +| 50 cm off-center, walk into closed door (outside) | ❌ Walks through | +| Inside walking out (closed door) | ❌ Walks through | +| Use door → swing → walk through | ✅ Works (ETHEREAL flip path) | + +**Probe-instrumented live capture confirms multi-part registration works:** + +- Every door spawn shows `[entity-source] shapes=cyl1+bsp1` — both shapes register. +- BSP part `0x010044B5` is visited 135 times for a single door at player approaches as close as `distXY=0.415 m`. +- `cacheHit=True` for every visit — the cache is populated. +- BUT: zero `[resolve-bldg]` attributions for the BSP shape (all 19 attributed hits show `gfxObj=0x00000000` = the Cylinder shape). + +So the BSP is being QUERIED but never produces an attributed hit. The +sphere walks through despite the BSP geometry being present and +visited. + +--- + +## What's in the tree right now + +``` +$ git log --oneline -8 +ca9341c feat(phys): A6.P4 Task 7 — RegisterLiveEntityCollision uses ShadowShapeBuilder + RegisterMultiPart +3b7dc46 fix(phys): GetNearbyObjects dedup-by-entityId silently drops multi-part shadows +e1d94d7 test(phys): door setup + GfxObj dat-inspection — Hypothesis A falsified +c89df8e docs(handoff): door collision per-part BSP session handoff (2026-05-24) +1498697 diag(phys): [cyl-test] probe — log every Cylinder shadow collision test +3e5dc8c test(phys): Task 6 regression — Deregister clears _entityShapes cache +d5ffb03 feat(phys): UpdatePosition handles multi-part entities +fca0a13 feat(phys): ShadowObjectRegistry.RegisterMultiPart +``` + +**Uncommitted (to commit next):** +- `src/AcDream.Core/Physics/TransitionTypes.cs` — new `[bsp-test]` probe in + the BSP collision dispatch, mirrors `[cyl-test]`. Fires when a BSP entry + is visited, BEFORE the cache lookup. Distinguishes "cache miss → silent + skip" from "queried but no hit." Gated on `ACDREAM_PROBE_BUILDING=1`. +- `tests/AcDream.Core.Tests/Physics/DoorCollisionApparatusTests.cs` — + new test `Apparatus_Grounded_50cmOffCenter_FrontApproach_DocumentsBug` + that attempts to reproduce the production bug with a grounded body + + seeded ContactPlane. Currently fails because the apparatus's behavior + diverges from production (apparatus blocks immediately at tick 0 with + a Z+ normal from the synthetic floor; production walks through). + +--- + +## Path 5 vs Path 6 — the divergence + +`BSPQuery.FindCollisions` dispatches to 6 paths based on `ObjectInfo` +state. The crucial difference: + +- **Path 6 (Default)** — fires when `obj.State` has no `Contact` flag. + Calls `SphereIntersectsPolyInternal` and `SetCollide` on hit. + **Apparatus tests use this path** (no body, `isOnGround=false`). They + all PASS — the door's BSP blocks the sphere correctly. + +- **Path 5 (Contact branch)** — fires when `obj.State.HasFlag(Contact)`. + Calls `SphereIntersectsPolyInternal`; on hit, calls + `StepSphereUp → DoStepUp → DoStepDown` to attempt climbing over the + obstacle. Returns OK if step succeeds, Slid if step fails. + **Production uses this path** (player grounded → `isOnGround=true` → + engine sets `Contact` flag at `PhysicsEngine.cs:631`). Production + WALKS THROUGH. + +So the bug is somewhere in Path 5's step-up logic. The leading +hypothesis (not yet proven): + +> When the player is standing on flat ground in front of the door, +> step-up's `DoStepDown` probes 0.6 m downward from the sphere's +> current position. It finds the SAME flat ground extending to the +> OTHER SIDE of the door (Holtburg cottages have no Z change between +> exterior and interior floor — both at Z=94). `find_walkable` +> declares step-up SUCCESS, the BSP collision returns `OK`, and the +> sphere walks through the door. +> +> The fix probably involves: step-up should reject if a forward probe +> at the lifted height STILL hits the same obstacle. The current +> DoStepDown probes only DOWNWARD; it doesn't verify that the +> forward motion at the lifted height is clear. + +This is speculation — needs apparatus verification. + +--- + +## Why the apparatus didn't reproduce the bug + +The grounded apparatus test (`Apparatus_Grounded_50cmOffCenter_*`) was +supposed to fail in the same way as production (walk through). Instead +it BLOCKED at tick 0 with normal=(0,0,1) — sphere position unchanged. + +Diagnostic output: +``` +[bsp-test] obj=0x000F424F gfx=0x010044B5 ... pos=(11.99,12.12,1.27) + distXY=1.234 cacheHit=True +[resolve] in=(12.500,11.000,0.480) tgt=(12.500,11.100,0.480) + out=(12.500,11.000,0.480) ok=True hit=yes n=(0,0,1) walkable=True +``` + +`ACDREAM_DUMP_STEPUP=1` produced no `stepup: ENTER` lines, so +`DoStepUp` was NOT called. The hit normal `(0,0,1)` came from +somewhere else (likely the seeded walkable polygon or the synthetic +floor interaction with the engine's terrain step-down). + +The apparatus's stub terrain (Z=-1000) + synthetic walkable poly at +Z=0 may be causing the engine to take a different code path than +production's real Holtburg terrain. Reproducing production fully +would require: + +1. Real terrain heightmap covering the test landblock at Z=94 +2. EnvCell or stab geometry near the test door +3. Proper cottage/cell setup so portal-reachable cells include + the door's outdoor cell when player is indoor + +This is significant apparatus investment. Worth it IF the bug +requires multi-tick simulation in real geometry to surface. For +now, the apparatus shows the broad shape: with proper grounded +state + seeded body, the engine doesn't take the same path as +the airborne (Path 6) test. + +--- + +## Recommended next steps (ranked) + +### Option A — Live diagnostic with ACDREAM_DUMP_STEPUP=1 (cheapest) + +Relaunch with `ACDREAM_PROBE_BUILDING=1` + `ACDREAM_DUMP_STEPUP=1`. +Walk into a closed door off-center. The step-up dump will show: +- Whether `DoStepUp` fires at all when the BSP hits +- If so, what the input normal is +- Whether `stepDown` succeeds or fails + +If `stepDown` succeeds (i.e., step-up climbs over the door), we've +confirmed the hypothesis above and can target the fix. + +### Option B — Build a richer apparatus + +Replace the stub terrain with a real heightmap-like surface at Z=94 +spanning the test landblock. Replace the synthetic walkable poly with +a proper terrain polygon at the door's world XY. This should let +Path 5 run the SAME way as production. Then iterate on the fix +locally in <500 ms. + +Estimated effort: 1-2 hours of apparatus work. + +### Option C — Direct retail cdb trace + +Attach cdb to a running retail client at a Holtburg cottage doorway, +break on `CTransition::step_up` or `CTransition::step_down`, and +observe how retail handles step-up against a door. Compare against +acdream's behavior. + +Estimated effort: 30 min - 2 hours depending on what we find. + +### Option D — Pivot to fix-and-verify + +Hypothesis-based fix: in `DoStepUp`, reject step-up if the input +collision normal is mostly horizontal AND the obstacle's bounding +sphere height range significantly exceeds the step-up height. The +door has BS radius 1.975 m centered at Z=1.275 → top of BS at Z=3.25, +way above step-up=0.6. If we detect "this obstacle is too tall to +step over," fall back to wall-slide. + +Risk: might break stairs / ramps. Need apparatus to verify. + +### Recommendation + +Option A first (~5 min, no code changes needed). If hypothesis +confirmed, then Option D (with apparatus from Option B for +regression testing). + +--- + +## Files touched this session (cumulative) + +**Committed:** +- `src/AcDream.Core/Physics/ShadowObjectRegistry.cs` (dedup fix) +- `src/AcDream.App/Rendering/GameWindow.cs` (Task 7 wiring) +- `tests/AcDream.Core.Tests/Physics/DoorSetupGfxObjInspectionTests.cs` (NEW) +- `tests/AcDream.Core.Tests/Physics/DoorCollisionApparatusTests.cs` (NEW) +- `docs/research/2026-05-24-door-dat-inspection-findings.md` (NEW) + +**Uncommitted (this doc + 2 file changes):** +- `src/AcDream.Core/Physics/TransitionTypes.cs` (added `[bsp-test]` probe) +- `tests/AcDream.Core.Tests/Physics/DoorCollisionApparatusTests.cs` + (added grounded test scenario — fails for unrelated apparatus + reasons but the probe wiring is sound) + +**Memory updated:** `feedback_dedup_keys_after_cardinality_change.md` + +--- + +## Pickup prompt for next session + +``` +A6.P4 Task 7 shipped (RegisterLiveEntityCollision uses +ShadowShapeBuilder + RegisterMultiPart) and the foundation fix +(GetNearbyObjects dedup on full ShadowEntry instead of entityId). +Production verification: center blocks, but off-center + inside-out +still walk through closed doors. The multi-part registration is +correct (verified by live probes); the remaining bug is downstream +in BSPQuery Path 5's step-up logic. + + Read docs/research/2026-05-24-door-collision-task7-shipped-but-bug-remains.md + + State both altitudes: + Currently working toward: M1.5 — Indoor world feels right + Current phase: A6.P4 door collision — step-up misbehavior + investigation. Multi-part registration shipped; + step-up at thin tall obstacles is the remaining bug. + + Recommended first move: Option A from the findings doc — relaunch + with ACDREAM_PROBE_BUILDING=1 + ACDREAM_DUMP_STEPUP=1, walk into + a Holtburg cottage door off-center. The step-up dump will reveal + whether DoStepUp is incorrectly succeeding for the door's BSP slab + hit (the leading hypothesis: DoStepDown finds the same flat floor + on the other side of the door, declaring step-up success). + + DO NOT re-investigate the multi-part registration or GetNearbyObjects + dedup — both are confirmed working. Focus on the step-up path 5 + behavior for thin tall obstacles. +``` diff --git a/src/AcDream.Core/Physics/TransitionTypes.cs b/src/AcDream.Core/Physics/TransitionTypes.cs index 297edbf..b67ad11 100644 --- a/src/AcDream.Core/Physics/TransitionTypes.cs +++ b/src/AcDream.Core/Physics/TransitionTypes.cs @@ -2254,6 +2254,21 @@ public sealed class Transition // ── BSP object: use the full 6-path retail dispatcher ──── // ACE: PhysicsObj.FindObjCollisions → Setup.BSP.find_collisions var physics = engine.DataCache.GetGfxObj(obj.GfxObjId); + + // A6.P4 door investigation (2026-05-24): log every BSP shadow + // visit before the cache lookup so we distinguish "cache miss + // → silently skipped" from "queried but no hit" (only the + // latter shows up in [resolve-bldg], which fires only on + // attributed hits). Mirrors [cyl-test] on the Cylinder branch. + if (PhysicsDiagnostics.ProbeBuildingEnabled) + { + Vector3 dxy = obj.Position - sp.GlobalCurrCenter[0].Origin; + float distXY = MathF.Sqrt(dxy.X * dxy.X + dxy.Y * dxy.Y); + bool cacheHit = physics?.BSP?.Root is not null; + Console.WriteLine(System.FormattableString.Invariant( + $"[bsp-test] obj=0x{obj.EntityId:X8} gfx=0x{obj.GfxObjId:X8} state=0x{obj.State:X8} radius={obj.Radius:F3} pos=({obj.Position.X:F2},{obj.Position.Y:F2},{obj.Position.Z:F2}) distXY={distXY:F3} cacheHit={cacheHit}")); + } + if (physics?.BSP?.Root is null) continue; // Transform player spheres to object-local space. diff --git a/tests/AcDream.Core.Tests/Physics/DoorCollisionApparatusTests.cs b/tests/AcDream.Core.Tests/Physics/DoorCollisionApparatusTests.cs index 183d0a8..55de362 100644 --- a/tests/AcDream.Core.Tests/Physics/DoorCollisionApparatusTests.cs +++ b/tests/AcDream.Core.Tests/Physics/DoorCollisionApparatusTests.cs @@ -199,6 +199,104 @@ public class DoorCollisionApparatusTests } } + /// + /// Reproduces the LIVE bug: a grounded player (isOnGround=true with + /// seeded ContactPlane) walking off-center toward a closed door + /// passes through. The path-difference test: this hits Path 5 + /// (Contact branch + StepSphereUp), while the other apparatus + /// tests above hit Path 6 (Default). If Path 5 incorrectly + /// declares step-up success the BSP collision returns OK and the + /// sphere walks through — exactly what the user reports in the + /// live Holtburg session 2026-05-24. + /// + [Fact] + public void Apparatus_Grounded_50cmOffCenter_FrontApproach_DocumentsBug() + { + if (!TryBuildScenario(out var ctx)) return; + + PhysicsDiagnostics.ProbeResolveEnabled = true; + PhysicsDiagnostics.ProbeBuildingEnabled = true; + Env.SetEnvironmentVariable("ACDREAM_DUMP_STEPUP", "1"); + + // Synthetic floor plane at Z = 0 so the grounded sphere has a + // walkable plane to rest on. Sphere foot center starts at Z=radius + // = 0.48, head at 0.48 + 1.20 = 1.68. + var floorPlane = new Plane(0f, 0f, 1f, 0f); // z = 0 plane + var floorVerts = new[] + { + new Vector3( 0f, 0f, 0f), + new Vector3(24f, 0f, 0f), + new Vector3(24f, 24f, 0f), + new Vector3( 0f, 24f, 0f), + }; + + var body = new PhysicsBody + { + Position = new Vector3(12.5f, 11f, 0.48f), + Orientation = Quaternion.Identity, + ContactPlaneValid = true, + ContactPlane = floorPlane, + ContactPlaneCellId = TestCellId, + WalkablePolygonValid = true, + WalkablePlane = floorPlane, + WalkableVertices = floorVerts, + WalkableUp = Vector3.UnitZ, + TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable, + }; + + var perTick = new Vector3(0f, 0.10f, 0f); + Vector3 pos = body.Position; + uint cellId = TestCellId; + bool isOnGround = true; + bool blocked = false; + Vector3 lastNormal = Vector3.Zero; + int ticks = 0; + + for (int tick = 0; tick < 30; tick++) + { + Vector3 target = pos + perTick; + var result = ctx.engine.ResolveWithTransition( + pos, target, cellId, + SphereRadius, SphereHeight, + StepUpHeight, StepDownHeight, + isOnGround, + body: body, + moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide, + movingEntityId: 0); + + ticks = tick; + body.Position = result.Position; + pos = result.Position; + cellId = result.CellId; + isOnGround = result.IsOnGround; + lastNormal = result.CollisionNormal; + + if (result.CollisionNormalValid) + { + blocked = true; + _out.WriteLine($"Tick {tick}: BLOCKED at pos=({pos.X:F3},{pos.Y:F3},{pos.Z:F3}) normal=({lastNormal.X:F3},{lastNormal.Y:F3},{lastNormal.Z:F3})"); + break; + } + } + + _out.WriteLine($"Final pos = ({pos.X:F3}, {pos.Y:F3}, {pos.Z:F3}) after {ticks + 1} ticks; blocked={blocked}"); + _out.WriteLine($"Grounded={isOnGround}"); + + // EXPECTED FAILURE (documents-the-bug): the grounded sphere walks + // straight through, reaching the far side at Y > 12.30. When the + // fix lands, flip this to Assert.True(blocked) — same shape as + // the Path-6 apparatus tests above. + PhysicsDiagnostics.ProbeResolveEnabled = false; + PhysicsDiagnostics.ProbeBuildingEnabled = false; + Env.SetEnvironmentVariable("ACDREAM_DUMP_STEPUP", null); + + Assert.True(pos.Y > 12.30f, + $"This test documents the production bug. If this is failing " + + $"because the sphere now blocks, the door fix worked — flip " + + $"the assertion to Assert.True(blocked) and Assert.True(pos.Y < 12.0f). " + + $"Current pos=({pos.X:F3},{pos.Y:F3},{pos.Z:F3}) blocked={blocked}"); + } + // ─────────────────────────────────────────────────────────────── // Apparatus setup // ───────────────────────────────────────────────────────────────