Read-only deterministic test that opens the real client dat and
dumps Setup 0x020019FF + every GfxObj id it references. Bypasses
PhysicsDataCache's four early-return filters so we see WHAT is in
the dat, not what got into the cache. Skips gracefully when the
dat directory isn't present (keeps CI green).
Result reframes the prior session's investigation:
GfxObj 0x010044B5 (part 0 of the door) DOES have a full door-slab
PhysicsBSP — 6 two-sided (SidesType=Landblock) polygons forming a
1.925m × 0.261m × 2.490m collision volume at frame[0] offset
(-0.006, 0.125, 1.275). Bounding sphere radius 1.975. HasPhysics
flag set. So the handoff's Hypothesis A ("0x010044B5 has no
collision-bearing polygons, only visual") is FALSE.
GfxObj 0x010044B6 (parts 1 + 2, the swinging leaves) IS visual-only
by retail design — HasPhysics clear, PhysicsBSP null, 0 PhysicsPolygons,
but 87 visual Polygons. Our ShadowShapeBuilder skipping these matches
retail's CPhysicsPart::find_obj_collisions short-circuit on
physics_bsp==0 (acclient_2013_pseudo_c.txt:275051) — not a bug.
So the door collision bug is in INTEGRATION, not data. The Task 7
experiment last session registered 0x010044B5's BSP but got zero
[resolve-bldg] attributions. With the data confirmed good, the
next apparatus is a deterministic harness that hydrates 0x010044B5
from a dat dump, registers it via RegisterMultiPart, and sweeps a
player sphere into the door to confirm whether BSP collision fires
in isolation.
Pickup prompt + full reading in
docs/research/2026-05-24-door-dat-inspection-findings.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
11 KiB
Door collision dat inspection — findings
Date: 2026-05-24 (evening, continuation of door collision investigation)
Branch: claude/strange-albattani-3fc83c
Status: Evidence gathered. Hypothesis A from
2026-05-24-door-collision-session-handoff.md FALSIFIED.
TL;DR
A deterministic, read-only dat-inspection test
(DoorSetupGfxObjInspectionTests.cs)
opens the real client dat and prints the raw state of door Setup
0x020019FF + its three referenced GfxObjs.
Result — Hypothesis A is wrong. Part 0 (0x010044B5) has a complete
1.925 m × 0.261 m × 2.490 m door-sized collision volume in the dat. Six
two-sided (SidesType=Landblock) physics polygons form the closed door
slab. Bounding sphere radius 1.975 m. Setup Flags=HasPhysicsBSP.
Parts 1, 2 (0x010044B6) are visual-only by design — HasPhysics
flag is clear, PhysicsBSP is null, PhysicsPolygons.Count = 0. This
matches retail's CPhysicsPart::find_obj_collisions
(acclient_2013_pseudo_c.txt:275051),
which explicitly short-circuits when physics_bsp == 0. So retail also
runs no collision against 0x010044B6 — and our skip-on-null-BSP
behavior is retail-faithful, not a bug.
This rewrites the "next-session approach" recommendation in the prior
handoff. The handoff said "if 0x010044B5's BSP has zero floor-touching
polys → Hypothesis A confirmed → pivot strategy." The BSP has six
collision polygons forming the whole door slab. The pivot is not needed;
we need to figure out why our integration of 0x010044B5's BSP didn't
fire during the Task 7 experiment.
Raw dump (verbatim from the test)
=== Setup 0x020019FF ===
Flags = HasParent, AllowFreeHeading, HasPhysicsBSP (0x0000000D)
Radius = 0.1414
Height = 0.2000
StepUp = 0.0900
StepDown = 0.0900
CylSpheres = 0
Spheres = 1
[0] r=0.1000 origin=(0.000,0.000,0.018)
Parts = 3
[0] gfxObj=0x010044B5
[1] gfxObj=0x010044B6
[2] gfxObj=0x010044B6
PlacementFrames = 1
[Default] frameCount=3
frame[0] pos=(-0.006,0.125,1.275) rot=(0.000,0.000,0.000,1.000)
frame[1] pos=(0.710,0.000,1.210) rot=(0.000,0.000,0.000,1.000)
frame[2] pos=(0.710,0.247,1.210) rot=(0.000,0.000,1.000,0.000)
=== GfxObj 0x010044B5 === (the door slab — has physics)
Flags = HasPhysics, HasDrawing, HasDIDDegrade (0x0000000B)
HasPhysics = True
VertexArray = non-null, 8 vertices
PhysicsPolygons = 6 polys
PhysicsBSP = non-null
PhysicsBSP.Root = non-null
Root.Type = BPnN
Root.BoundingSphere = (-0.390,-0.056,-0.150) r=1.975
BSP tree total polys (including children) = 6
PhysicsPolygon AABB sweep (first 6 polys):
[0x0000] nVerts=4 sides=Landblock min=(-0.954,-0.134,-1.236) max=(0.971,0.127,-1.236) # bottom face
[0x0001] nVerts=4 sides=Landblock min=(-0.954,-0.134,-1.236) max=(-0.954,0.127,1.255) # left face
[0x0002] nVerts=4 sides=Landblock min=(-0.954,-0.134, 1.255) max=(0.971,0.127,1.255) # top face
[0x0003] nVerts=4 sides=Landblock min=( 0.971,-0.134,-1.236) max=(0.971,0.127,1.255) # right face
[0x0004] nVerts=4 sides=Landblock min=(-0.954,-0.134,-1.236) max=(0.971,-0.134,1.255) # front face
[0x0005] nVerts=4 sides=Landblock min=(-0.954, 0.127,-1.236) max=(0.971,0.127,1.255) # back face
PhysicsPolygons combined AABB: min=(-0.954,-0.134,-1.236) max=(0.971,0.127,1.255)
size=(1.925, 0.261, 2.490)
=== GfxObj 0x010044B6 === (the leaves — visual-only by design)
Flags = HasDrawing, HasDIDDegrade (0x0000000A)
HasPhysics = False
VertexArray = non-null, 40 vertices
PhysicsPolygons = 0 polys
PhysicsBSP = NULL
Polygons (visual) = 87 polys
DrawingBSP = non-null
What this means
The data is right
Part 0's BSP is a six-faced thin slab oriented as a vertical door:
1.925 m wide × 0.261 m thin × 2.490 m tall. Placed at frame[0] offset
(-0.006, 0.125, 1.275), it occupies entity-local Z ∈ [0.039, 2.530] —
a standard door height. All six faces are
SidesType=Landblock (two-sided collision — catches a sphere
approaching from either side).
This is exactly what retail's collision system uses to block doors. No mystery, no missing data, no need to fall back to a wider Cylinder approximation.
The leaves are correctly visual-only
0x010044B6 is the swinging door leaf (used twice — left + right
panels). It has no physics by retail design. Our ShadowShapeBuilder
skipping these parts matches both the dat and retail's
CPhysicsPart::find_obj_collisions.
So the bug is in integration, not data
The previous session's Task 7 experiment registered 0x010044B5's BSP
correctly (we saw type=BSP gfxObj=0x010044B5 radius=2.000 localPos=(-0.006,0.125,1.275) in the per-shape registration), yet got
zero [resolve-bldg] attributions during live play. With the data
now confirmed good, that gap must be in:
-
The BSP collision dispatch never enters for the door entry —
TransitionTypes.cs:2257silentlycontinues whenengine.DataCache.GetGfxObj(obj.GfxObjId)?.BSP?.Root is null. If the GfxObj wasn't cached at collision time (race with renderer load), the entry is invisibly skipped. No log distinguishes this from "queried-and-no-hit." -
Broadphase placeholder radius — Task 2's
ShadowShapeBuilderusesbspRadius = 2fas a stand-in pending a Task 5/6 caller replacement. The real dat value is1.975— close enough not to be the blocker, but the placeholder convention means callers MUST substitute the real BS radius fromcache.GetGfxObj(gfxId).BoundingSphere.Radiusbefore registering. If a future caller forgets, the broadphase will still mostly work but won't be tight. -
The broadphase center is the part's FRAME origin, not the BSP's bounding-sphere center. Frame origin =
(-0.006, 0.125, 1.275); BS center in part-local =(-0.390, -0.056, -0.150). Distance: 1.48 m. The 2.0 m broadphase radius nominally covers the BS sphere (radius 1.975) from the frame origin only on the side closest to the BS center. For approaches on the opposite side, the broadphase sphere extends 2.0 m + 1.48 m = 3.48 m from the BS center — wider than needed, but never too tight in the door case. Still, a more faithful encoding centers the broadphase on the part's BS center + frame offset, with radius = BS radius. -
BSPQuery against
SidesType=Landblockpolys —BSPQuery.cspass-through-copiesSidesType(line 2277) but doesn't filter on it. We have not yet verified thatLandblock-typed polys actually produce collision hits in our query pipeline against a thin-slab geometry. (Note: indoor cells useSidesType=Single-typed cell-floor polys and those work — the cellar replay tests pin that. But Doors'Landblockpolys may behave differently — particularly w.r.t. two-sided collision.) -
Entity rotation at the doorway — Holtburg cottage doors face non-cardinal directions. The entity's world rotation
entity_rotcomposes withframe[0].Rotation(identity for part 0) to produceobj.Rotation = entity_rot. The sphere transforminv(entity_rot) * (sphere_world − obj.Position)is sensitive to that rotation. If we register with identity (forgetting to plumb the spawn's rotation through), the BSP polys will be oriented "into the world" wrong — passing tests that approach from the wrong axis.
Recommended next step
The handoff's "DO NOT speculate-and-fix again" rule still applies. The right next move is apparatus-first, not another implementation attempt:
Write a focused unit test that:
- Loads the real
0x010044B5PhysicsBSP from the dat via the inspection test's pattern (or useGfxObjDumpSerializer.Hydratefor a deterministic fixture). - Constructs a synthetic door entity at a known world position
(132.6, 17.1, 94.08)with a known rotation (try identity AND a ~90° Z rotation to cover both axes). - Sweeps a player sphere at the door from each of the four compass directions, at off-center positions (50 cm off-center) AND dead center.
- Calls
Transition.FindObjCollisions/ResolveWithTransitiondirectly (apparatus path mirrors the live one). - Asserts:
- Dead-center approach →
Collided/Adjusted/SlidwithCollideObjectGuidscontaining the door entity. - 50 cm off-center approach → same.
- From inside walking out → same.
- Dead-center approach →
If the test fails: we have a deterministic reproduction of the live bug in <500 ms, and we can fix the integration with confidence. If the test passes: the door bug is elsewhere (cell registration, spawn-time race, etc.).
This is the next apparatus the previous session was building toward when it ran out of cycles. With the data question now closed by the dat inspection, it's the highest-information next move.
What's in the tree right now
$ git status --short
?? docs/research/2026-05-24-door-dat-inspection-findings.md
?? tests/AcDream.Core.Tests/Physics/DoorSetupGfxObjInspectionTests.cs
[+ untracked launch logs from prior sessions]
Build green; existing tests still pass; new test runs in 34 ms and produces the dump above.
Pickup prompt for next session
Door collision dat inspection (2026-05-24 evening) FALSIFIED
Hypothesis A. Part 0 (0x010044B5) has a full door-slab BSP in the
dat — 6 Landblock-typed polys forming a 1.925 m × 0.261 m × 2.490 m
collision volume. Parts 1, 2 (0x010044B6) are visual-only by retail
design (HasPhysics flag clear). Retail and acdream both skip those
in CPhysicsPart::find_obj_collisions — that's not a bug.
Read docs/research/2026-05-24-door-dat-inspection-findings.md
State both altitudes:
Currently working toward: M1.5 — Indoor world feels right
Current phase: A6.P4 — door collision investigation continues.
Per-part BSP infrastructure (Tasks 1-6) ships
already; data is confirmed good in the dat; need
to determine WHY our integration of 0x010044B5's
BSP didn't fire collisions during the Task 7
experiment.
Next moves (in order):
1. Write CellarUpTrajectoryReplay-style apparatus test that
loads 0x010044B5's PhysicsBSP from a dat dump, registers a
synthetic door via RegisterMultiPart, and sweeps a player
sphere at it. Confirm BSP collision fires (or doesn't) in
isolation.
2. If the test passes → bug is in live registration (likely
cell scoping, entity rotation, or race with renderer
loading). Investigate live cell membership for door
entities.
3. If the test fails → bug is in BSPQuery.FindCollisions
against thin-slab Landblock-typed polys. Investigate the
6-path dispatcher for that case.
DO NOT re-attempt Task 7 (per-part BSP wiring in
RegisterLiveEntityCollision) until the apparatus test confirms
the BSP works in isolation. Tasks 1-6 stay; they're correct.