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>
This commit is contained in:
parent
3d4e63f9c8
commit
b36eff1c10
1 changed files with 328 additions and 0 deletions
328
docs/research/2026-05-25-a6-door-cyl-investigation-handoff.md
Normal file
328
docs/research/2026-05-25-a6-door-cyl-investigation-handoff.md
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
# 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`:
|
||||
|
||||
```c
|
||||
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.FindObjCollisions` —
|
||||
`references/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:
|
||||
|
||||
```bash
|
||||
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
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue