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>
16 KiB
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
(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 cells0xA9B40143/146/147that 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) atY=26.60, stepping up in X and Z (0.25 m per step, Z: 94.22 → 96.47). state=0x00000000on each cyl part — noHAS_PHYSICS_BSP_PSflag, so A6.P7's dispatch gate (Transition.BspOnlyDispatch) does NOT skip them.- The cyls fire 284
result=Slidwith 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(addedPhysicsStateFlags.HasPhysicsBsp = 0x00010000) - File:
src/AcDream.Core/Physics/TransitionTypes.cs(addedTransition.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. - 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 23hit=yesevents; no diagonal normals; user ran up + down twice. Cells0xA9B40143/146/147. - Broken stairs:
stairs-broken.jsonl(8.1 MB, 4216 records). Z stayed at 94.00 for the entire capture. Cells0xA9B40159+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 reach0.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:
- 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. - Attach cdb with breakpoints:
acclient!CCylSphere::collides_with_sphereat0x53a880— counter$t0, log every 100 hits with thethispointer and the moving sphere's position,gc. Auto-detach after 5000.acclient!CCylSphere::intersects_sphere(the dispatch fromCPhysicsObj::FindObjCollisionscyl 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.
- Have the user walk straight into the broken stairs from outside, then try to climb them. Capture 30 seconds.
- Detach. Analyze:
- Does
CCylSphere::collides_with_spherefire 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_collisionsfire heavily? If yes → retail might be using cell BSP polygons for the stairs (and the entity cyls are decorative/click-targets only).
- Does
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:
- Grep
src/AcDream.App/Rendering/GameWindow.csfor theBuildInteriorEntitiesForStreamingpath (CLAUDE.md says it hydrates EnvCell static objects with id0x40xxxxxx). - Add a temporary
[entity-source]probe that logs the Setup id when an entity gets registered. Or check existing diagnostic output — thegfxObj=0x0100081Ais the part's GfxObj, but we need the parent Setup. - With the Setup id in hand, look up retail's behavior:
- Decompile / grep
docs/research/named-retail/acclient_2013_pseudo_c.txtforCPhysicsObj::InitPartArrayFromSetupor 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?
- Decompile / grep
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:
- Find the code path in our tree that does this fallback. Likely in
src/AcDream.Core/Physics/ShadowShapeBuilder.csFromSetupor inRegisterMultiPart. Look for cases whereGfxObj.PhysicsBSPis null and a cyl is synthesized. - 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)?
- 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:
- Use
ACDREAM_DUMP_CELLS=0xA9B40159,0xA9B4015Ato dump the cell BSPs to JSON. (Confirm the env var path; see existingCellDumpinfra near issue #98's apparatus.) - 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)
- 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).
- Don't extend A6.P7's
BspOnlyDispatchto entities withstate=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. - Don't disable cyl fallback when
hasPhys=Falsewithout checking retail. Until we know how retail handlesGfxObjwith no physics BSP, "just skip the cyl" might break other content (small decorative items that DO collide in retail). - Don't add per-entity workarounds ("if entity id 0x0040B500, skip cyls"). Per CLAUDE.md no-workarounds rule.
- 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.
- Don't synthesize cyls from visual AABB when
GfxObj.PhysicsBSPis null. Suppress at registration time inShadowShapeBuilder.FromSetup. Retail-anchored: if retail'sCPartArraydoesn't include such parts in the collision list, our registration shouldn't either. The cell BSP would then be the only collision source. - Use cell BSP polygons for stair geometry; entity cyls are
decorative-only for this entity class. Requires: (a) confirming cell
0xA9B40159BSP has walkable stair polys, (b) ensuring our cell BSP query iterates them. Likely a no-op on our side once (1) is done. - Make
step_sphere_upcyl-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)