docs: issue #101 — broken stairs cyl phantom (post-A6.P7 finding)

Visual verification of A6.P7 at Holtburg cottage door passed cleanly
(1187 [cyl-skip-bsp] guard fires, 0 [cyl-test] on doors, 30/30
axis-aligned hits, smooth NE/SE slide along door face). While
exploring post-verification, the user discovered a different
staircase in cells 0xA9B40159 + 0xA9B4015A where the sphere cannot
climb at all.

Captured working baseline (stairs-working.jsonl, cottage cellar
stairs in cells 0xA9B40143/146/147 — clean ↔ Z=90.95-94.00 traversal)
and broken scenario (stairs-broken.jsonl, Z stays at 94.00 the entire
4216-record capture).

Root cause is NOT a regression of A6.P7. It's a different bug shape:
the staircase is built as a multi-part EnvCell entity (entityId
0x0040B500, ~150 parts), with 10 of those parts being 0.80m-radius
cylinders forming the steps. Each cyl carries state=0x00000000 — no
HAS_PHYSICS_BSP_PS — so A6.P7's BspOnlyDispatch guard correctly
doesn't fire. Cyl height 0.80m exceeds A6.P6's step-up budget 0.60m
so grounded step-over fails. Falls through to wall-slide which
produces the same diagonal radial phantom A6.P7 closed for the door.

The [resolve-bldg] lines reveal gfxObj=0x0100081A hasPhys=False
bspR=0.00 vAabbR=0.82 — the underlying GfxObj has NO physics BSP;
we appear to be synthesizing a cyl from the visual AABB radius. That
synthesis path is the suspected misregistration.

Filed as issue #101 with severity HIGH. Investigation handoff written
covering 4 retail-research questions (cdb on retail at this stair
location, Setup trace via entity-source probe, ShadowShapeBuilder
vAabbR fallback audit, cell BSP poly dump), do-not-retry list, and 3
candidate fix shapes (don't synthesize cyl from vAabbR / cell BSP for
stairs / cyl-height-tolerant step-over). The handoff explicitly
defers implementation to a later session pending retail evidence.

Files:
- docs/research/2026-05-25-stairs-cyl-investigation-handoff.md (new)
- docs/ISSUES.md — added #101 entry

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-25 19:03:58 +02:00
parent 888272aad1
commit 8795655250
2 changed files with 398 additions and 0 deletions

View file

