From 85a164f4a8d39086b13467f5a305ee61993b0aed Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 25 May 2026 08:14:49 +0200 Subject: [PATCH] fix(test): correct geometric pin test for door slab Z math MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Geometric_DoorSlabZRange_AbovePlayerSphereTop test was computing slabWorldZBottom as (entity.Z + partFrame.Z) — assuming the slab's local Z=0 was its bottom. Actually checking the dat shows the slab's PhysicsPolygons local AABB is min=(-0.954, -0.134, -1.236) max=(0.971, 0.127, 1.255) — the slab's local origin is at its GEOMETRIC CENTER, not the bottom. With partFrame.Z=1.275 lifting the origin, the slab world Z is actually [94.139, 96.630], not [95.375, 97.865]. Corrected test now computes both slabLocalZMin and slabLocalZMax from the polygon vertices and asserts the opposite (correct) geometric fact: the slab IS at sphere height — overlap from Z=94.139 to Z=95.20 (1.061 m of vertical overlap with the player's sphere). The slab is NOT a lintel that misses the sphere; it should collide. Test renamed: Geometric_DoorSlabZRange_AbovePlayerSphereTop → Geometric_DoorSlabAtSphereHeight_OverlapsInZ. Handoff doc 2026-05-25-door-bug-partial-fix-shipped.md updated with the corrected analysis. The "next investigation candidates" list now points toward cdb attach to retail as the highest-ROI option, since the BSP collision IS active at sphere height but production still shows asymmetric walkthrough behavior. The bug is in either the GetNearbyObjects coverage at primary-cell boundaries, the BSP polygon partial-overlap handling, or missing cell-BSP collision for cottage doorway walls. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...2026-05-25-door-bug-partial-fix-shipped.md | 136 +++++++++--------- .../Physics/DoorBugTrajectoryReplayTests.cs | 118 ++++++++------- 2 files changed, 125 insertions(+), 129 deletions(-) diff --git a/docs/research/2026-05-25-door-bug-partial-fix-shipped.md b/docs/research/2026-05-25-door-bug-partial-fix-shipped.md index d775a0d..ed0cfdc 100644 --- a/docs/research/2026-05-25-door-bug-partial-fix-shipped.md +++ b/docs/research/2026-05-25-door-bug-partial-fix-shipped.md @@ -106,90 +106,90 @@ The `[bsp-test]` probe fires 245 times for the door entity during the post-fix inside-out attempts — door IS being queried. The collision-detection mechanics produce the wrong response. -## What's next (separate bug — REFRAMED 2026-05-25 evening) +## What's next (separate bug) -**Initial hypothesis was wrong.** Two new directional tests built -post-fix (`Directional_OutsideIn_*`, `Directional_InsideOut_*`) BOTH -PASS — the BSP collision response is symmetric at unit-test level. -The asymmetric production bug must come from something the unit tests -weren't capturing. +**Investigation status (corrected 2026-05-25 late evening).** Two new +directional tests + a geometric pin test all PASS: -A geometric pin test (`Geometric_DoorSlabZRange_AbovePlayerSphereTop`) -reveals the real story: +- `Directional_OutsideIn_SouthApproach_BlocksAtSlabSouthFace` PASSES. +- `Directional_InsideOut_NorthApproach_BlocksAtSlabNorthFace` PASSES. +- `Geometric_DoorSlabAtSphereHeight_OverlapsInZ` PASSES. + +The geometric test reveals (correctly computed this time): ``` -Setup 0x020019FF (cottage door): - CylSphere[0]: r=0.10, h=0.20, origin=(0, 0, 0.018) - → world Z [94.118, 94.318] when entity at Z=94.1 - Part 0 (GfxObj 0x010044B5, the BSP "slab"): - placement frame [Default][0].Origin = (-0.006, 0.125, 1.275) - → BSP world Z [95.375, 97.865] when entity at Z=94.1 +Setup 0x020019FF (cottage door) PhysicsPolygons local AABB: + min=(-0.954, -0.134, -1.236) max=(0.971, 0.127, 1.255) + (slab origin at GEOMETRIC CENTER, not the bottom) -Player at floor Z=94: - sphere height = 1.20, sphere top = 95.20 +partFrame[0].Origin = (-0.006, 0.125, 1.275) → lifts slab origin + 1.275 m above entity Z - BSP slab BOTTOM (95.375) is ABOVE sphere TOP (95.20) by 0.175 m. - The slab NEVER collides with the player's body sphere. +With entity at world (132.6, 17.1, 94.1) + 180° entity rotation: + partWorldPos = (132.606, 16.975, 95.375) + +Slab WORLD AABB: + X: [131.635, 133.560] (1.925 m wide) + Y: [16.848, 17.109] (0.261 m thick) + Z: [94.139, 96.630] (2.491 m tall, bottom JUST above floor) + +Player sphere at foot Z=94: + Z: [94, 95.20] + +Slab DOES overlap sphere in Z (overlap Z=[94.139, 95.20] = 1.061 m). ``` -The slab is a LINTEL (the door frame above the doorway), not a leaf. -The door's only collision against a player at floor level is the -0.10 m radius foot cylinder. Sphere radius 0.48 + cyl 0.10 = 0.58 m -collision reach. Any sphere center > 0.58 m from cylinder center -(132.6, 17.1) passes freely. +**The slab IS at sphere height — it should collide.** Both directional +tests prove BSP collision response is symmetric for sphere-to-slab +approach. Yet production shows asymmetric inside-out walkthrough at +off-center positions. The bug must be in one of: -The user-reported "inside-out walkthrough at ~50 cm off-center" is -the sphere walking AROUND the cylinder, at X = 132.6 ± 0.6+ m where -collision misses entirely. "Body partially intersects door" is the -character model occupying the visual door's volume while the collision -sphere passes beside the foot cylinder. +1. **The portal-reachable cells from indoor cell 0x0150 still miss the + door's shadow at certain sphere positions**, despite the + AddAllOutsideCells fix. The user's walkthrough at X=133.655 (1.05 m + east of door center) puts the sphere mostly east of slab X range + [131.635, 133.560]. The sphere's WEST edge (X=133.175) is barely + inside the slab. If GetNearbyObjects's outdoor radial sweep uses + sphere center XY for cell lookup, it computes + gridX = (int)(133.655 / 24) = 5 → cell 0xA9B40029. But AddAllOutsideCells + only adds cells based on the sphere's PRIMARY position. The east-cell + neighbor might not be added if the sphere is wholly within the primary + cell's grid XY. Worth verifying. -The "outside-in works" case is also the foot cylinder doing the -blocking — when the user approaches more centered or hits the cylinder -head-on. The cell-visibility fix made the cylinder visible from indoor -cells (it wasn't before), which is why outside-in went from "walks -through" to "blocks" — but the cylinder is the actual collider. +2. **The BSP polygon-level test for partial-overlap geometry.** Sphere + half-east-of-slab, sphere south edge at slab north edge, moving +Y: + sphere is on the verge of leaving the slab volume. BSPQuery's polygon + intersection might consider this a "leaving collision" with no + response, even though the sphere body still partially occupies the + slab volume. Retail might handle this as "depenetration push" to + resolve the overlap. -**This is a door-geometry interpretation question, NOT a BSP query bug.** -Three candidate next-step investigations: +3. **Cell BSP (cell 0x0150's PhysicsPolygons) is missing**. The doorway + alcove cell has 4 physics polygons — likely walls + floor. If retail + relies on the cell's walls to catch sphere-vs-doorway-side-wall + collisions (in addition to the door slab), and we're not loading / + testing the cell BSP correctly for the player's foot at sphere + height, the side walls would miss. -1. **Retail-faithfulness audit on door collision.** Read retail's - `CPhysicsObj::set_setup_for_door` (or similar) to determine what - shapes retail loads for a closed cottage door. If retail uses the - SAME setup data + finds the same shapes we do, the door geometry - in the dat IS the spec. The "blocking" the user sees in retail - might similarly be the foot cylinder + perhaps a different default - sphere from setup.Radius/setup.Height that we're not registering. +Three candidate investigations, ranked by ROI: -2. **Inspect door parts 1+2 (GfxObj 0x010044B6).** Per prior session's - handoff they are visual-only (HasPhysics=false). Verify by direct - dat read — maybe the PhysicsBSP is null but the cylinder/sphere - list on the GfxObj itself has collision data we're missing. - `DoorSetupGfxObjInspectionTests` already prints this; re-read. +**A. cdb attach to retail** at a Holtburg cottage doorway. Break on +`CTransition::FindObjCollisions` for the door entity. Inspect what +shapes retail actually tests against. THIS IS DEFINITIVE — answers +"what should we be doing differently" in 15-30 min. CLAUDE.md has the +toolchain ready. -3. **The cottage cell BSP encloses the doorway.** Cell 0x0150 - (the doorway alcove) is bounded by cottage walls. The DOOR opens - through a gap in those walls. When the door is closed, the gap - is filled by the door geometry. Maybe retail's collision relies - on the cottage walls (cell BSP) for the "doorway side walls" and - the door's slab covers ONLY the opening's leaf area. The asymmetric - block we see could be the cottage cell walls catching outside-in - approach (cell BSP collision via FindEnvCollisions, before the - door's shadow ever fires) but NOT catching inside-out at the same - alcove geometry. +**B. Reproduce inside-out walkthrough at unit-test speed.** Load real +cell 0x0150 BSP into the harness (via CacheCellStruct from dat) + +register door at faithful transform + replay captured tick 3262. +If walkthrough reproduces at unit speed, can iterate on the fix in +<500 ms. -The 50cm off-center approach probably exits the cottage walls' BSP -through the doorway gap, walks past the foot cylinder (which is small), -and never has anything to collide against in the world's collision -representation. Retail might block this via a `setup.Radius=0.1414` -cylinder we're missing (Setup.Radius isn't currently registered by -`ShadowShapeBuilder.FromSetup` for entities WITH a CylSphere). +**C. Audit GetNearbyObjects radial sweep + AddAllOutsideCells coverage** +for east-neighbor cell when sphere XY is at primary cell boundary. -Apparatus to write next: a test that registers the door using -ShadowShapeBuilder.FromSetup AND verifies setup.Radius is reflected -in the shape list. If it isn't, that's a candidate fix — the door's -0.14 m radius + 0.20 m height SETUP sphere is wider than the 0.10 m -CylSphere and would catch the off-center approach. +Recommendation: **A first** (cdb), then **B** to validate the fix at +unit-test speed. ## Commits diff --git a/tests/AcDream.Core.Tests/Physics/DoorBugTrajectoryReplayTests.cs b/tests/AcDream.Core.Tests/Physics/DoorBugTrajectoryReplayTests.cs index dc81a21..f1711d5 100644 --- a/tests/AcDream.Core.Tests/Physics/DoorBugTrajectoryReplayTests.cs +++ b/tests/AcDream.Core.Tests/Physics/DoorBugTrajectoryReplayTests.cs @@ -300,9 +300,9 @@ public class DoorBugTrajectoryReplayTests /// /// /// - /// Geometric finding (2026-05-25 evening) — pins the door geometry - /// math that explains why the "inside-out walkthrough" persists - /// after the cell-visibility fix. + /// Geometric pin (2026-05-25 evening, CORRECTED) — pins where the + /// cottage door's BSP slab actually lives in world space relative + /// to the player's sphere. /// /// /// The cottage door Setup 0x020019FF has: @@ -319,58 +319,55 @@ public class DoorBugTrajectoryReplayTests /// /// /// - /// With entity at (132.6, 17.1, 94.1) (the captured Holtburg cottage - /// door spawn position): + /// AABB measured: min=(-0.954,-0.134,-1.236) max=(0.971,0.127,1.255). + /// The slab's local origin is at the slab's GEOMETRIC CENTER (each + /// axis is roughly symmetric around 0). With partFrame.Z = +1.275 + /// lifting the local origin up from the entity, the slab's world + /// extents are: + /// + /// /// - /// Cylinder world Z range: [94.118, 94.318] — touches the - /// ground (sphere foot Z=94 to top Z=95.20 overlaps cyl Z up to 94.318). - /// Slab world Z range: [95.375, 97.865]. The slab's BOTTOM - /// (95.375 m) is 0.175 m ABOVE the player's sphere top - /// (95.20 m). The slab NEVER intersects the player's - /// body sphere vertically. + /// X: [131.635, 133.560] (1.925 m wide; after 180° entity Z rot) + /// Y: [16.848, 17.109] (0.261 m thick) + /// Z: [94.139, 96.630] (2.491 m tall, bottom JUST above floor) /// + /// + /// + /// Player sphere (radius 0.48, height 1.20) at floor Z=94 extends + /// Z=[94, 95.20]. Slab bottom (94.139) is BELOW sphere top (95.20) + /// by 1.061 m. The slab DOES overlap the sphere in Z over + /// world Z range [94.139, 95.20]. The slab is at sphere height, + /// not above it. /// /// /// - /// Implication: the door's only effective collision against a player - /// at floor level is the 0.10 m radius foot cylinder. The 1.93 m wide - /// slab is collision-irrelevant at sphere height — it's a LINTEL - /// (the door frame above), not a leaf collision. The user-reported - /// "off-center walkthrough" is the sphere walking AROUND the - /// 0.10 m foot cylinder (sphere reach 0.48 + cyl 0.10 = 0.58 m; - /// any sphere center >0.58 m from cylinder center passes freely). - /// The "body partially intersects door" is the rendered character model - /// occupying volume the door visual fills, but no collision body to - /// stop it because the slab is too high. + /// The foot cylinder (r=0.10, h=0.20) sits at world Z [94.118, 94.318] + /// — barely above the floor, well within the sphere's foot region. /// /// /// - /// Next session: this is a DAT or registration issue, not a BSP query - /// bug. Options: - /// - /// Verify retail actually USES this geometry — maybe the door - /// relies on the cottage CELL'S walls (cell 0x0150's - /// PhysicsPolygons) to enclose the doorway, and the door's - /// only collision is the foot cylinder + a leaf shape we're - /// missing. - /// Inspect parts 1+2 (the door LEAVES, GfxObj 0x010044B6) to - /// confirm they're truly visual-only or if we missed a - /// physics shape. - /// cdb attach to retail at a Holtburg cottage door — set a - /// breakpoint on CTransition::FindObjCollisions for the door - /// entity and inspect what shapes retail tests against. - /// + /// Both shapes are at collision-able height. So the post-fix + /// inside-out walkthrough at off-center positions is NOT explained + /// by the slab being above the sphere. The bug must be in the BSP + /// polygon-level collision response, OR in how the multi-cell + /// portal-reachable cells produce the shapes list for a player on + /// the indoor side of the doorway. Next session investigation: + /// add a focused test that replays the captured inside-out + /// walkthrough tick with the door registered at its FAITHFUL + /// production transform (180° entity rot + dat-loaded partFrame) + /// and shows what BSPQuery.FindCollisions actually does at that + /// tick. /// /// [Fact] - public void Geometric_DoorSlabZRange_AbovePlayerSphereTop() + public void Geometric_DoorSlabAtSphereHeight_OverlapsInZ() { var datDir = ResolveDatDir(); if (datDir is null) return; - // Load the door setup + part 0's PlacementFrame. DatReaderWriter.Types.Frame partFrame; - float slabLocalZMax; + float slabLocalZMin = float.MaxValue; + float slabLocalZMax = float.MinValue; using (var dats = new DatCollection(datDir, DatAccessType.Read)) { var setup = dats.Get(0x020019FFu)!; @@ -380,38 +377,37 @@ public class DoorBugTrajectoryReplayTests var gfx = dats.Get(DoorGfxObjId)!; Assert.NotNull(gfx.PhysicsPolygons); - // Compute local AABB Z max from vertex array. - slabLocalZMax = float.MinValue; + // Walk every physics polygon vertex to find local Z extents. foreach (var poly in gfx.PhysicsPolygons.Values) { foreach (ushort vid in poly.VertexIds) { - if (gfx.VertexArray.Vertices.TryGetValue(vid, out var sv)) - if (sv.Origin.Z > slabLocalZMax) slabLocalZMax = sv.Origin.Z; + if (!gfx.VertexArray.Vertices.TryGetValue(vid, out var sv)) continue; + if (sv.Origin.Z < slabLocalZMin) slabLocalZMin = sv.Origin.Z; + if (sv.Origin.Z > slabLocalZMax) slabLocalZMax = sv.Origin.Z; } } } - // The door's entity is placed at world Z=94.1 in Holtburg (per - // captured spawn log). Slab local Z=0 origin offsets to: - float slabWorldZBottom = DoorSpawnPos.Z + partFrame.Origin.Z; - float slabWorldZTop = slabWorldZBottom + slabLocalZMax; + // Slab local origin shifted up by partFrame.Z. Slab world Z extents: + float partWorldZ = DoorSpawnPos.Z + partFrame.Origin.Z; + float slabWorldZBottom = partWorldZ + slabLocalZMin; + float slabWorldZTop = partWorldZ + slabLocalZMax; - // The player has sphereHeight=1.20, sphereRadius=0.48. Sphere top - // in world = foot.Z + height - radius + radius = foot.Z + height - // (the top of the head sphere centered at foot.Z + height - radius). - const float SphereHeight = 1.20f; - const float PlayerFootZ = 94f; // standard Holtburg floor - float sphereTopZ = PlayerFootZ + SphereHeight; + const float SphereHeight = 1.20f; + const float PlayerFootZ = 94f; + float sphereTopZ = PlayerFootZ + SphereHeight; - // The crucial assertion: slab bottom is above sphere top. - Assert.True(slabWorldZBottom > sphereTopZ, - $"Door slab bottom ({slabWorldZBottom:F3}) should be ABOVE " + - $"player sphere top ({sphereTopZ:F3}). Gap = " + - $"{slabWorldZBottom - sphereTopZ:F3} m. This pins the geometric " + - $"fact that the slab does not collide with the player at floor " + - $"level — only the foot cylinder does. The inside-out 'walkthrough' " + - $"is the sphere passing around the cylinder, not through the slab."); + // The slab IS at sphere height — bottom should be below sphere top. + Assert.True(slabWorldZBottom < sphereTopZ, + $"Door slab bottom ({slabWorldZBottom:F3}) should be BELOW " + + $"player sphere top ({sphereTopZ:F3}). Slab Z range = " + + $"[{slabWorldZBottom:F3}, {slabWorldZTop:F3}]. Player sphere Z = " + + $"[{PlayerFootZ:F3}, {sphereTopZ:F3}]. The slab IS at " + + $"sphere height (overlap from {MathF.Max(slabWorldZBottom, PlayerFootZ):F3} " + + $"to {MathF.Min(slabWorldZTop, sphereTopZ):F3}). So the inside-out " + + $"walkthrough is NOT caused by the slab being above the sphere — " + + $"the bug must be in BSP polygon-level collision response."); } [Fact]