acdream/docs/research/2026-05-24-door-dat-inspection-findings.md
Erik e1d94d7094 test(phys): door setup + GfxObj dat-inspection — Hypothesis A falsified
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>
2026-05-24 18:34:41 +02:00

11 KiB
Raw Blame History

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:

  1. The BSP collision dispatch never enters for the door entryTransitionTypes.cs:2257 silently continues when engine.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."

  2. Broadphase placeholder radius — Task 2's ShadowShapeBuilder uses bspRadius = 2f as a stand-in pending a Task 5/6 caller replacement. The real dat value is 1.975 — close enough not to be the blocker, but the placeholder convention means callers MUST substitute the real BS radius from cache.GetGfxObj(gfxId).BoundingSphere.Radius before registering. If a future caller forgets, the broadphase will still mostly work but won't be tight.

  3. 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.

  4. BSPQuery against SidesType=Landblock polysBSPQuery.cs pass-through-copies SidesType (line 2277) but doesn't filter on it. We have not yet verified that Landblock-typed polys actually produce collision hits in our query pipeline against a thin-slab geometry. (Note: indoor cells use SidesType=Single-typed cell-floor polys and those work — the cellar replay tests pin that. But Doors' Landblock polys may behave differently — particularly w.r.t. two-sided collision.)

  5. Entity rotation at the doorway — Holtburg cottage doors face non-cardinal directions. The entity's world rotation entity_rot composes with frame[0].Rotation (identity for part 0) to produce obj.Rotation = entity_rot. The sphere transform inv(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.


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:

  1. Loads the real 0x010044B5 PhysicsBSP from the dat via the inspection test's pattern (or use GfxObjDumpSerializer.Hydrate for a deterministic fixture).
  2. 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).
  3. 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.
  4. Calls Transition.FindObjCollisions / ResolveWithTransition directly (apparatus path mirrors the live one).
  5. Asserts:
    • Dead-center approach → Collided / Adjusted / Slid with CollideObjectGuids containing the door entity.
    • 50 cm off-center approach → same.
    • From inside walking out → same.

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.