Slice 3 v2 (3e140cf) added point-in cell-stickiness in
ResolveCellId's indoor branch. User verification + slice3v2 capture
confirms: cell-resolver ping-pong is FULLY CLOSED.
Data:
- scen2_v2 capture (pre-slice-3): 20+ cell-transit events with
rampant ping-pong (0xA9B4014B ↔ 0xA9B4014A ↔ 0xA9B4013F at the
cellar boundary, Z stable ~96.4 — same tick re-classification)
- slice3v2 capture (post-fix): 1 cell-transit event (login teleport
only) — ping-pong fully eliminated
Findings:
- A6.P2 Finding 3 (cell-resolver sling-out family) CLOSED.
- Issue #90 (sphere-overlap stickiness workaround in same function)
now redundant; can be removed in A6.P4 after broader visual
verification.
- Issue #97 (phantom collisions + fall-through on 2nd floor) hypothesis
pending: same instability family, likely closed as side-effect of
this fix. Re-test on next happy-test session.
- Issue #98 (cellar-up stuck) PERSISTS but with NEW DIAGNOSIS.
Originally filed as cell-resolver ping-pong (which was true and now
fixed), but user verification shows the cellar-up symptom remains
with a DIFFERENT root cause: BSP step-physics at the cellar stair
TOP. Push-back trace from slice3v2 capture:
n=(0, -0.719, 0.695) sloped face (walkable per FloorZ=0.664)
delta=(0, 0, 0.75) step-down probe lifts sphere by 0.75m
winterp=1.0->0.0 entire walk-interp consumed per tick
Player progresses up most of the stairs but blocks at top step
where the cellar transitions to the cottage main floor. #98 issue
updated with this re-diagnosis.
Includes:
- scen4_cottage_cellar_slice3 acdream.log (slice 3 v1 evidence;
ping-pong already closed by v1's sphere-overlap stickiness, but
v1 over-corrected by holding player in cellar during legitimate
transitions)
- scen4_cottage_cellar_slice3v2 acdream.log (slice 3 v2 evidence;
point-in stickiness fixes the over-correction; cellar-up reveals
the deeper BSP step-physics bug)
Docs updated:
- ISSUES.md — #98 re-diagnosed
- docs/plans/2026-04-11-roadmap.md — A6.P3 slice 3 marked SHIPPED;
slice 4 (or A6.P4) scoped for #98 step-physics investigation
- CLAUDE.md — Currently-working-toward block updated
Test suite: 1148 pass + 8 pre-existing fail (baseline maintained).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Slice 2 v1 (`892019b`) attempted to close issue #96 by removing the
PhysicsEngine.cs L622 per-tick CP seed. v1 build/test green, CP-write
count dropped 91% in scen3 re-capture, BUT user happy-test surfaced
a regression: BSP step_up at the last step of stairs failed because
sub-step 1's AdjustOffset had no ContactPlane to compute the lift
direction.
Slice 2 v2 (`f8d669b`) reverted the seed removal + added a no-op-if-
unchanged guard inside CollisionInfo.SetContactPlane. The guard
early-returns when called with values matching current ci state.
Outcome:
- #96 PARTIALLY ADDRESSED, scope updated in ISSUES.md to "accepted as
documented retail divergence." The seed is load-bearing for step_up;
closing #96 fully would require deeper refactor (AdjustOffset
fallback to body.ContactPlane). Guard is benign improvement.
- Slice 2 v2 verification capture (scen3_inn_2nd_floor_slice2v2/
acdream.log) committed as evidence — 226,464 cp-writes from L624
seed confirms guard doesn't trigger for fresh-ci-per-tick pattern.
- Slice 2 v1 verification capture (scen3_inn_2nd_floor_slice2/
acdream.log) also committed — confirms v1 actually reduced cp-writes
(2,690 total) but the step_up regression made it unshippable.
NEW M1.5 BLOCKER FILED — issue #98: cellar ascent stuck at last step.
Evidence in slice2v2 capture's cell-transit chain:
0xA9B4014B → 0xA9B4014A → 0xA9B4013F → 0xA9B4014A → 0xA9B4014B → ...
(Z stable ~96.4; CellId ping-pongs every tick)
This is Finding 3 family (cell-resolver hysteresis missing) — same
root cause as #90 workaround + scen4 sling-out. Retail oracle:
CObjCell::find_cell_list Position-variant at
acclient_2013_pseudo_c.txt:308742-308783.
NEXT — A6.P3 slice 3:
- Port retail's cell-array hysteresis into ResolveCellId +
CheckBuildingTransit.
- Closes#98 (cellar-up), possibly #97 (phantom collisions same
instability family), enables #90 workaround removal.
Documents updated:
- ISSUES.md — #96 scope updated, #98 filed
- docs/plans/2026-04-11-roadmap.md — A6.P3 slice 2 marked SHIPPED,
slice 3 scope added
- CLAUDE.md — Currently-working-toward block updated to slice 3
Test suite: 1148 pass + 8 pre-existing fail (baseline maintained).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Unexpected slice 1 win: the synthesis-strip + Mechanism B (LKCP
restore) fix didn't just close Finding 2 (CP-write blowup) — it also
unblocked stair-walking, which A6.P2 had categorized as Finding 1+3
territory expected to need separate fixes. User reports walking up
and down the inn stairs multiple times in acdream post-fix.
Shape shift in tag distribution:
Tag Pre-fix (FAIL) Post-fix (SUCCESS) Signal
---- ------------- ------------------ ------
indoor-walkable 859 0 synthesis gone
push-back-cell 1478 879 (-40%) multi-cell relaxed
push-back 51 345 (+577%) real step-up firing
push-back-disp 4156 6055 (+46%) real traversal
cp-write 33969 57846 L622 seed (slice 2)
Pre-fix: synthesis firing while physics hammers BSP trying to resolve
stair-step (failure mode). Post-fix: real BSP queries succeeding, real
step-up + step-down landing. Same shape as retail's stair-climb
(retail scen2: BP2 step_up=188, push-back-disp dominates).
A6.P2 Finding 1 (dispatcher entry frequency mismatch) hypothesis was
"likely secondary effect of Finding 2 — may close as side effect of
the fix." Confirmed empirically: dispatcher activity now matches
retail-like shape without explicit Finding 1 work.
Remaining (slice 2 territory):
- L622 per-tick PhysicsEngine.ResolveWithTransition seed fires 99.3%
of remaining cp-writes; retail's equivalent fires zero times on
flat-floor walks. Gate this seed to close the remaining CP-write
gap.
- Phantom collisions + occasional fall-through on 2nd floor reported
by user during happy-testing. New issue to file.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A6.P1 (cdb probe spike) + A6.P2 (analysis report) both SHIPPED this
session. Updated:
docs/plans/2026-04-11-roadmap.md — M1.5 phase block now shows A6.P1
+ A6.P2 SHIPPED with commit refs; A6.P3 entry expanded with the
Finding-2-first sequencing recommendation from A6.P2; A6.P4 entry
notes the original "Holtburg Sewer end-to-end" acceptance walk is
unreachable (sewer doesn't exist).
docs/plans/2026-05-12-milestones.md — M1.5 demo scenario split into
building/cellar half (achievable post-A6.P3) + dungeon half (blocked
on issue #95 visibility blowup; promote to post-M1.5 if #95 isn't
fixed in scope). Issue list updated: added #95 + indoor sling-out
(new from scen4); marked stairs/2nd-floor/cellar as characterized by
A6.P2 Finding 2 family.
CLAUDE.md — Currently-working-toward block now points at A6.P3 as
the active phase. A6.P1 + A6.P2 ship noted with the findings doc
pointer. Demo-scenario note updated to reflect the sewer + #95
reality. Issues-in-scope updated.
Also includes a 1-line trailing-prompt addition to scen3 + scen4
retail.log files (cdb wrote one more `0:000>` after the kill that
landed after the original capture commits).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Substituted "Holtburg Sewer" portal with Town Network Portal — no
sewer entry exists in this world (user-verified). Town Network is
also an outdoor->indoor portal transition with the same physics
signature.
Both clients walked to the portal, entered, walked 2 m inside.
Retail: clean traversal. Acdream: also clean (no failure mode).
Retail (decoded, 23,890 raw / 9,769 BP lines):
BP1 transitional_insert: 13,863
BP4 find_collisions: 9,552
BP5 adjust_sphere: 97
BP6 check_walkable: 55
BP7 set_contact_plane: 65 (moderate, portal threshold + indoor)
BP2 step_up: 1
Acdream (31,914 lines, no failure):
[cp-write]: 20,956 (vs retail BP7 = 65 — ~322x ratio)
[cell-cache]: 9,642 (Holtburg landblock streaming)
[check-bldg]: 740
[push-back-disp]: 34 (flat-ground walking)
[push-back]: 1
[cell-transit]: 12 (CLEAN traversal, no thrashing)
cell-transit event chain — captures the portal entry signature:
0x00000000 -> 0xA9B30030 (login teleport)
0xA9B30030 -> 0xA9B40029 -> 0xA9B40021 -> 0xA9B40019 ->
0xA9B40011 -> 0xA9B40012 -> 0xA9B4000A -> 0xA9B4000B ->
0xA9B40003 (walked across Holtburg, all reason=resolver)
0xA9B40003 -> 0x00070143 reason=teleport (PORTAL ENTRY)
scen5 is the "control" — both clients reached their target, no
visible failure. The CP-write blowup persists as the only A6.P2
divergence. Useful baseline for separating "indoor physics broken
during walking" (scen2, scen3, scen4) from "indoor physics okay
when portal-delivered" (scen5).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Asymmetric pair (scenario-level, not protocol-failure):
- Retail: user walked UP out of the cellar (ascent of 2 cellar
steps + exit through doorway) — captures ascent + indoor-to-
outdoor transition.
- Acdream: user teleported INTO the cellar, walked a few meters,
the resolver flung +Acdream OUTSIDE the cottage entirely
(landblock prefix changed A9B4 -> A9B3 mid-walk) — captures
a real indoor physics failure that's not a stair issue per se.
Both traces are valuable to A6.P2 even though they don't match
walk-for-walk.
Retail (decoded, 22,536 raw / 12,875 decoded BP lines):
BP1 transitional_insert: 9,402
BP4 find_collisions: 12,596 (ended in mem-access error
@ hit#12596 - cdb hit a null
transition arg, dropped to
interactive prompt; worth a
note for A6.P2 retail edge)
BP5 adjust_sphere: 136
BP6 check_walkable: 128
BP2 step_up: 13 (2-step cellar = 13 vs scen2
4-step inn = 188; non-linear)
BP7 set_contact_plane: 3 (Finding 2 holds)
Acdream (42,001 lines, ended with sling-out):
[cp-write]: 35,624
[check-bldg]: 5,495 (CheckBuildingTransit fired
constantly trying to re-resolve
which building +Acdream was in)
[cell-cache]: 540
[push-back-disp]: 82 (very few dispatcher hits)
[push-back]: 1 (almost no sphere-adjustment)
[indoor-bsp]: 2 (indoor BSP barely queried!)
[cell-transit]: 3 (3 transit events captured the sling:
0xA9B40148 -> 0xA9B40029 -> 0xA9B30030
all reason=resolver)
Sling-out signature: indoor BSP never engaged (only 2 indoor-bsp
hits), but the cell resolver fired 3 transit events crossing a
landblock boundary, with check-bldg thrashing in between. This is
distinct from scen2's stair-attempt pattern (which hammered the
BSP); scen4 shows the resolver pushing the character out of indoor
space entirely without triggering the indoor BSP collision path.
A6.P2 fix surface: investigate why ResolveCellId / CheckBuildingTransit
push a player from indoor cell 0xA9B40148 to outdoor cell 0xA9B30030
through routine walking. Likely the same family as the M1.5 hypothesis:
indoor cell membership isn't sticky (the ping-pong bug from the
2026-05-20 A4 handoff in a different guise).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User reached the 2nd floor in acdream via ACE @teleport (stair-physics
unblocked separately by scen2). Retail walked normally to 2nd floor.
Both clients performed the same walk: forward 3 m, sidestep 1 m,
walk back. Flat-floor scenario, no stairs, no transitions.
Retail (decoded, 21,337 lines):
BP1 transitional_insert: 10,217 hits
BP4 find_collisions: 10,636 hits
BP5 adjust_sphere: 113 hits
BP6 check_walkable: 113 hits threshold=0.6642
BP2 step_up: 0 hits (no stairs)
BP3 set_collide: 0 hits (no walls)
BP7 set_contact_plane: 0 hits (KEY: zero CP updates)
Acdream (93,558 lines):
[cp-write]: 86,748 (vs retail BP7 = 0 — INFINITE ratio)
[push-back-disp]: 2,752
[push-back]: 320
[push-back-cell]: 550
[other-cells]: 550
[indoor-bsp]: 1,061
[indoor-walkable]: 707
KEY FINDING for A6.P2: scen3 is the strongest CP-write blowup
evidence yet. On a flat 2nd-floor walk where retail's
set_contact_plane fires ZERO times across the entire scenario,
acdream rewrites the contact plane 86,748 times. This is the
exact pattern Finding 2 hypothesized (M1.5 design spec §1.2):
acdream resynthesizes CP every frame instead of retaining it
through the documented retention mechanisms (LKCP-restore,
Path-6 land write, post-OK step-down probe).
scen3 pair confirms CP-write blowup isn't stair-specific — it
fires equally for ordinary flat-floor walking inside any indoor
cell. A6.P3 fix surface: same as Finding 2 — stop resynthesizing
CP per frame.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Original acdream capture (a9a427f) was a doorway-walk because acdream's
indoor stair physics doesn't work. For A6.P2 to characterize the
divergence we need the FAILURE captured, not a substitute walk.
User re-attempted the inn stairs in acdream (whatever it produces:
bumping, sliding, stuck). Failure signature is dramatic vs door-walk:
Tag | door-walk | stair-attempt | ratio
----------------+-----------+---------------+------
push-back-disp | 1,141 | 4,156 | 3.6x
push-back-cell | 87 | 1,478 | 17x
other-cells | 87 | 1,478 | 17x
indoor-bsp | 343 | 1,286 | 3.7x
indoor-walkable | 227 | 859 | 3.8x
cp-write | 70,244 | 33,969 | 0.5x (!)
The 17x explosion on push-back-cell / other-cells says acdream's
CheckOtherCells loop fires constantly when physics can't resolve a
stair-step — the indoor BSP query fails, then the multi-cell
fallback fails, then the next tick repeats. The cp-write DROP
(half the door-walk volume) is the inverse signal: when no ground
plane resolves, no CP gets written. Both are A6.P2 fix-surface
indicators.
Now scen2 pair = retail successfully climbs (BP2 step_up=188) vs
acdream tries and fails (push-back-cell explosion).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Python tool that decodes the retail.log hex-bits float fields produced
by a6-probe.cdb v4 into IEEE 754 single-precision values. Required
because cdb's .printf %f doesn't reliably format floats from dwo()
reads — v4 works around this by emitting 32-bit hex, this script
reinterprets via struct.unpack('<f', struct.pack('<I', value)).
Verified against scen1 retail.log:
BP6 threshold_h=0x3F2A0751 → threshold=0.6642 (= FloorZ exactly)
BP5 hit#1 Nz_h=0x3F800000 → Nz=1.0 (ground normal)
9,517 float fields decoded across 9,331 lines.
Output written next to input as .decoded.log. Format matches
acdream-side [push-back] probe (4-decimal floats), so A6.P2
analysis can compare line-for-line.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Acdream-side capture for the Holtburg inn doorway walk, paired with
the v4 retail capture committed at 180b4a5. 84,130 lines total.
Probe line distribution (~30 sec session, ~2 sec actual walk):
[push-back] (adjust_sphere): 8 hits — vs retail BP5 12 hits
[push-back-disp] (dispatch): 295 — vs retail BP4 5818 (!)
[push-back-cell] (other_cells): 5 — vs retail's check_other_cells
[indoor-bsp]: 26
[cell-transit]: 30 (cell ID changes)
[cp-write]: 73,304 (per-field writes) — vs retail BP7 18 fn calls (!)
[cell-cache]: 540
Two major divergences already visible from this single scenario:
1. DISPATCH FREQUENCY: retail's BSPTREE::find_collisions fires 20×
more than acdream's BSPQuery.FindCollisions. Could reflect either
different physics tick rate, different sub-step cadence, or
different call paths into the dispatcher.
2. CONTACTPLANE LIFECYCLE: acdream writes CP fields 73,304 times
in 30 seconds (~2,400/sec). Retail calls set_contact_plane 18
times (~0.6/sec). Even with a 6× field-write multiplier per
set_contact_plane call, that's ~100 actual CP updates in retail
vs ~12K in acdream — 100-1000× more frequent in acdream. This
directly confirms the spec's hypothesis that FindEnvCollisions
indoor branch is rewriting CP every frame (sub-step?) instead
of retaining it across frames. Same family as the
TryFindIndoorWalkablePlane workaround.
Per-call shape comparison (BP5 hit#1):
Retail: plane=(0,0,1) d=-0.0, sphere=(0.0046,10.31,-0.27) r=0.48,
mvmt=(0,-0,-0.75), winterp=1.0
Acdream: plane=(0,0,1) d=-0.0, sphere=(-0.43,11.02,0.46) r=0.48,
mvmt=Z-down, winterp 1.0→0.96 (small adjust applied)
Identical operation SHAPE (ground plane + vertical step-down probe
+ same radius). XY positions differ because walks were independent.
Scenario 1 complete. Remaining 8 scenarios deferred per user
direction. Python hex→float decoder + A6.P1 handoff doc to follow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v4 cdb probe captured paired field data for the Holtburg inn
doorway walk. 13,552 BP hits in ~2 sec of walking. Distribution:
- BP1 transitional_insert: 7,686 (sub-step loop)
- BP4 find_collisions: 5,818 (per cell per sub-step)
- BP5 adjust_sphere_to_plane: 12 (the over-correction suspect)
- BP6 check_walkable: 12
- BP7 set_contact_plane: 18
Smoking-gun verification:
BP6 threshold_h=0x3F2A0751 ≈ 0.664 = PhysicsGlobals.FloorZ
BP5 plane normal = (0,0,1), movement = (0,-0,-0.75) — classic
step-down probe against the ground polygon
BP5 sphere radius = 0x3EF5C28F ≈ 0.480 m — player foot sphere
All hex-bits floats decode cleanly via Python struct.unpack('<f').
Decoder script TBD as part of the handoff.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v3 with @@c++(*(float*)..) STILL produced 0.000000 across the board.
Conclusion: cdb's .printf %f is unreliable for our use case — possibly
doesn't handle the float-to-double promotion in varargs the way C
printf does, or has a deeper limitation we don't have time to debug.
Pivoting to: print all floats as 32-bit hex bits via %08X, reinterpret
in the Python analysis pipeline via struct.unpack('<f', bytes.fromhex(...))
to recover IEEE 754 single-precision values.
This bypasses cdb's float formatting entirely. Integer reads (which
work — substeps, insertType, collide flag, isWater) stay as %d.
The smoking gun: BP6's check_walkable threshold should be 0.0871556997
(cos 85°) per the decomp call site at acclient_2013_pseudo_c.txt:273202.
v4's BP6 should output threshold_h=0x3DB283D7. If it does, the
infrastructure is sound and we can proceed to all 9 scenarios.
v3 capture preserved as retail-v3-cpp-zero-floats.log audit trail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v2 dry-run produced correct hit counts but all %f field values
printed as 0.000000 — including BP6 threshold which the decomp says
must be 0.0871556997f (cos 85°). Root cause: cdb's MASM evaluator
returns dwo(addr) as a 32-bit integer; .printf %f expects a 64-bit
double; passing the integer to %f produces formatted-zero garbage.
Fix: switch all float-reading expressions to @@c++(*(float*)addr).
The C++ evaluator dereferences memory as a float pointer, returning
a proper float that .printf %f formats correctly. Integer reads (%d)
still use MASM dwo() — that works.
For double-indirect (pointer args), the form is
@@c++(*(float*)(*(unsigned int*)(@esp+N)+offset))
which reads the pointer at [esp+N], adds the offset, and treats the
result as a float pointer.
v2 capture preserved as retail-v2-zero-floats.log audit trail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dry-run of scenario 1 (retail-v1-broken-offsets.log preserved as
audit trail) surfaced three issues with the v1 cdb script:
1. STACK-ARG OFFSETS WRONG: BP actions used arbitrary registers
(@edx, @edi) to read function args, but __thiscall puts non-this
args on the stack ([esp+N] after the return address). All 12 BP5
"adjust_sphere" hits printed Nx=0.0 Ny=0.0 ... — fields not read.
Fixed by writing a type dumper (a6-types-dump.cdb + runner) that
uses cdb's `dt` command against the loaded PDB to get authoritative
struct offsets. v2 probe script (to be written next) will use
double-indirect reads (dwo(poi(@esp+N)+offset)) with correct
offsets from the dump.
2. TEE-OBJECT UTF-16 ENCODING: PowerShell's default Tee-Object writes
UTF-16 LE with BOM, making logs unparseable by grep without
conversion. Runner now uses Out-File -Encoding ASCII. Sacrifices
live console echo; use `Get-Content -Tail 50 -Wait` in a separate
shell if live monitoring is needed.
3. BP6 SYMBOL NOT FOUND: `acclient!CTransition::validate_walkable`
doesn't exist in the PDB. Decomp at line 272811 has
`CTransition::check_walkable` — likely the actual name. To be
verified + fixed in v2.
The BP hit-count distribution from v1 is still meaningful diagnostic
data (14,318 transitional_insert + 16,558 find_collisions + 40
set_contact_plane + 12 adjust_sphere + 1 step_up + 1 set_collide in
a 2-second walk through the inn doorway). Preserved as a baseline
sanity-check the v2 distribution can be diffed against.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User swapped in the correct Sept 2013 EoR build acclient.exe.
GUID {9e847e2f-777c-4bd9-886c-22256bb87f32}, linker UTC
2013-09-06T00:17:56 — exact match for refs/acclient.pdb.
T15 captures are unblocked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Creates the 9 per-scenario capture directories (gitkeep stubs) and
the findings doc stub at docs/research/2026-05-21-a6-cdb-capture-findings.md.
A6.P1 fills the capture log slots (Task 15, user-driven); A6.P2
fills the analysis tables and findings section.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit trail for the A6.P1 capture session: the retail binary at
C:\Turbine\Asheron's Call\acclient.exe is the 2015-06-12 build
(GUID {08e25c14-e2a1-46d5-b056-92b2e43a7234}), not the Sept 2013
EoR build that pairs with refs/acclient.pdb
(expected GUID {9e847e2f-777c-4bd9-886c-22256bb87f32}).
BP-driven A6 captures cannot proceed until the matching binary is
installed. User needs acclient.exe v11.4186 (linker timestamp
2013-09-06) to match our PDB.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>