@ -761,6 +761,65 @@ family (sling-out — also likely).
---
## #101 — Stair-step cylinder phantom blocks player on multi-part EnvCell entity
**Status:** OPEN
**Severity:** HIGH (M1.5 — blocks stair climbing in at least one Holtburg
building; same symptom class as the just-closed A6.P7 door bug but
different root cause)
**Filed:** 2026-05-25
**Component:** physics, dat-handling
**Description:** At a Holtburg staircase in cells `0xA9B40159` +
`0xA9B4015A` (XY ≈ 110, 26; user-discovered post-A6.P7 visual
verification), the player sphere cannot climb the stairs. Walking into
the foot of the stairs from the east, the sphere hits a phantom slide
with radial-cyl normals (e.g. `(0.88, -0.47, 0)`) and never gains
altitude. Z stays at 94.00 for the entire 4216-record capture in
`stairs-broken.jsonl`.
**Root cause / status:** The staircase is built as ~10 stacked cylinder
parts of multi-part entity `entityId=0x0040B500`. Each cyl is
`radius=0.80m, height=0.80m` at `Y=26.60`, stepping up in X+Z by 0.25m
per step. All parts carry `state=0x00000000` — **no
`HAS_PHYSICS_BSP_PS` flag**, so A6.P7's `Transition.BspOnlyDispatch`
gate does NOT fire. Cyls are tested. Cyl height (0.80m) exceeds A6.P6's
step-up budget (0.60m), so grounded step-over fails too. Falls through
to wall-slide → diagonal radial normal → slide tangent driven into
perpendicular cell wall → stuck.
The `[resolve-bldg]` lines show `gfxObj=0x0100081A hasPhys=False
bspR=0.00 vAabbR=0.82` — the underlying `GfxObj` has NO physics BSP. We
appear to be synthesizing a cyl from the visual AABB radius
(`vAabbR=0.82` → registered as `r=0.80`). This synthesis path is the
suspected mis-registration.
This is NOT a regression of A6.P7 (the cottage door works correctly
post-A6.P7, visually verified). It's a separate bug shape that A6.P7's
fix didn't address.
**Files:**
- Captures: `stairs-broken.jsonl`, `stairs-broken.launch.log`,
`stairs-working.jsonl`, `stairs-working.launch.log` (gitignored, on
disk for the next session to read)
- Suspected mis-registration: `src/AcDream.Core/Physics/ShadowShapeBuilder.cs::FromSetup`
- Entity hydration site: `src/AcDream.App/Rendering/GameWindow.cs::BuildInteriorEntitiesForStreaming`
**Research:**
- [`docs/research/2026-05-25-stairs-cyl-investigation-handoff.md`](research/2026-05-25-stairs-cyl-investigation-handoff.md)
— full investigation handoff with 4 research questions, do-not-retry
list, and 3 candidate fix shapes
- A6.P7 background (closed companion):
[`docs/research/2026-05-25-a6-door-cyl-retail-dispatch-investigation.md`](research/2026-05-25-a6-door-cyl-retail-dispatch-investigation.md)
**Acceptance:** Walking west into the broken staircase at cells
`0xA9B40159` + `0xA9B4015A`, the player sphere ascends step-by-step to
the top (Z=96.47), then can walk back down to the bottom (Z=94.22) — no
phantom diagonal slide normals attributed to entity `0x0040B500`.
Comparable trajectory to `stairs-working.jsonl` cellar-stairs baseline.
---
## #100 — Transparent rectangular patches around every house (terrain rendering)
**Status:** OPEN

View file

