acdream/docs/research/2026-05-25-a6-door-cyl-investigation-handoff.md
Erik b36eff1c10 docs(handoff): A6.P7 door-cyl + slab interaction — retail investigation needed
A6.P5 (cellSet fix, 3b1ae83) + A6.P6 (cyl step-over, 3d4e63f) shipped
and verified. Original phantom radial-push is gone. Residual symptom:
sphere blocked at NE/SE headings approaching closed cottage door
because cyl's radial normal drives slide direction into the slab.

Handoff covers:
  - What landed today (don't redo)
  - Concrete evidence from door-a6p6-v2.utf8.log (12 resolves with
    cn=(0.86,0.51,0) on door entity post-A6.P6)
  - 3 fix options (BSP-first per-entity / per-physobj dispatch port /
    door-cyl-informational)
  - 3 retail investigation questions for next session (state bit
    0x10000 semantics, cdb trace on door cyl in retail, Setup parsing
    comparison)
  - Files to read first + tests to keep green + do-not-retry list
  - Pickup prompt with brainstorming-only discipline

Next session's deliverable: a research report, NOT an implementation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 14:54:59 +02:00

16 KiB

A6.P6 / A6.P7 — Door cylinder + slab interaction handoff

Date: 2026-05-25 PM Status: A6.P5 cellSet fix shipped (3b1ae83). A6.P6 cyl step-over shipped (3d4e63f). Residual symptom remains: sphere can't slide tangentially past the door's foot cylinder when the cyl's radial collision normal dominates the slide direction. Three fix options identified; user picked "investigate retail first" — that's this session's work.


TL;DR

Walking into a closed cottage door from outside in acdream:

Before A6.P5 After A6.P5 only After A6.P6 too
Sphere walks through (cellSet didn't include door) Sphere blocks BUT cyl phantom radial-pushes sphere AWAY from target (~10 cm push-out at door center) Sphere stops at current position when cyl fires — no more push-out, but also can't slide tangentially past the cyl on some headings

A6.P5 made the door reliably visible from all approach angles (closed the cellSet bug); A6.P6 routed Contact-grounded cyl collisions through step-over instead of radial push. Both retail-anchored. But the residual "can't slide past cyl on certain headings" still happens because:

  1. The door has two collision shapes: a tiny foot cylinder (r=0.10, h=0.20) and the big slab BSP.
  2. Our FindObjCollisions tests shapes in registration order. The cyl gets tested FIRST. When cyl fires, FindObjCollisions returns immediately — slab BSP never tested in that iteration.
  3. The cyl's collision normal is radial (away from cyl axis). For a sphere wanting to move SE past a door at world (132.6, 17.1), the cyl-radial normal is roughly (0.86, 0.51, 0). The slide tangent from that normal points mostly south — INTO the slab. Slab then blocks (in a downstream iteration). Net: sphere doesn't move.
  4. If the slab's clean (0, +1, 0) normal were used instead, the slide tangent would be pure east. Sphere would slide cleanly along the door. This is what retail does visibly.

So the question is: how does retail end up with the slab's normal driving the slide, when retail also has the cyl AND tests it?


What today shipped (DO NOT redo this)

A6.P5 — cellSet portal expansion fix (commit 3b1ae83)

  • File: src/AcDream.Core/Physics/CellTransit.cs
  • Function: FindTransitCellsSphere exit-portal branch + BuildCellSetAndPickContaining
  • Change: exit portals contribute exitOutside = true by topology, not by sphere-plane overlap.
  • Retail anchor: CObjCell::find_cell_list at acclient_2013_pseudo_c.txt:308742-308869.
  • Tests: CellTransitTests.A6P5_BuildCellSetFromIndoorStart_ReachesDoorOutdoorCell + A6P5_BuildCellSetFromAlcove_AlsoReachesDoorOutdoorCell. Both pass.
  • Fixture: tests/AcDream.Core.Tests/Fixtures/door-bug/over-penetration-capture.jsonl (3 records from the 17 MB live capture).

A6.P6 — cyl step-over for Contact movers (commit 3d4e63f)

  • File: src/AcDream.Core/Physics/TransitionTypes.cs
  • Function: CylinderCollision — added Contact-grounded branch
  • Change: when oi.Contact && !sp.StepUp && !sp.StepDown && engine != null and cyl height fits step-up-height, attempt DoStepUp(collisionNormal, engine). On failure → StepUpSlide(this). On step-fail, behavior changes from radial push to tangent-along-crease.
  • Retail anchor: CCylSphere::intersects_sphere at acclient_2013_pseudo_c.txt:324626-324641 (Contact branch dispatches step_sphere_up) + CCylSphere::step_sphere_up at acclient_2013_pseudo_c.txt:324516-324538.
  • Tests: all A6P5_* + Path 5 tests + door directional tests pass in isolation. Full Core suite 17 failures (same as A6.P5 baseline) — diff is documented static-leak flakiness.

Probes added (still in place — useful for next session)

  • ACDREAM_PROBE_CELLSET=1[cellset-build] line per BuildCellSetAndPickContaining call.
  • ACDREAM_PROBE_BUILDING=1[cyl-test] + [bsp-test] (existing).
  • ACDREAM_PROBE_RESOLVE=1[resolve] (existing).
  • ACDREAM_CAPTURE_RESOLVE=<path> → JSONL capture for replay.

Captures from today (gitignored, on disk)

  • door-stuck-capture.jsonl (17 MB, 8483 records) — the original phantom reproduction.
  • door-phantom-capture.jsonl (13 MB, ~7000 records) — captured with cyl/bsp probes ON post-A6.P5.
  • door-a6p6-v2.launch.log (UTF-16) + door-a6p6-v2.utf8.log — most recent diagnostic launch with all 3 probes on after A6.P6 fix landed. Shows residual cyl phantom (12+ resolves with cn=(0.86, 0.51, 0) attributed to door entity 0x000F4245).

The remaining symptom (what to fix)

User walks into a closed cottage door (Setup 0x020019FF, entity at world ≈ (132.6, 17.1, 94.1)). When the sphere ends up at certain angles to the door (NE / SE of the cyl center), the cyl's slide "blocks" the sphere from making tangential progress along the slab face.

Specific evidence from door-a6p6-v2.utf8.log (line ~23553):

[resolve-bldg] obj=0x000F4245 ... hitPoly: plane=(0.000,0.000,-1.000,-1.236)  ← slab BOTTOM hit, but culled (no Z motion)
[cyl-test] obj=0x000F4245 ... result=Slid                                      ← cyl fired
[resolve] in=(132.777,17.724) tgt=(133.044,17.400) out=(132.777,17.724)
          hit=yes n=(0.86,0.51,0.00) obj=0x000F4245 nObj=9

The cn=(0.86, 0.51, 0) is the cyl's radial normal (sphere is NE of cyl axis). The slide direction is perpendicular = (0.51, -0.86, 0) ≈ mostly south = into the slab. Slab blocks in subsequent iteration. Net: out == in.

Counts from the latest launch (~7K resolves):

  • 117 hit=yes attributed to door entity 0x000F4245
  • 99 hit=yes attributed to cottage GfxObj 0xA9B47900
  • 350 cyl-tests result=Slid (out of 1623 total cyl tests)
  • 12 resolves with cn=(0.86, 0.51, 0) on the door — the "phantom slide direction" pattern

The three options (user picked #2-investigation first)

Option 1: BSP-first per-entity test order (smallest fix)

Within an entity's shapes, test BSP shapes before Cylinder shapes. If BSP fires, skip the cyl. The slab's clean (0, ±1, 0) normal drives the slide → sphere slides smoothly along door face.

  • ~10 lines in FindObjCollisions (sort nearbyObjs per-entity).
  • Retail-faithful behaviorally; whether it's retail-faithful architecturally is uncertain (see Option 2 research).

Option 2: Port retail's per-physobj dispatch (architectural)

Restructure ShadowObjectRegistry to group shapes by entity. Implement retail's CPhysicsObj::FindObjCollisions dispatch including the state & 0x10000 branch logic (acclient_2013_pseudo_c.txt:276861).

  • Large change; touches many files.
  • True retail-faithful architecture. But behaviorally may end up producing the same outcome as Option 1 if our state flag mapping is correct.

Option 3: Door-cyl-as-informational

Hypothesis: retail's door cyl is for click-target / sound trigger / foot-slip prevention for non-player entities, NOT a physics blocker for the standard player. Skip registering it as a collision shape on entities that also have a BSP.

  • Needs retail research to confirm.
  • Risk: breaks foot-slip prevention for small entities.

Retail investigation needed (THIS SESSION's main work)

The fundamental question: what does retail do with the door's cyl that produces clean sliding past it? Two specific things to read + test:

Investigation 1: What does state & 0x10000 mean?

Retail's CPhysicsObj::FindObjCollisions at acclient_2013_pseudo_c.txt:276861:

if (((this->state & 0x10000) == 0 || ebp_1 != 0) || eax_12 != 0) {
    // iterate cylspheres + spheres
} else {
    // iterate BSP parts via CPartArray::FindObjCollisions
}

Door state at spawn = 0x00010008. Bit 0x10000 (bit 16) IS set. So condition state & 0x10000 == 0 is FALSE. The branch depends on ebp_1 and eax_12.

Investigation steps:

  1. Grep acclient_2013_pseudo_c.txt for what assigns to ebp_1 and what eax_12 is computed from. Identify which mover/target state bits drive the branch.
  2. Search docs/research/named-retail/acclient.h for state flag bit definitions (look for constants 0x10000, OBJECT_USES_PHYSICS_BSP or similar around the OBJECTINFO / PhysicsObj state field).
  3. Determine which branch fires for: closed door (state 0x10008) + grounded player.
  4. If cyl branch fires for our case: how does retail block player from passing through the door without the BSP test?
  5. If BSP branch fires: why? What state condition is off in our replica?

Cross-reference with ACE's PhysicsObj.FindObjCollisionsreferences/ACE/Source/ACE.Server/Physics/PhysicsObj.cs. ACE might have cleaner names for the same logic.

Investigation 2: What does the door's cyl actually DO in retail?

Concrete experiment using cdb on the live retail client:

  1. Attach cdb to retail acclient.exe (toolchain in CLAUDE.md "Retail debugger toolchain" section).
  2. Set breakpoint on CCylSphere::collides_with_sphere (acclient address 0x53a880) with action: log entity id + sphere position + result. Use qd after ~5000 hits to detach.
  3. Walk retail player into a closed cottage door from outside, trying to slide along it.
  4. Capture trace. Look for:
    • Does the door cyl ever fire collides_with_sphere returning 1? If yes → cyl IS active in retail.
    • If no → cyl is somehow excluded from physics in retail (Option 3 plausible).
  5. Set breakpoint on BSPTREE::find_collisions for the same scenario. Determine if BSP slab is tested.

Investigation 3: Inspect Setup parsing differences

Compare what our ShadowShapeBuilder.FromSetup produces from Setup 0x020019FF vs what retail's PhysicsObj constructs from the same Setup:

  1. dotnet test --filter "FullyQualifiedName~DoorSetupGfxObjInspectionTests" --logger "console;verbosity=detailed" for our parse.
  2. Inspect retail's PhysicsObj creation flow (acclient.exe around the PhysicsObj constructor + part_array initialization). Look for filtering: does retail include the Setup's cyl in its physics shape list, or is there a flag-driven include/exclude?

Files to read FIRST next session

File / location What to find
docs/research/named-retail/acclient_2013_pseudo_c.txt:276776+ CPhysicsObj::FindObjCollisions (the dispatch + state flag branch)
docs/research/named-retail/acclient_2013_pseudo_c.txt:324558 CCylSphere::intersects_sphere (the per-cyl dispatch for state & 3)
docs/research/named-retail/acclient_2013_pseudo_c.txt:324516 CCylSphere::step_sphere_up (our A6.P6 anchor; verify our port matches)
docs/research/named-retail/acclient.h OBJECTINFO state bit constants (esp. 0x10000)
references/ACE/Source/ACE.Server/Physics/PhysicsObj.cs ACE's port — cleaner names
docs/research/named-retail/acclient_2013_pseudo_c.txt:308916 CObjCell::find_obj_collisions (per-cell shadow iteration, calls CPhysicsObj::FindObjCollisions)

Tests to keep green (do NOT regress)

Run these in isolation when verifying any new fix:

dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --no-build -c Debug --filter "FullyQualifiedName=AcDream.Core.Tests.Physics.CellarUpTrajectoryReplayTests.LiveCompare_FirstCap_FixClosesCottageFloorCap|FullyQualifiedName=AcDream.Core.Tests.Physics.DoorBugTrajectoryReplayTests.Directional_OutsideIn_SouthApproach_BlocksAtSlabSouthFace|FullyQualifiedName=AcDream.Core.Tests.Physics.DoorBugTrajectoryReplayTests.Directional_InsideOut_NorthApproach_BlocksAtSlabNorthFace|FullyQualifiedName=AcDream.Core.Tests.Physics.DoorBugTrajectoryReplayTests.CornerSlide_AlcoveEastToCottageNorth_ShouldBlock|FullyQualifiedName=AcDream.Core.Tests.Physics.DoorBugTrajectoryReplayTests.Geometric_DoorSlabAtSphereHeight_OverlapsInZ|FullyQualifiedName=AcDream.Core.Tests.Physics.DoorBugTrajectoryReplayTests.InsideOut_Tick3254_WithCottageWalls_ShouldBlock|FullyQualifiedName~BSPQueryTests.FindCollisions_Path5|FullyQualifiedName~CellTransitTests.A6P5|FullyQualifiedName~DoorCollisionApparatusTests.Apparatus_DeadCenter"

Expected: all 14 pass.

Full Core suite has 17 documented flaky-in-full-run failures — those are the static-leak flakiness CLAUDE.md describes, not regressions.


Things NOT to do (do-not-retry list)

  1. Don't reverse cyl/BSP iteration order globally. Cross-entity ordering should follow registration sequence (matches retail per-cell shadow_object_list). Only within-entity ordering needs adjustment.
  2. Don't disable the door cyl unconditionally. Foot-slipping matters for small entities even if not for the player.
  3. Don't enlarge EPSILON in slide-back-off math to "give more margin." The 11mm residual penetration is a separate issue (SlideSphere preserves currPos.Y which may already be slightly penetrating); changing epsilon would mask other bugs.
  4. Don't add per-call workarounds in CylinderCollision (like "if entity has a sibling BSP, return OK"). Per CLAUDE.md no-workarounds rule — fix the architectural issue, not the symptom.
  5. Don't break A6.P6 step-over for non-door cyls (tree trunks, rock pillars, NPCs). Whatever fix lands must keep cyl-only entities blocking correctly.

Open issue tracking

Add to docs/ISSUES.md after this handoff:

- door-cyl-residual-block: After A6.P5 + A6.P6, sphere can still be
  blocked at NE/SE headings approaching a closed cottage door because
  the cyl's radial collision normal drives the slide direction into
  the slab. Three fix options outlined in
  docs/research/2026-05-25-a6-door-cyl-investigation-handoff.md;
  pending retail investigation to pick the retail-faithful path.
  Severity: M1.5 polish (does not block "kill a drudge" demo).

Pickup prompt for next session

A6.P6 / A6.P7 — door-cyl residual block investigation.

Read first (in this order):
  1. docs/research/2026-05-25-a6-door-cyl-investigation-handoff.md
     (full context: what landed, what's still broken, the 3 fix options,
     do-not-retry list)
  2. docs/research/named-retail/acclient_2013_pseudo_c.txt:276776
     (CPhysicsObj::FindObjCollisions — the state-flag dispatch)
  3. docs/research/named-retail/acclient_2013_pseudo_c.txt:324558
     (CCylSphere::intersects_sphere — the cyl dispatch)

State both altitudes:
  Currently working toward: M1.5 — Indoor world feels right
  Current phase: A6.P7 — retail investigation for door cyl + slab
                 collision interaction.

The session's main work: retail investigation. NOT implementation.
Specific questions to answer (cite retail line numbers in the report):

  1. What does state bit 0x10000 mean? Closed cottage doors have it
     set (state = 0x00010008). Retail's FindObjCollisions branches on
     `((state & 0x10000) == 0 || ebp_1 != 0) || eax_12 != 0`. What are
     ebp_1 and eax_12? Which branch fires for a closed door + grounded
     player? (Cross-reference references/ACE/Source/ACE.Server/Physics/
     PhysicsObj.cs for cleaner names.)

  2. Does the door cyl actually fire collides_with_sphere in retail
     when player slides along the door? Set a cdb breakpoint on
     CCylSphere::collides_with_sphere (acclient address 0x53a880),
     walk a retail player into the cottage door, observe. If cyl
     fires: how does retail produce smooth sliding past it? If cyl
     doesn't fire: by what mechanism is it excluded?

  3. Compare our ShadowShapeBuilder.FromSetup output vs retail's
     PhysicsObj shape list for Setup 0x020019FF. Where do they
     diverge?

Deliverable: a short report (~2-3 pages) covering the 3 questions with
retail line numbers + cdb trace excerpts. Then propose which of the
3 fix options (BSP-first per-entity / per-physobj dispatch port /
door-cyl-informational) is the most retail-faithful, justified by
the research.

DO NOT implement the fix this session — the brainstorming-only
discipline applies. After the report, the next session will pick
the implementation approach + execute via writing-plans → executing-plans.

Do-not-retry list (in handoff doc) — read it before starting.

Tests to keep green if any code changes happen: see handoff doc.

Reproduction setup ready to relaunch with diagnostics if needed:
  ACDREAM_PROBE_BUILDING=1 ACDREAM_PROBE_RESOLVE=1 ACDREAM_PROBE_CELLSET=1
  ACDREAM_CAPTURE_RESOLVE=<path>.jsonl