@ -0,0 +1,339 @@
# M1.5 — Broken stairs (cyl-only multi-part entity) — investigation handoff
**Date:** 2026-05-25 PM
**Status:** Filed as issue #101 (post-A6.P7 visual verification surfaced a NEW
bug, not the closed door bug). **Research-only next session.** No
implementation until we know what retail does at this exact stair location.
**Predecessor handoff:** [`2026-05-25-a6-door-cyl-investigation-handoff.md`](2026-05-25-a6-door-cyl-investigation-handoff.md)
(closed by A6.P7 commit `888272a`).
---
## TL;DR
A6.P7 visual verification at Holtburg confirmed the cottage door is fixed.
While exploring, the user found **a different staircase that doesn't work**
sphere can't climb at all. Captures show:
- Stairs are in cells `0xA9B40159` + `0xA9B4015A` (NOT the cottage-cellar
cells `0xA9B40143/146/147` that work post-A6.P3 cellar fix).
- Geometry is a **multi-part entity** `0x0040B500` (entityId; ~150 parts in
the setup; 10 of them are stair-step cylinders).
- Each step is a separate cylinder (`r=0.80m, h=0.80m`) at `Y=26.60`, stepping
up in X and Z (0.25 m per step, Z: 94.22 → 96.47).
- `state=0x00000000` on each cyl part — **no `HAS_PHYSICS_BSP_PS` flag**, so
A6.P7's dispatch gate (`Transition.BspOnlyDispatch`) does NOT skip them.
- The cyls fire 284 `result=Slid` with diagonal radial normals like
`(0.88, -0.47, 0)` — the same phantom shape A6.P7 closed for the cottage
door, but here the cause is per-cyl-without-BSP, not per-entity-with-both.
- **Player Z stayed at 94.00 for the entire 4216-record capture** — never
gained altitude.
This is **NOT** a regression of A6.P7. The fix did exactly what retail does
for entities with `HAS_PHYSICS_BSP_PS`. The stair bug is a separate class:
**cyl-only entities (no BSP) whose cyl geometry shouldn't physically block
the player but does.**
---
## What today shipped (DO NOT redo)
### A6.P7 — retail-binary cyl/BSP dispatch (commit `888272a`)
- File: `src/AcDream.Core/Physics/PhysicsBody.cs` (added
`PhysicsStateFlags.HasPhysicsBsp = 0x00010000`)
- File: `src/AcDream.Core/Physics/TransitionTypes.cs` (added
`Transition.BspOnlyDispatch(uint)` predicate + per-entry guard at the
cyl/sphere branch)
- Test: `tests/AcDream.Core.Tests/Physics/A6P7DispatchRulesTests.cs` (7 tests)
- Investigation:
[`docs/research/2026-05-25-a6-door-cyl-retail-dispatch-investigation.md`](2026-05-25-a6-door-cyl-retail-dispatch-investigation.md).
- **Visual-verified at Holtburg cottage door 2026-05-25.** Captures:
`launch-a6p7.log`, `launch-a6p7-v2.log` — 1187 `[cyl-skip-bsp]`, 0
`[cyl-test]` on the door, 30 axis-aligned hits, no phantom diagonals.
---
## The new bug — captures + evidence
### Captures (on disk, gitignored — DO NOT commit them; treat as live data)
- **Working baseline** (cellar stairs that work): `stairs-working.jsonl`
(16.9 MB, ~22K records). Z range 90.95 ↔ 94.00 (full cellar climb). 12
cell transitions; only 23 `hit=yes` events; no diagonal normals; user
ran up + down twice. Cells `0xA9B40143/146/147`.
- **Broken stairs**: `stairs-broken.jsonl` (8.1 MB, 4216 records). Z stayed
at 94.00 for the entire capture. Cells `0xA9B40159` + `0xA9B4015A`. The
player tried multiple approach angles; never climbed any step.
- **Launch logs with probes**: `stairs-working.launch.log`,
`stairs-broken.launch.log`. Contain `[cyl-test]`, `[cyl-skip-bsp]`,
`[bsp-test]`, `[resolve]`, `[resolve-bldg]` probe lines.
### Reproduction
Login as `+Acdream` at Holtburg. The cellar stairs work (verified). The
broken stairs the user found are at world XY around (110, 26), Z range
94 → 96. Walk west into them — sphere hits something diagonal and gets
stuck oscillating between `n=(0, 1, 0)` and `n=(0.87, -0.49, 0)` slides.
### Geometry summary (from `stairs-broken.launch.log`)
The blocker is multi-part entity `entityId=0x0040B500`. Ten of its parts
are cylinders forming a staircase at `Y=26.60`:
| Part | World XY | Z (cyl bottom) |
|---|---|---|
| `0x40B5008C` (part 140) | (108.72, 26.60) | 96.47 |
| `0x40B5008D` (part 141) | (108.97, 26.60) | 96.22 |
| `0x40B5008E` (part 142) | (109.22, 26.60) | 95.97 |
| `0x40B5008F` (part 143) | (109.47, 26.60) | 95.72 |
| `0x40B50090` (part 144) | (109.72, 26.60) | 95.47 |
| `0x40B50091` (part 145) | (109.97, 26.60) | 95.22 |
| `0x40B50092` (part 146) | (110.22, 26.60) | 94.97 |
| `0x40B50093` (part 147) | (110.47, 26.60) | 94.72 |
| `0x40B50094` (part 148) | (110.72, 26.60) | 94.47 |
| `0x40B50095` (part 149) | (110.97, 26.60) | 94.22 |
Each cyl: `radius=0.80, height=0.80, state=0x00000000`. The entity also
has a BSP part `obj=0xB5008900 gfx=0x01000C16 radius=2.645 pos=(109.30,
26.30, 95.75)` but it's effectively non-physics
(`hasPhys=False bspR=0.00 vAabbR=0.82`) — the `vAabbR` here is the
**visual** AABB radius being borrowed as a cylinder fallback because the
underlying `GfxObj` has no physics BSP.
### What's blocking the player
Sphere at `(112.115, 25.995, 94.00)` wants to move west. The closest cyl
`0x40B50095` is at `(110.97, 26.60, 94.22)`:
- `distXY = 1.295m` (just barely outside reach `0.80 + 0.48 = 1.28m`)
- But during sub-stepping the sphere center crosses 1.28m → cyl overlaps
- Radial normal direction from cyl center to sphere: `(0.884, -0.467, 0)`
matches observed phantom hits `(0.88, -0.47)`, `(0.86, -0.51)`, etc.
The cyl is **too tall (0.80m) to step over** under A6.P6's grounded
step-over check (step-up budget = 0.60m). Falls through to the
wall-slide branch which produces the diagonal radial normal that drives
the sphere's slide tangent into the perpendicular cell wall, then
re-blocks. Net: stuck.
### Why A6.P7 doesn't help
A6.P7 gates the cyl branch on `(state & 0x10000) != 0`. These stair cyls
have `state=0x00000000` — bit not set. Guard does NOT fire. Cyls are
tested. Sphere blocks.
---
## What this session needs — retail investigation
**Mandate:** report-only research, NO implementation. Use the `/investigate`
skill. The fix design comes in a subsequent session once the retail
behavior is settled.
### Question 1 — What does retail DO at this exact staircase?
**Use cdb.** The toolchain in `CLAUDE.md` "Retail debugger toolchain" is
ready. The matching binary + PDB are verified.
Concrete experiment:
1. Have the user run the retail acclient.exe (Microsoft AC official build
v11.4186) at the same world location (cells `0xA9B40159` + `0xA9B4015A`,
XY ≈ (110, 26)). The user needs to be IN the building, AT the foot of
these stairs.
2. Attach cdb with breakpoints:
- `acclient!CCylSphere::collides_with_sphere` at `0x53a880` — counter
`$t0`, log every 100 hits with the `this` pointer and the moving
sphere's position, `gc`. Auto-detach after 5000.
- `acclient!CCylSphere::intersects_sphere` (the dispatch from
`CPhysicsObj::FindObjCollisions` cyl branch) — counter `$t1`, log
entity address.
- `acclient!CObjCell::find_env_collisions` — counter `$t2`. Tells us if
retail uses cell BSP for stair collision.
- `acclient!CPartArray::FindObjCollisions` — counter `$t3`. Confirms BSP
dispatch path.
3. Have the user walk straight into the broken stairs from outside, then
try to climb them. Capture 30 seconds.
4. Detach. Analyze:
- Does `CCylSphere::collides_with_sphere` fire on the stair entity? If
yes → retail's cyls ARE active here, and retail somehow handles them
differently (different step-up threshold? cell-context-aware?). If
no → retail's cyls are excluded by something we don't replicate.
- Does `CObjCell::find_env_collisions` fire heavily? If yes → retail
might be using cell BSP polygons for the stairs (and the entity cyls
are decorative/click-targets only).
### Question 2 — What's the Setup ID? Compare retail's PhysicsObj construction
Our `[resolve-bldg]` lines show the entity is built from GfxObj
`0x0100081A` with `hasPhys=False`. **What's the Setup ID for entity
`0x0040B500`?** Trace through our streaming code to find which Setup
emitted the 150-part build.
Steps:
1. Grep `src/AcDream.App/Rendering/GameWindow.cs` for the
`BuildInteriorEntitiesForStreaming` path (CLAUDE.md says it hydrates
EnvCell static objects with id `0x40xxxxxx`).
2. Add a temporary `[entity-source]` probe that logs the Setup id when an
entity gets registered. Or check existing diagnostic output — the
`gfxObj=0x0100081A` is the part's GfxObj, but we need the parent Setup.
3. With the Setup id in hand, look up retail's behavior:
- Decompile / grep `docs/research/named-retail/acclient_2013_pseudo_c.txt`
for `CPhysicsObj::InitPartArrayFromSetup` or similar to see how retail
builds the part_array from a Setup. Does retail include every part as
a collision shape, or filter by some flag?
### Question 3 — Why is `vAabbR` becoming a cylinder?
The `[resolve-bldg]` line shows `gfxObj=0x0100081A hasPhys=False bspR=0.00
vAabbR=0.82`. We registered a `r=0.80` cyl. The 0.80 ≈ 0.82 match is
suspicious — we're using the **visual AABB radius** as a fallback cyl
radius when there's no physics BSP.
Steps:
1. Find the code path in our tree that does this fallback. Likely in
`src/AcDream.Core/Physics/ShadowShapeBuilder.cs` `FromSetup` or in
`RegisterMultiPart`. Look for cases where `GfxObj.PhysicsBSP` is null
and a cyl is synthesized.
2. Cross-reference retail: does retail synthesize a cyl from visual bounds
when physics is null? Or does retail skip such parts entirely for
collision (visual-only)?
3. ACE check: `references/ACE/Source/ACE.Server/Physics/PhysicsObj.cs`
how does ACE construct the part_array from a Setup with mixed
physics/visual-only parts?
### Question 4 — Cell BSP fallback
If retail's stairs are walked via cell BSP polygons (not entity cyls),
what's in cell `0xA9B40159`'s BSP at this XY/Z? Is there a walkable
polygon staircase that we're not iterating?
Steps:
1. Use `ACDREAM_DUMP_CELLS=0xA9B40159,0xA9B4015A` to dump the cell BSPs to
JSON. (Confirm the env var path; see existing `CellDump` infra near
issue #98's apparatus.)
2. Look for inclined polygons in the dump that form the staircase. If
present → retail likely uses these for collision; our entity cyls are
either a setup misinterpretation or redundant.
---
## Files to read FIRST next session
| Path | Why |
|---|---|
| `docs/ISSUES.md` (#101) | The filed issue with severity + acceptance |
| `docs/research/2026-05-25-a6-door-cyl-retail-dispatch-investigation.md` | A6.P7 background (closed; companion bug) |
| `docs/research/named-retail/acclient_2013_pseudo_c.txt:276776` | `CPhysicsObj::FindObjCollisions` |
| Setup dat reader path in `src/AcDream.Core/Physics/ShadowShapeBuilder.cs` | Cyl synthesis from Setup; the suspected fallback |
| `src/AcDream.App/Rendering/GameWindow.cs::BuildInteriorEntitiesForStreaming` | Entity hydration for EnvCell statics |
| `references/ACE/Source/ACE.Server/Physics/PhysicsObj.cs` | ACE PartArray construction |
| `references/ACE/Source/ACE.Server/Physics/Common/Setup.cs` | ACE Setup → PartArray pipeline |
---
## Tests that must stay green
Same as A6.P7 list:
```
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|FullyQualifiedName~A6P7DispatchRulesTests"
```
Expected: 20/20 pass.
---
## Things NOT to do (do-not-retry)
1. **Don't lower step-up height** to make A6.P6's grounded step-over fit the
0.80m cyl. Step-up budget = 0.60m is retail-faithful. Tweaking it would
regress every other surface where 0.60m is correct (curbs, low ledges).
2. **Don't extend A6.P7's `BspOnlyDispatch` to entities with `state=0`.**
That gate is retail-specific (`HAS_PHYSICS_BSP_PS`). Skipping cyls
purely because peer parts exist with BSP would diverge from retail and
break NPC cyl-only entities.
3. **Don't disable cyl fallback when `hasPhys=False` without checking
retail.** Until we know how retail handles `GfxObj` with no physics
BSP, "just skip the cyl" might break other content (small decorative
items that DO collide in retail).
4. **Don't add per-entity workarounds** ("if entity id 0x0040B500, skip
cyls"). Per CLAUDE.md no-workarounds rule.
5. **Don't enlarge the sphere's step-up budget for tall cyls.** Retail's
threshold is what it is. If retail steps over 0.80m cyls in this
scenario, the mechanism is something else.
---
## Three fix-shape candidates (for the FOLLOWING session, not this one)
Listed in rough order of retail-faithfulness based on the limited evidence
we have. The retail investigation will decide which is right.
1. **Don't synthesize cyls from visual AABB when `GfxObj.PhysicsBSP` is
null.** Suppress at registration time in `ShadowShapeBuilder.FromSetup`.
Retail-anchored: if retail's `CPartArray` doesn't include such parts in
the collision list, our registration shouldn't either. The cell BSP
would then be the only collision source.
2. **Use cell BSP polygons** for stair geometry; entity cyls are
decorative-only for this entity class. Requires: (a) confirming cell
`0xA9B40159` BSP has walkable stair polys, (b) ensuring our cell BSP
query iterates them. Likely a no-op on our side once (1) is done.
3. **Make `step_sphere_up` cyl-height-tolerant** — if the sphere is on a
walkable plane and a cyl is detected, attempt step-up even when cyl
height > step-up budget IF a walkable surface exists at the top of the
cyl. Retail-anchored ONLY if cdb shows retail does this on these
specific stairs.
---
## Pickup prompt for next session
```
A6 — Broken stairs cyl investigation (issue #101). Investigation-only session.
Read first (in this order):
1. docs/research/2026-05-25-stairs-cyl-investigation-handoff.md
(this file — full context, captures, geometry, do-not-retry list)
2. docs/ISSUES.md #101
3. docs/research/2026-05-25-a6-door-cyl-retail-dispatch-investigation.md
(A6.P7 background — closed; companion bug)
State both altitudes:
Currently working toward: M1.5 — Indoor world feels right
Current phase: A6 — broken-stairs investigation (issue #101)
Session mandate: retail investigation, NOT implementation. Use the
/investigate skill. Specific questions (each must be answered with cited
evidence — retail line numbers, cdb traces, dat dumps):
1. Does retail's CCylSphere::collides_with_sphere fire on the stair-step
cylinders at cells 0xA9B40159/0xA9B4015A when a player walks in to
climb them? If yes — how does retail walk past 0.80m-tall cyls? If
no — what excludes them?
2. What's the Setup ID for entity 0x0040B500? Trace from
GameWindow.cs::BuildInteriorEntitiesForStreaming. Cross-reference how
retail's CPhysicsObj::InitPartArrayFromSetup (or equivalent) builds
the collision shape list — does retail include parts with
hasPhys=False?
3. Why does our ShadowShapeBuilder synthesize an r=0.80 cyl from
vAabbR=0.82 when GfxObj.PhysicsBSP is null? Identify the code path.
Does retail do this?
4. Dump cell 0xA9B40159's BSP polygons (ACDREAM_DUMP_CELLS). Does the
cell BSP have walkable stair polygons? If yes — retail's stair
collision is the cell BSP, not the entity cyls.
Deliverable: a short report (~2-3 pages) covering the 4 questions with
retail line numbers, cdb trace excerpts, code citations. Then propose
which of the 3 fix-shape candidates is most retail-faithful (or a fifth
shape that emerges from the research).
DO NOT implement the fix this session. Save it for the session after.
Do-not-retry list (in handoff doc) — read it before starting.
Tests to keep green if any code changes happen (none expected this
session): see handoff doc.
Reproduction setup for the broken scenario:
ACDREAM_PROBE_BUILDING=1 ACDREAM_PROBE_RESOLVE=1
ACDREAM_CAPTURE_RESOLVE=<path>.jsonl
walk to cells 0xA9B40159/A in Holtburg (XY ≈ 110, 26)
```