Drive turn, movement, jump, and combat through the production InputDispatcher so connected Release testing works without an interactive Windows desktop. Track held automation actions through normal completion and every shutdown path.
Add a seven-destination ACE route with post-liveness memory, allocation, update, fatal-log, outbound-movement, and graceful-close gates. Record dynamic ACE population changes as context instead of a false lifetime oracle, and document the accepted rebaseline.
Co-authored-by: OpenAI Codex <codex@openai.com>
Replace the incomplete package path with one DatCollection-backed compatibility seam for PhysicsScripts and Animations. Preserve CreateBlockingParticle's inherited payload and following cursor, route every production and audit consumer through the corrected loaders, and apply retail's post-UnPack StartTime ordering.
Add exact stored-order PhysicsScriptTable upper-threshold resolution, high-byte DID and embedded-ID validation, plus live effect profiles with Setup-to-PhysicsDesc precedence across top-level and attached entity lifetimes. Keep blocking execution deferred and narrow TS-11 accordingly.
Pin synthetic malformed/cursor/order fixtures, installed-DAT blocking and recall audits, high-index IDs, IEEE boundaries, profile teardown, and ordinary decoder parity; synchronize architecture, inventory, milestones, roadmap, research, and memory.
Co-Authored-By: Codex <noreply@openai.com>
Establish the executable-backed PhysicsDesc, sequence-gate, PhysicsScript, CreateBlocking, particle-anchor, projectile, and Hidden-state behavior before changing runtime code. Correct stale blocking/threshold claims and synchronize the project instructions with the current UI architecture and matching retail binary.
Add copyright-safe packet and DAT-container fixtures plus a failing installed-DAT conformance audit for projectile shapes, typed tables, recall motion, default scripts, and raw CreateBlocking inventory.
Co-Authored-By: Codex <noreply@openai.com>
Preserve the authored gray track, trained-Recklessness range, live bright charge meter, and independent desired-power thumb while keeping Speed and Power over gray side regions. Correct retail text justification value 2 to left alignment and retain direct RenderSurface decoding in the texture inspection tool used to verify the assets.
Co-Authored-By: Codex <codex@openai.com>
The indoor GREY flap at a top-floor connecting room. The render portal side-cull
reconstructed each doorway's "interior side" (PortalClipPlane.InsideSide) from the
cell's AABB CENTROID. For a THIN connector cell (0xF6820118, 5 render polys), the
bounding-box center falls on the WRONG side of the 0118->0116 doorway, so the eye
read as a back-portal and the forward room 0116 was culled -> the aperture showed
the fog clear color = grey.
Retail's PView::InitCell (0x005a4b70) and acdream's own PHYSICS path
(CellTransit.cs:190) both read the explicit dat PortalSide bit ((Flags&2)==0)
instead of guessing from geometry. Port the render path (GameWindow.BuildLoadedCell)
to the same bit.
Proven by a live retail cdb trace (retail draws 0116 from the 0118 root at the grey
pose; tools/cdb/issue186-connector-decider.cdb) + an offline dat diagnostic
(Issue186...PortalSide_CentroidVsDatBit_AtGreyEye): the dat bit matches the old
centroid on every portal of these cells EXCEPT the one #186 breaks, so the switch is
surgical. Full regression green (App 741 / Core 2631); the CornerFlood + Issue113
dat-loading helpers updated to the same bit confirm every real Holtburg/tower/hall
cell floods identically. Touches neither PortalSideEpsilon nor the deleted
EyeInsidePortalOpening rescue (the two DO-NOT-RETRY traps).
Live-gated: user-confirmed no grey at any camera angle; probe shows 216 root=0118
frames, 0 still grey (0118->0116 now TRV, vis=4).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Slice 0 of the #182 verbatim rebuild. The classifier reproduces the design
baseline off acdream-crowd-resolve.jsonl (2883 move-intent resolves:
52.8% OK / 25.1% partial / 22.1% stuck / 107 airborne-stuck) — the A/B
'before' the rebuild measures against (retail target ~78% OK, 0 airborne-stuck).
The plan refines the design spec's §7: the airborne-stuck bleed is the
frames_stationary_fall counter (validate_transition increments; handle_all_collisions
zeros velocity at fsf>1), NOT the cached_velocity field (a separate reporting value).
Slices reorder accordingly; calc_friction (retail 0.25 vs acdream 0.0) is an
orthogonal L.3c divergence kept out of scope.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The #182 CSphere port (96ae2740) failed its visual gate and introduced an
airborne "stuck in the falling animation" regression. A player-attributed retail
cdb trace (tools/cdb/retail-crowd-jump3.cdb) proved retail's LOCAL client fully
runs player-vs-creature collision (76 land_on_sphere, 188 COLLIDED, 130 SLID,
~78% OK, glides across) -- NOT server-authoritative (an earlier unfiltered
land_on_sphere=0 read was a false lead the attributed trace refuted).
acdream's same-repro capture: 50.9% OK, 22.4% stuck, 115 airborne-stuck. Root
divergence: retail CPhysicsObj::UpdateObjectInternal (0x005156b0, pc:283688) sets
cached_velocity = (resolved - old)/dt -- velocity from ACTUAL movement, so a
blocked jump collapses to ~0 -> gravity -> the player falls/glides. acdream
integrates velocity + reflects on collision (PlayerMovementController ~:1008-1069),
so the jump velocity (~18) persists against the creature -> hang.
Fix = verbatim rebuild of the per-frame player-physics loop (UpdateObjectInternal
chain), velocity model first, transition internals kept. Full design +
retail function inventory + the capture apparatus + retail target numbers:
docs/superpowers/specs/2026-07-07-player-physics-update-verbatim-rebuild-design.md.
Implementation deferred to a fresh session (user decision). Also files #183
(floating distant scenery, observed during testing). #182 stays as the base.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Attached cdb to live retail (PDB MATCH), broke on PView::DrawCells (0x005a4840),
dumped cell_draw_num + cell_draw_list cell ids + the eye
(Render::FrameCurrent->viewer.viewpoint) while descending the Facility Hub spiral.
Retail's flood is dynamic and IDENTICAL in character to ours: from the spiral cells
it swings num 3->27 with gaze and collapses to 3 cells at many poses (cam=015d ->
{015d 015e 015f}). Our flood does the same (3->43). So retail does NOT keep the
staircase where we drop it -- the flood is exonerated as the cause.
Session trail (all in ISSUES #177): ruled out lighting, membership, camera coherence,
the collision sweep, the 0178/0182/0183 handoff cells, and edge-on eye-in-opening
(fix#1 shipped -> visual-gate-failed -> reverted, PortalVisibilityBuilder + AP-86 both
restored exactly). Freshest un-chased lead: the steps are STATIC objects (GfxObj
0x010000DE x6/cell) drawn via the viewcone cull, not cell shell.
Adds: Issue177StairDescentCameraFloodTests (real-camera+flood + composition +
flood-depth characterization pins) and a reusable retail-cdb capture toolchain
(tools/cdb/pview-verify.cdb, pview-spiral2.cdb with the correct top-level-qd detach --
qd in a CONDITIONAL bp action does NOT fire and strands cdb attached).
No production code change (fix#1 reverted). PARKED per user; M1.5 critical path next.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cdb trace of LIVE retail (tools/cdb/issue176-floor-light.cdb, binary<->PDB MATCH) PROVED retail
applies ALL its dynamic lights — 4 intensity-100 magenta portal lights (d3dIdx 3-6, falloff 6) +
the viewer fill (d3dIdx 1, 2.25) — as D3D hardware lights to EVERY Facility Hub cell, every frame,
stable. So the faceted purple wedges on the floor are retail-FAITHFUL. acdream did a per-cell
SelectForObject sphere-overlap 8-cap for cells, so the portal set could differ/flip per cell.
- LightManager.SelectForCell (retail minimize_envcell_lighting 0x0054c170): ALL dynamic lights
applied unconditionally (shader range cutoff zeroes non-reaching = D3D hardware range), then
nearest static torches fill remaining slots. Wired into EnvCellRenderer.GetCellLightSet.
Objects keep SelectForObject (minimize_object_lighting). Pins:
SelectForCell_AppliesAllDynamicLights_EvenOutOfReach + _SameDynamicSet_ForCellsFarApart_NoFlap.
- Apparatus: [light-detail] gains owner/cell/dyn (pinned the culprit = 2 portal weenies
0x000F4247/48 in 0x8A020118/19, intensity=100 magenta); CellVertexNormals_SmoothOrFaceted_Dump
(corridor floor uses SMOOTH per-vertex dat normals, not flat); tools/cdb/issue176-floor-light.cdb.
#176 RESIDUAL is NOT this fix. It's a RUNTIME draw z-fight in the seam floor. Eliminated (evidence):
NOT lighting (per-light cap + this both no-change), NOT membership (render cell 0x8A020164 stable
100% of 188k frames / 526 angles, res=None), NOT dat geometry (coplanar sweep empty at z=-6 floor
incl. cell 0164). NEXT = RenderDoc pixel-history. Full handoff + DO-NOT-RETRY:
docs/research/2026-07-06-176-seam-floor-zfight-handoff.md. Suites green: Core 2599 + 2 skip.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pseudocode for the S2 port (unpack_movement case 0 / move_to_interpreted_state
/ apply_current_movement / apply_interpreted_movement / DoInterpretedMotion),
anchored on decomp lines + validated against a LIVE cdb trace of a retail
observer (per-UM DIM order confirmed: style -> forward -> sidestep-stop ->
turn-stop; empty UM = wholesale Ready stop).
Also settles the packer question: RawMotionState::Pack (0x0051ed10) is pure
static-default-difference — outbound L.2b port already verbatim; the
empty-vs-explicit walk variance between captures is driver-client state,
handled identically by the wholesale apply.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The paperdoll doll now matches retail: correct held pose, framing, and
facing. Three decomp-sourced fixes closed the visual gate.
Pose: cdb-confirmed m_didAnimation = 0x030003C0 (gmPaperDollUI), played
once + HELD (set_sequence_animation framerate=0, RedressCreature
0x004a3c22). Dumping the dat showed the 29-frame anim has only two
distinct keyframes — frame 0 (transitional, bent arm) and frames 1..28
(byte-identical: the settled stance, arms down + leg back) — so
ApplyPaperdollPose applies the LAST frame statically (no looping).
Camera: ported verbatim from UIElement_Viewport::SetCamera (decomp
0x004a5a39). position (0.12,-2.4,0.88); direction (0,0,0) => IDENTITY
view frame => look straight down +Y, ZERO yaw; FOV pi/4 (CreatureMode
ctor default 0x004543cf); ambient 0.3. The prior hand-tune aimed the
camera at mid-body, adding a ~2deg yaw that turned the doll's face away
— full-body framing comes from eye-height + FOV, not aiming.
Heading: retail Frame::set_heading(h) (0x00535e40) builds facing
(sin h, cos h); System.Numerics CreateFromAxisAngle(+Z, +h) rotates the
body's default +Y forward to (-sin h, cos h) — the X-lean was MIRRORED
(~22deg), the real cause of the turned-away face. Negate the angle to
land on retail's facing.
Wrap-up: stripped the temporary O/P pose-frame stepper + Slice2
diagnostics; divergence register AP-66 reworded, AP-67 (RTT doll render
vs in-cell CreatureMode::Render) + AP-68 (per-race UpdateForRace
unimpl) added; DollCameraTests pinned to the retail values + a zero-yaw
guard. tools/cdb/paperdoll-pose.cdb = the pose-DID capture script.
Build + full suite green (Core 1579 / Core.Net 343 / App 597 / UI 425).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolves the divergence-register conflict: kept the accurate per-VERTEX AP-35
(Fix A shipped per-vertex; main's row was the stale pre-Fix-A per-pixel text),
kept main's UI rows AP-37..AP-42, and renumbered this branch's torch-gate row
AP-37 -> AP-43 (AP-37 was taken by main's LayoutDesc row). AP count 41 -> 42.
Retargeted the AP-37 references in WbDrawDispatcher + the CHECKPOINT to AP-43.
Marked ISSUES #140 RESOLVED (b7d655b) with the corrected root cause.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve the Fix D contradiction with decomp (workflow wf_f660eb88 + adversarial
verify) + 4 live cdb captures. The D3D-FF model was the WRONG oracle: retail has
TWO light systems — STATIC torches BAKE into wall vertices (calc_point_light,
triple-clamped: range gate + per-channel min(scale*color,color) + per-vertex
[0,1] from black), DYNAMIC lights go D3D hardware. The captured intensity=100 is
the purple PORTAL (magenta, dynamic), not a wall torch. Ground truth: 38 static
warm torches (orange (1,0.588,0.314)/cream, intensity=100, falloff 3-5) + 2 dynamic.
acdream over-brightness = two confirmed bugs: D-1 mesh_modern.vert folds
ambient+sun+torches into one UNCLAMPED accumulator (single frag clamp) -> warm
blowout; D-2 EnvCellRenderer never binds SSBO 4/5 so the cell shell reads a leaked
light set. Spec: D-1 in-shader clamp-split (clamp the torch sum on its own before
ambient/sun); D-2 bind the shell's own per-cell light set (mirror WbDrawDispatcher);
LightBake.cs is the C# conformance oracle. Adds the 4 reusable cdb capture scripts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(D.2b): exact retail chat colors from a live cdb dump
Attached cdb to a live retail acclient (PDB-matched) and read the named RGBAColor
constants at acclient 0x81c4a8+ (colorWhite/colorBrightPurple/colorLightBlue/
colorGreen/colorLightRed/colorGrey), used by ChatInterface::BuildChatColorLookupTable
@0x4f31c0. Replaced the approximated RetailChatColor palette with the ground-truth
values: speech=white, tell=colorBrightPurple(1,.498,1), channel=colorLightBlue
(.247,.749,1), system/popup=colorGreen(.5,1,.498), combat=colorLightRed, emote=colorGrey.
Capture scripts saved under tools/cdb/.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
The register's UN-2 row recorded a contradiction: the GetMaxSpeed XML doc
claimed the bare run rate was retail-correct (~5.9 m/s catch-up, calling
the xRunAnimSpeed multiply a misread), while the implementation multiplied
by RunAnimSpeed citing ACE. Settled against the binary, not the pseudo-C:
- BN pseudo-C (acclient_2013_pseudo_c.txt:305127) renders get_max_speed as
void with a bare `this->my_run_rate;` because it DROPS x87 instructions.
- Disassembling the PDB-matched v11.4186 binary at VA 0x00527cb0: all THREE
return paths end `fld <rate>; fmul dword ptr [0x007C8918]; ret`, and the
.rdata dword at 0x007C8918 is 4.0f. Sibling get_adjusted_max_speed
(0x00527d00) carries the same trailing fmul. Verifier committed at
tools/verify_un2_fmul.py (PE parse + byte decode, rerunnable).
- Retail paths: weenie null -> 1.0 x4; InqRunRate ok -> queried x4;
InqRunRate failed -> my_run_rate x4. ACE MotionInterp.cs:665-676 matches.
Changes:
- Doc-comment rewritten: the implementation is retail-correct; the catch-up
speed 2 x get_max_speed ~= 23.5 m/s at run 200 IS retail. The 1-Hz
remote-blip symptom the old comment attributed to this multiply is
therefore UNEXPLAINED by it (if it recurs: #41 family, not this).
- Weenie-null path aligned to retail's LITERAL 1.0 default (was MyRunRate).
- Tests re-pinned to the three retail paths (the old NoWeenie test pinned
the non-retail fallback).
- Register: UN-2 row deleted per the retire rule (6 -> 5 UN rows);
shortlist renumbered.
This is the 2nd confirmed instance of the BN x87-dropout artifact class
(memory: feedback_bn_decomp_field_names) deciding a register row.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reconciles the wip/main-local-claudemd-condensation distillation (made
~2026-06-03 by a parallel session) onto post-merge main:
- Current state section: ONE status block (<=5 lines + pointers, by
rule) + canonical reading order + the two digest entry points + the
divergence register; replaces status sediment scattered through
Goal/Roadmap sections.
- Kept from the merged main (the condensation predated them): the
memory/digest rule in How to operate, the divergence-register
section + phase-checklist item, de-dated milestone rules.
- Dropped: shipped-phase ship-notes, stale next-phase candidate lists,
the superseded reference_render_pipeline_state pointer.
- Also salvaged from the wip branch: .gitignore entries (.obsidian/,
claude-memory junction) + pdb_extract.py __main__ guard. The wip's
TextureDump edit predates main's args support (discarded) and its
physics-probe edits were STRIP-marked leftovers (discarded).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Attaches to live retail, dumps CameraManager/SmartBox type offsets, captures viewer eye origin per frame (pub+sought) to measure boom jitter vs acdream. Used to pin the indoor flicker to the camera's published/swept eye (retail settled ~tens of um vs acdream ~1.3mm).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Attached cdb to the live 2013 retail client at the Holtburg doorway + read the decomp.
The indoor flap is a STRUCTURAL divergence, settled by measurement (not inference):
- Retail has ONE render path: DrawInside(viewer_cell) every frame. NO inside/outside
branch (RenderNormalMode's outside branch is dead code; is_player_outside only gates
sky/lighting). "Entering a building" is not a render event — only the camera sweep
resolving a different viewer_cell. Same path before/after threshold -> no seam.
- Retail's eye JITTERS ~36um at rest yet membership is stable -> robustness is
STRUCTURAL: many small per-building floods (~7/frame, ~2 cells each, via terrain BSP
-> DrawPortal -> ConstructView(CBldPortal)), not one giant knife-edge flood.
- Our 3 divergences: (D1) invented inside/outside branch (GameWindow.cs:7498,
clipRoot = viewerRoot ?? _outdoorNode :7396); (D2) synthetic _outdoorNode; (D3) one
unified flood.
DECISION (user-approved): Option A — rip out branch + outdoor node, root always at the
real viewer_cell, one DrawInside, per-building rendering. Phased, conformance-tested,
visual-gated.
REFUTED by measurement (do not retry): bounded-propagation/churn (maxPop=1, 0/63k
reciprocals empty); byte-stable eye (retail's jitters ~36um — rest-snap cd974b2 failed +
regressed, reverted 9b1857a).
Lands the canonical exhaustive handoff for a FRESH session
(docs/research/2026-06-08-full-retail-render-port-OPTION-A-handoff.md), the CLAUDE.md
READ-THIS-FIRST banner, and reusable cdb apparatus. No project code changed; working tree
at the known-good baseline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Apparatus + handoff for the indoor flap. Confirmed (primary evidence): the flap is the
portal-flood clip being µm-sensitive at the threshold, driven by a ~1-8µm jitter in the
player RenderPosition (physics resting position not bit-stable; Lerp surfaces it). REFUTES
the 2026-06-07 see-through/EnvCell/outdoor-node diagnosis (ModelId GfxObj 0x01000A2B IS the
solid exterior) AND an enqueue-once attempt (retail propagates late slices via AddToCell;
the existing PropagatesNewSlicesToExit test caught it; reverted). Adds: Build determinism
test, A8CellAudit gfxobj dump, [pv-input] 6dp probe + [render-sig] outRoot/bshell fields.
No functional fix shipped. Next: higher-precision physics rest trace -> port retail
kill_velocity/contact rest-stability. Canonical: docs/research/2026-06-08-flap-rootcause-physics-rest-handoff.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Checkpoint of the unified retail-faithful indoor render. The two-week HANG/grey is fixed and the
interior seals (live-verified by the user). Commits the session render-rewrite foundation together
with the fixes that made it functional.
- HANG fix: PortalVisibilityBuilder.Build portal flood did not terminate (the faithful ProjectToClip
near-side clip drifts per round, defeating the CellView dedup; the BFS had no bound after U.2a removed
MaxReprocessPerCell). Fix = drift-tolerant snapped/canonical CellView.Add dedup (PortalView.cs) plus
restored MaxReprocessPerCell=16 bounded re-enqueue (PortalVisibilityBuilder.cs). Re-enqueue is kept
(load-bearing for late-slice propagation, Build_ViewGrowthAfterDoneCell_PropagatesNewSlicesToExit);
only its count is capped. CellViewDedupTests added.
- Seal (DrawCells Task 2): RetailPViewRenderer.DrawEnvCellShells draws EVERY visible cell via
IndoorDrawPlan.ShellPass (was gated on the ClipFrameAssembler slot filter, leaving slot-less cells grey).
- Look-in FPS: GameWindow exterior look-in candidates limited to the player landblock +-1 (was all ~81
loaded LBs iterated every outdoor frame). No behaviour change (far cells were >48m, already culled).
Remaining dominant issue = the FLAP at transitions: viewer-cell metastability (render roots at the
camera-eye cell, which oscillates outdoor-indoor as the 3rd-person boom drifts across the doorway,
confirmed in render-sig). SEPARATE fix, NOT the DrawCells port. Full handoff + flap fix plan + tracked
follow-ups (#78 terrain, look-in-from-inside, look-in FPS, L-spotlight):
docs/research/2026-06-07-indoor-render-session-handoff.md.
Baselines: build 0 err; App.Tests 210/210; Core.Tests 1331 pass / 4 fail (pre-existing) / 1 skip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diagnosis session: the indoor bluish void + grey/texture flicker is visibility metastability at cell boundaries, not a missing flood (R1's per-cell DrawInside is built; the cellar seals). Confirmed by named-retail decomp AND a live cdb capture of retail (viewer_cell rock-stable: clean monotonic transitions, zero oscillation across 4916 samples). Retail stays stable via boom stability + a 0.2mm viewer-cell dead-zone + clip-space portal clipping; acdream diverges on all three. Handoff documents the root cause, the cdb evidence, and the prioritized 3-part retail-faithful fix (boom stability -> dead-zone -> w-space clip) with decomp anchors + a planning/implementation kickoff prompt. Adds the reusable retail viewer-cell cdb capture script and the superseding CLAUDE.md banner.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
P2 / M1.5 "blocked at the last step" cellar-lip wedge. This session built a faithful
deterministic reproduction and peeled the cause through six evidence-disproven framings
to one bounded question. NO fix landed — the last layers were each disproven by evidence,
and guessing at the load-bearing collision code is the saga's failure mode.
Apparatus:
- CellarLipWedgeTests.cs + Fixtures/cellar-lip/ (3 real cell dumps + wedge-records.jsonl =
29 captured ACDREAM_CAPTURE_RESOLVE wedge calls). Replays the exact calls + body-before
through the lip-cell engine: all 29 reproduce at 0% advance in <200 ms. Tests are
documents-the-bug / diagnostics (GREEN while the wedge exists).
- TEMP probes ([path5-wall]/[fw-enter]/[find-walkable] in BSPQuery; [neg-poly]/[stepsphereup]/
[stepdown-decide]/CheckOtherCells cn/sn/negHit in TransitionTypes), gated on
ACDREAM_PROBE_INDOOR_BSP, marked STRIP. TransitionTypes neg-poly shortcut has a reverted-fix
comment (slide attempt didn't clear the wedge).
- tools/cdb/retail-*-trace.cdb (retail cdb traces).
Findings (handoff: docs/research/2026-06-04-p2-cellar-lip-flatfloor-cp-handoff.md, see the
"NEXT-SESSION KICKOFF" at top):
- Flat-floor contact plane is retail-faithful (v1 trace, full-file correlation). NOT the bug.
- PosHitsSphere cull sign is retail-faithful (cdb -z verified; the Binary Ninja `test ah,N; jp`
parity-jump reads inverted — caught + reverted a wrong fix from that mis-read).
- Sphere radius correct (0.48 player / 0.30 camera probe).
- Retail connector cell 0xA9B40175 never blocks (CEnvCell::find_collisions trace: 0 Collided/Slid).
- PINNED: during the step-up's step-down, BSPQuery.FindWalkableInternal is never called for cell
0171, so the cottage floor (poly 0x0023, Z=94) is never tested as walkable -> no contact plane
-> step-up fails -> StepUpSlide=Collided -> wedge.
Next: trace FindEnvCollisions -> FindCollisions path dispatch for 0171 during StepDown=true (why
StepSphereDown/find_walkable is skipped), port retail, validate via CellarLipWedgeTests, regress
DoorBugTrajectoryReplayTests + visual gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Live retail cdb trace (tools/cdb/cellar-corner-escape.cdb) of the Holtburg
cottage cellar-top corner decodes the ground truth: retail escapes by
step_sphere_up->step_up (196x vs 38 near-misses), transitioning the contact
plane from the ramp (N.z=0.78) onto the flat cottage floor (N.z=1.0, 76
landings). acdream slides at the lip and never makes that ramp->floor
transition -> the intermittent cellar wedge.
So the remaining cellar bug is the #98-core step-up-onto-cottage-floor
(DoStepDown / step_sphere_down / find_walkable), which the shipped B1 (abbd761)
+ slide_sphere (0935a31) fixes got close to but didn't finish. Door still
blocks; generic step-up climbs; cellar went always-stuck -> works-mostly.
Next (handoff doc): instrument acdream's OWN corner path (does step_up fire at
the lip and fail to land on the cottage floor?) before porting the lip-climb --
no guessing (#98 saga rule).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The P1 "doorway membership lags retail" premise is FALSIFIED. acdream's swept
ResolveWithTransition already matches retail's true per-frame curr_cell: the
production gate ProductionPath_IndoorCrossings reads 9/9 on the indoor 0170<->0171
crossings with NO code change, once fed an aligned retail golden.
Root cause of the false 0/11: CPhysicsObj::SetPositionInternal calls change_cell
(acclient_2013_pseudo_c.txt:283456) BEFORE set_frame writes m_position (:283458),
so the original golden (find-cell-list-capture.cdb, read at the change_cell BP)
paired each frame's NEW cell with the PREVIOUS frame's position — a one-frame skew.
Verified 3 ways: the decomp ordering; golden_picked[i] == geom(golden_position[i+1])
for all 22 rows; acdream's static pick == golden_picked[i-1] for all rows. Both
retail and acdream pick with center-only point_in_cell on global_sphere[0] (no XY
lead; cache_global_sphere @ pc:274196). curr_cell commits via validate_transition
(@ pc:272608, curr_cell = check_cell) = the find_cell_list pick, structurally
identical to acdream's RunCheckOtherCellsAndAdvance -> FindCellSet -> SetCheckPos.
There was nothing to port; a swept advance would make membership LEAD by a frame.
- tools/cdb/find-cell-list-capture-aligned.cdb: re-capture reads the committed
position from the set_frame that follows change_cell (cell+position same instant).
- Fixtures/find-cell-list-threshold.log: replaced with the aligned capture.
- ThresholdPortalCrossingReplayTests / FindCellListConformanceTests: rewritten from
documents-the-bug to assert retail truth (per-segment / per-indoor-pick equality).
- handoff + notes + README + memory: banners correcting the disproven premise.
Still open (NOT indoor membership, which is DONE): outdoor->indoor 0031<->0170 entry
conformance (needs landcell + building stab in the gate cache); master-plan cleanups
(delete CheckBuildingTransit, unify find_env_collisions, demote ResolveCellId) refactor
working retail-faithful code -> need explicit user approval.
Conformance 60 pass / 1 skip / 0 fail; full Core 1309 pass / 5 fail (pre-existing
2 BSPStepUp + 3 door-collision = P2) / 1 skip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
P0 Task 6 complete. Captured live retail membership at the 0031<->0170<->0171
doorway via cdb on CPhysicsObj::change_cell (symbol-driven; offsets verified by
discover-types.cdb; PDB MATCH). 22 transitions, clean monotonic sequence, NO
ping-pong (retail is correct-by-construction). Golden:
Conformance/Fixtures/find-cell-list-threshold.log.
ROOT-CAUSE FINDING (the central P1 work): retail transitions membership at the
PORTAL CROSSING (CEnvCell::find_transit_cells @ 0x52c820 pc:309968 — sphere crosses
the doorway polygon plane), while acdream's FindCellList re-picks by POINT-IN-CELL
containment at the foot. Retail commits room 0171 while the foot is STILL inside
vestibule 0170's BSP (in_0171=0); acdream lags. ALL 22 transitions diverge for this
one criterion mismatch — not a per-cell hysteresis or a building-entry-only split.
This is master-plan §0 'hysteresis gap' confirmed against the real client.
FindCellList_DoorwayThreshold_DivergesFromRetail_PendingP1 (documents-the-bug, GREEN)
+ ThresholdDivergenceDiagnosticTests (per-transition containment print) pin it; both
flip when P1 ports the directed portal crossing. Conformance 59 pass / 1 skip / 0 fail;
full Core 1308 pass / 5 fail (baseline) / 1 skip — no new failures.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
P0 Task 5. RetailTrace parses the [fcl] golden format (seed/pos/picked,
RetailCellPick); 4 TDD tests green. find-cell-list-capture.cdb targets
CPhysicsObj::change_cell (commit-on-diff) to capture retail's accepted
membership sequence at the doorway; README is the operator runbook
(dt offset verification + decode_retail_hex float decode). The live run
is P0's one user-gated step (Task 6 mines existing traces first).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A8CellAudit portals now dumps each cell's local AABB. Real flap cells: 0171 local
y in [-7.65, 1.15], 0170 in [-8.61, -7.65]; the 0171->0170 portal plane is at
y=-7.65 (0171's MIN boundary), no overlap. So an eye genuinely inside 0171 always
has side-test D<=0 -> always traverses 0171->0170; the side test cannot cull 0170
while the eye is in 0171. The flap therefore requires the eye OUTSIDE 0171 while
root is still 0171 (cache/grace/3rd-person camera) -> a camera-cell-resolution
issue, not the side test (H2, disproven) and not the per-frame PVS set (H1, in
doubt). Mechanism still unconfirmed -> needs a live eye-pos capture. Stale H2
conclusion in the characterization note corrected with a banner.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
InitCell decode (PortalFlags.PortalSide=0x2) + a swept-pose A8CellAudit comparison
(O=centroid, A=winding-corrected PortalSide, B=opposite) over the real flap cells.
A is IDENTICAL to O at every pose/every portal — the (Flags&2)==0 boolean convention
makes the dat PortalSide sense equal to our centroid sense, so swapping is a no-op
and cannot fix the flap. B culls true-interior poses (wrong polarity). Conclusion:
the flap is NOT the side-test sense — it's the 3rd-person camera eye crossing an
interior portal plane while FindCameraCell still roots in the cell; ANY plane-side
test culls there. No production code changed (no no-op shipped).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A8CellAudit portals dump extended to print per-portal plane + centroid-derived
InsideSide vs the dat's authored PortalSide. Real Holtburg cottage cells show:
the flap is a DIRECT 0xA9B40171->0xA9B40170 portal side-test flip (0170 is a
direct neighbour, not multi-hop), and our centroid-derived InsideSide is
anti-correlated with the dat PortalSide that retail InitCell (432896) uses.
Evidence selects H2 (port the side test) over H1 (PVS set-grounding). Camera
cell 0171 seenOutside=Y. Full reading + fix direction + open sign question in
the note.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First-fix from the visual-gate-failure handoff: an empty OutsideView means
"no outdoors visible from here," not "all outdoors." When inside a building
with an empty clipped mask, Step 4 now draws NO terrain/scenery instead of
disabling the stencil and flooding ungated terrain over the cell interior
(the Step-3 walls already occupy the framebuffer). Visual-confirmed: Holtburg
cottage cellar walls are solid now, no terrain bleed-through.
Also adds portal diagnostics that root-caused so-called "Bug B":
- PortalVisibilityBuilder: per-camera-cell CAMPORTAL census (polyLen +
side-test result) emitted BEFORE the BFS guards, so an empty OUTSIDEVIEW
can be traced to the exact gate.
- A8CellAudit `portals`: replicate BuildLoadedCell's polygon-vertex
resolution so PortalPolygons[i] validity is checkable offline.
Finding: the builder is largely CORRECT — it produces narrowed clipped
OutsideView regions for most cells (0172/0173/0162/015E/0165/016F). The
empty cases are mostly legitimate (windowless cellar can't see out; the
3rd-person camera eye on the outdoor side of a front-door plane culls that
exit). The handoff's Finding 2 ("under-produces, never narrows") is
substantially not real. Remaining wall-missing regressions in OTHER
buildings live in the cross-building Step-5 enforcement, escalated separately.
All gated behind ACDREAM_A8_INDOOR_BRANCH=1; default play unaffected.
App tests 108/108.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Lands the working A8 indoor-rendering and streaming fixes accumulated this
session. User has verified these visually to some degree (e.g. lifestone /
translucent meshes confirmed fine under the FrontFace flip; bridge / wall /
collision regressions confirmed fixed after travel); not every path has been
exhaustively gated. The cellar-flap defect remains OPEN and will be solved
the retail-faithful way via a dedicated brainstorm (see handoff docs).
Rendering core (reviewed, high confidence):
- EnvCellRenderer SSBO stride fix: upload packed Matrix4x4[] (64B) instead of
the 80B CPU InstanceData struct the shader never expected — fixes the
transform/texture "explosion" for any draw with >1 instance (cells that
dedupe to a shared cellGeomId). Real root cause.
- WB-style global FrontFace(CW) + per-batch CullMode carried through the MDI
layout (GroupKey + BuildIndirectArrays + DrawIndirectRange split into
same-cull runs with absolute uDrawIDOffset per run).
- EntitySet partitioning (IndoorPass / OutdoorScenery / LiveDynamic) +
WorldEntity.BuildingShellAnchorCellId so building shells scope to their
dat-derived building cell instead of rendering everywhere.
- RenderOutsideInAcdream (look into buildings from outside) +
CollectVisiblePortalBuildings frustum cull of portal bounds.
- Sky-when-inside-building + per-cell audit probe + GL-state probe.
Streaming / perf (test-covered; not independently code-reviewed this session):
- Near/far priority queues so near work wins over far; PromoteToNear carries
full landblock + mesh data; LandblockEntriesWithoutAnimatedIndex avoids
rebuilding the animated-lookup dict in the hot draw path. Fixes the
bridge-not-appearing / missing-walls / broken-collision-after-travel
regressions and improves post-transition FPS.
Tooling + docs:
- tools/A8CellAudit: offline dat cell/portal/building dumper (portals +
buildings modes) — reproduces the cellar-flap investigation with no launch.
- docs/research cellar-flap root-cause + option-2 handoff (the didInsideStencil
double-duty finding + the WB-recursive design decision + brainstorm prompt),
entity-taxonomy, replan, issue-78 visibility investigation.
Diagnostics retained on purpose: ACDREAM_A8_DIAG_* gates, portal_stencil.vert
provisional pos.w clamp, and the probe families are kept (env-var gated, zero
cost when off) because the pending option-2 cellar-flap brainstorm needs them.
Strip in the option-2 ship commit.
Indoor branch stays behind ACDREAM_A8_INDOOR_BRANCH=1 (default off = pre-A8
visual). Build green; App tests + Core (streaming/dispatcher/loader) tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
over-penetration-capture.jsonl is 3 records extracted from
door-stuck-capture.jsonl: the cell-crossing over-penetration tick
(0xA9B4013F -> 0xA9B40150, sphere committed 0.27m INTO slab), a
stuck-position hit=yes tick, and a stuck-position hit=no tick. Drives
the A6.P5 replay tests that prove the cellSet gate removal closes both
the over-penetration and the intermittent-visibility bugs.
extract-records.ps1 is a one-shot extractor; reusable if we capture more.
Source captures (door-stuck-*.jsonl, door-stuck-*.launch.log) gitignored.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cdb attached to retail at a Holtburg cottage door while user walked the
inside-out off-center scenario. Three trace iterations identified that
retail's collision-recording happens via SPHEREPATH::set_neg_poly_hit
(fires hundreds of times during inside-out walk), NOT via the more
obvious-named COLLISIONINFO setters (which fire 0 times). Apparatus
scripts at tools/cdb/door-inside-out-v[1-3].cdb + symbol-probe.cdb.
Our codebase has NegPolyHitDispatch defined but never called. The
downstream TransitionalInsert NegPolyHit handler was a stub. Two-part
fix landed:
1. BSPQuery.FindCollisions Path 5 (Contact branch) restructured —
distinguishes full hit (hit0 == true → StepSphereUp) from near-miss
(hit0 == false but hitPoly0 != null → NegPolyHitDispatch). Mirrors
retail BSPTREE::find_collisions at
acclient_2013_pseudo_c.txt:0053a630-0053a6fb.
2. Transition.TransitionalInsert NegPolyHit handler — dispatches to
step_up + step_up_slide (NegStepUp=true) or records collision
normal + returns Collided (NegStepUp=false). Mirrors retail
CTransition::transitional_insert at
acclient_2013_pseudo_c.txt:0050b7af-0050b7e6.
Tests: all 11 fix-relevant + regression tests pass including issue #98.
VISUAL VERIFICATION (user-driven inside-out off-center): still squeezes
through. Diagnostic [neg-poly-dispatch] probe shows ZERO hits in
production. The Path 5 restructuring doesn't surface NegPolyHit
because our SphereIntersectsPolyInternal only sets hitPoly on FULL
hits — retail's sphere_intersects_poly sets var_5c (closest polygon)
even on near-misses via BSP-traversal side effect.
Remaining fix (next session): add near-miss polygon recording to
SphereIntersectsPolyInternal. Once it sets hitPoly on near-miss BSP
traversal, the Path 5 NegPolyHit dispatch (this commit) will fire
and the TransitionalInsert handler (this commit) will block.
Full handoff with cdb trace table + next-step plan:
docs/research/2026-05-25-door-bug-cdb-retail-trace-findings.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step 4 of the apparatus plan. Adds the cdb script + runner that pairs
with Issue98CellarUpReplayTests to compare retail's walkable-query
behavior against acdream's during the Holtburg cottage cellar ascent.
Breakpoints (all symbols verified against refs/acclient.pdb via grep
docs/research/named-retail/symbols.json):
- BPA: BSPLEAF::find_walkable — leaf-level walkable query
- BPB: CPolygon::walkable_hits_sphere — per-polygon overlap test
- BPC: CPolygon::find_crossed_edge — per-polygon edge containment
- BPD: CTransition::check_other_cells — outer dispatcher
- BPE: COLLISIONINFO::set_contact_plane — GOLD signal: retail accepted
this plane
- BPF: CPolygon::adjust_sphere_to_plane — per-polygon projection
Output format: 32-bit hex bits for all floats via dwo() + %08X (cdb's
%f handling is broken for dwo reads; see a6-probe.cdb v3→v4 history).
Decoder: tools/cdb/decode_retail_hex.py already handles _h=0x... fields.
Auto-detach threshold: 50000 hits across BPA/B/C/D/F. BPE is unbounded
(contact plane writes are rare, ~18 per ascent per slice 5 capture).
Runner: tools/cdb/issue98-runner.ps1
.\tools\cdb\issue98-runner.ps1 -ScenarioTag "cellar_up_attempt_1"
Prereqs (per CLAUDE.md retail debugger toolchain section):
- Retail acclient.exe v11.4186 running and in-world
- ACE running on 127.0.0.1:9000
- Character at the BOTTOM of a Holtburg cottage cellar stair
- cdb.exe present at the Windows Kits 10 path
Output:
docs\research\2026-05-23-a6-captures\<ScenarioTag>\retail.log
Reading the log:
- [BPE] lines tell you which plane retail accepted (the answer we need).
- Cross-reference [BPE]'s normal/d against the cell fixtures in
tests/AcDream.Core.Tests/Fixtures/issue98/*.json to identify which
cell + polyId retail picked.
- The divergence between retail's accepted polygon and our replay test's
"no walkable accepted" result IS the fix target.
The capture itself is a user action (cdb requires a live retail
process); this commit only ships the protocol. Step 5 (comparison doc)
follows after the capture lands.
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>
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>
Replaces v1's broken-offset BP actions with PDB-authoritative field
reads. All offsets extracted from `dt acclient!TYPENAME` against the
loaded PDB (output preserved at tools/cdb/a6-types-dump.txt).
Key offsets:
Plane.N at +0x00, .d at +0x0c
CSphere.center at +0x00, .radius at +0x0c
CPolygon.plane at +0x20
SPHEREPATH.collide +0x104, .walkable_allowance +0x1b8, .walk_interp +0x1bc
CTransition.sphere_path +0x020 (so e.g. CTransition+0x174 = insert_type)
Per-BP arg-read fixes (all use __thiscall: ecx=this, args at [esp+N]):
BP1: substeps from [esp+4], insertType from this+0x174
BP2: walkable_allowance from this+0x1d8, normal.z from *(arg+8)
BP3: normal.x/y/z from *arg
BP4: collide+insertType via *(arg2+0x124/0x174), walkAllow from arg3
BP5 (the over-correction suspect): full plane + sphere + walk_interp +
movement vector. 12 fields, all double-indirect for pointer args.
BP6 SYMBOL FIXED: CTransition::check_walkable (v1 had
validate_walkable which doesn't exist; check_walkable confirmed
in symbols.json and at decomp line 272811).
BP7: plane + isWater from *arg.
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>
Documents prerequisites (PDB match, cdb install, retail+ACE
running), per-scenario invocation, the 9-scenario tag table, and
the parallel acdream capture command. Includes the CLAUDE.md cdb
watchouts inline so probe operators don't have to chase them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wrapper that attaches cdb to a live retail acclient.exe with a
scenario-tagged log path. Per-scenario invocation:
.\tools\cdb\a6-probe-runner.ps1 -ScenarioTag "scen1_inn_doorway"
Output: docs\research\2026-05-21-a6-captures\<ScenarioTag>\retail.log
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sets non-blocking breakpoints on transitional_insert, step_up,
set_collide, find_collisions, adjust_sphere_to_plane,
validate_walkable, set_contact_plane. Each BP increments a counter
and emits a single printf line. Auto-detach via qd at 50K total
hits to avoid retail lag (CLAUDE.md gotcha — high BP rates trigger
ACE timeout).
Also adds !tools/cdb/*.cdb negation to .gitignore so committed
reference scripts in tools/cdb/ are tracked despite the blanket
*.cdb scratch-file rule.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five parallel agents + dat probes ruled out:
- byte-level decode primitive (matches ACViewer)
- polygon emission (no ST_DOUBLE / Surface.Type & 6 issues)
- per-PART texture-override scoping (correctly per-MeshRef'd)
- SubPalette indexing convention (full-size 2048 palettes, *8 wire un-pack
is single-applied)
Smoking gun: for +Acdream the server sends 10 SubPaletteSwap ranges that
overlay palette indices [0..320), [576..1024), [1392..1488), [1728..1920).
The complement — [320..576), [1024..1392), [1488..1728), [1920..2048) —
is NOT overlaid. Base palette 0x0400007E at those indices has
red/skin tones. Coat texture UVs sampling those non-overlaid indices
render as visible "skin stub at top of coat".
Either ACE sends incomplete SubPaletteSwap data, or retail does extra
client-side ClothingTable computation we (and ACE) don't.
Diagnostic harness now lives at tools/InspectCoatTex/Program.cs;
GameWindow's DUMP_CLOTHING also probes runtime SubPalette dat sizes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds five diagnostics, no behavior changes. All gated on existing
ACDREAM_REMOTE_VEL_DIAG=1 env var. Plan at
~/.claude/plans/yes-make-a-plan-parsed-axolotl.md.
Five hypotheses surviving from the four-agent investigation
(docs/research/2026-05-03-remote-anim-cycle/investigation-prompt.md):
H1 SEQSTATE silently swallowed by OMEGA_DIAG sharing throttle clock
H2 ApplyServerControlledVelocityCycle races UM-driven SetCycle per UP
H3 SetCycle fast-path returns without updating _currNode
H4 GetLink/GetCycle null → defensive fallback lands on stale head
H5 PartTemplate.Count diverges from anim PartFrames.Count → silent
identity-quat freeze
Diagnostics added (all log lines are grep-prefixed):
D1 Split LastSeqStateLogTime field for SEQSTATE — own throttle.
Foundational: every other diag depends on SEQSTATE telling truth.
D2 [UPCYCLE] inside ApplyServerControlledVelocityCycle, +
[UPCYCLE_SRC] at the call site (wire vs synth velocity).
D3 [SCFAST] in fast-path return, [SCFULL] at full-rebuild end.
D4 [SCNULLFALLBACK] in the null-data defensive fallback.
D5 [PARTSDIAG] with pt.Count / seqFrames.Count / setup.Parts.Count /
anim.PartFrames[0].Frames.Count + sum-of-components hash.
Repro recipe:
$env:ACDREAM_INTERP_MANAGER = "1"
$env:ACDREAM_REMOTE_VEL_DIAG = "1"
dotnet run … 2>&1 | Tee-Object tools/diag-logs/walkrun-<ts>.log
Then watch a retail-driven character through acdream and exercise:
idle → W run → release → shift+W walk → release → demote → promote →
run+turn (this last one is the H1 trap).
Decision matrix in the plan file maps each [TAG] signature to a
specific Commit B fix.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When AnimationSequencer.SetCycle transitions between forward-locomotion
cycles (Walk↔Run, Walk↔WalkBackward, etc.) — i.e. when both old and new
motion's low byte is in {0x05 WalkForward, 0x06 WalkBackward, 0x07
RunForward} — do a full queue drain + _currNode/_firstCyclic reset
(matching the existing skipTransitionLink branch) instead of just
ClearCyclicTail. Without this, _currNode is left pointing into the
previous cycle's non-cyclic head (link frames from the prior Ready→walk
transition), and the visible legs continue playing those head frames
before reaching the new run cycle.
Investigation findings (cdb live trace of retail at
tools/cdb-scripts/walk_run_motion_trace.log):
Retail's actual approach is "additive add_to_queue with no truncate" —
MotionTableManager handles the natural progression via per-tick
CheckForCompletedMotions / remove_redundant_links cleanup. Acdream
doesn't have that machinery, so this fix is the closest viable
emulation: force the queue back to a clean state and rebuild from
scratch on the locomotion-cycle transition.
User-reported symptom this addresses (walk→run direct transition,
release shift while W held): visible animation cycle did not switch
until next motion event. Verified via FWD_WIRE + SETCYCLE diags that
both ACE and our SetCycle are firing correctly on the transition.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Live cdb trace of retail acclient.exe (v11.4186, PDB-matched) capturing
the exact function call sequence for a direct walk-to-run motion
transition where the user holds shift+W (walk) then releases SHIFT
while still holding W (transition to run).
Trace bps on:
- CPhysicsObj::DoInterpretedMotion (0x0050EA70)
- CPartArray::DoInterpretedMotion (0x00518750)
- MotionTableManager::PerformMovement (0x0051C0B0)
- MotionTableManager::add_to_queue (0x0051BFE0)
- MotionTableManager::truncate_animation_list (0x0051BCA0)
- CMotionTable::DoObjectMotion (0x00523E90)
- CMotionTable::StopObjectMotion (0x00523EC0)
Captured trace at tools/cdb-scripts/walk_run_motion_trace.log shows
the precise walk-to-run sequence:
[79] CPhysicsObj::DoInterpretedMotion: motion=45000005 walk start
[82] CMotionTable::DoObjectMotion: motion=45000005
[83] MotionTableManager::add_to_queue: arg1=45000005 arg2=00000001
[89] CPhysicsObj::DoInterpretedMotion: motion=44000007 run start
[92] CMotionTable::DoObjectMotion: motion=44000007
[93] MotionTableManager::add_to_queue: arg1=44000007 arg2=00000001
[104] CMotionTable::StopObjectMotion: motion=44000007 run end
Critical structural finding for #L.4-walk-run:
Retail does NOT call truncate_animation_list during the walk→run
transition. truncate_animation_list never fires in the entire 200-hit
trace. Retail also does NOT call StopObjectMotion(WalkForward) before
add_to_queue(RunForward). Retail just appends the new motion to the
queue and lets MotionTableManager (and its CheckForCompletedMotions /
remove_redundant_links per-tick cleanup, not yet traced) handle the
natural progression.
acdream's AnimationSequencer.SetCycle aggressively calls
ClearCyclicTail() at line 430 BEFORE enqueuing the new cycle, which
destroys the in-flight walk cycle's frames. The new run cycle is
enqueued but _currNode is left in a state that doesn't smoothly
continue — visible to the user as "it just blips forward walking,
AS SOON as press another key like turning, its starts running"
(the next motion event re-fires SetCycle which finally aligns state).
Fix is a structural refactor of SetCycle to mirror retail's
"additive queue with auto-cleanup" semantics. Out of scope for this
research commit; filed as #L.4 in the next ISSUES.md entry.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three intertwined changes from a single investigation session driven by
attaching cdb to a live retail acclient.exe (v11.4186, Sept 2013 EoR
build) and tracing what retail actually DOES on the steep-roof wedge
scenario the user reported in acdream.
═══════════════════════════════════════════════════════════
1. L.5 — physics-tick MinQuantum gate (PlayerMovementController)
═══════════════════════════════════════════════════════════
Retail's CPhysicsObj::update_object subdivides per-frame dt into 1/30 s
sized integration steps and SKIPS entirely when accumulated dt is below
MinQuantum. Live trace evidence:
update_object = 40,960 calls
UpdatePhysicsInternal = 25,087 calls (61%)
i.e., 39% of update_object calls return early via the MinQuantum gate.
Retail's effective physics tick rate is 30Hz even at 60+ Hz render.
acdream's PlayerMovementController bypassed the existing PhysicsBody.
update_object and called UpdatePhysicsInternal(dt) directly each render
frame, which compressed bounce-energy / gravity-tangent accumulation
into half the time and amplified our steep-roof wedge dynamics.
Fix: add `_physicsAccum` accumulator. Integrate only when accumulated
dt ≥ MinQuantum (clamped to MaxQuantum to bound stale-frame jumps).
HugeQuantum drops accumulated time to discard truly stale frames
(debugger break, GC pause). Render still runs at full rate; only the
physics step is gated.
═══════════════════════════════════════════════════════════
2. Phase 3 reset retail-faithful kill_velocity (TransitionTypes)
═══════════════════════════════════════════════════════════
Retail's reset path (acclient_2013_pseudo_c.txt:273231-273239) gates
kill_velocity on `last_known_contact_plane_valid`:
if (last_known_valid == 0) {
set_collision_normal(step_up_normal); return COLLIDED;
}
kill_velocity(this);
last_known_valid = 0;
return COLLIDED;
Earlier in this session I deviated to "unconditional kill_velocity" as
a hypothesis-driven wedge fix. The live trace then showed the
deviation CAUSED a different wedge by zeroing V every frame, leaving
the body with no tangent momentum to escape (V = (0,0,0) for 169
consecutive frames while position pre/resolved frozen). The retail-
faithful gate is restored.
Note: the gate rarely fires in normal airborne play because our L.2.4
proximity guard clears last_known_valid soon after the body separates
from its remembered floor. Live retail trace also showed
kill_velocity = 0 hits over an entire play session — same behavior. So
acdream's kill_velocity is correct as ported now.
The supporting ObjectInfo.VelocityKilled flag + StopVelocity wiring +
PhysicsEngine.ResolveWithTransition consumer that actually zeros
body.Velocity when the flag is set — these were a no-op stub before
this session and are now correctly wired. Retail anchor:
OBJECTINFO::kill_velocity → CPhysicsObj::set_velocity({0,0,0}, 0) at
acclient_2013_pseudo_c.txt:274467-274475.
═══════════════════════════════════════════════════════════
3. Retail debugger toolchain (#35)
═══════════════════════════════════════════════════════════
When the question is "what does retail actually DO at runtime?" — not
"what does retail's code SAY" — the decomp at docs/research/named-retail/
is invaluable but doesn't capture state interactions across frames.
This commit ships infrastructure to attach Windows' cdb.exe to a live
retail acclient.exe with full PDB symbols and capture state at any
breakpoint.
- tools/pdb-extract/check_exe_pdb.py — reads any PE's CodeView entry
and reports MATCH / MISMATCH against refs/acclient.pdb's GUID.
Always run before attaching cdb. The matching v11.4186 build's
GUID is 9e847e2f-777c-4bd9-886c-22256bb87f32.
- tools/pdb-extract/dump_pdb_info.py — dumps a PDB's expected
build timestamp + GUID + age. Used to figure out which acclient.exe
build pairs with our PDB.
CLAUDE.md gets a Step -1 in the development workflow ("ATTACH cdb
TO RETAIL when behavior is the question, not code") and a full
"Retail debugger toolchain" section with the workflow, sample .cdb
script structure, and watchouts (PDB names use snake_case for some
classes / PascalCase for CPhysicsObj; ; is cdb's command separator;
killing cdb kills the debuggee; high-hit-rate breakpoints lag the game).
memory/project_retail_debugger.md captures the workflow + key findings
so future sessions inherit the toolchain by reading project memory.
═══════════════════════════════════════════════════════════
4. BSPQuery Path 6 slide-tangent restored (b1af56e behavior)
═══════════════════════════════════════════════════════════
After this session's retail-strict experiments showed that retail-
faithful Path 6 (SetCollide + Phase 3 reset chain) produces a
"lands on roof in falling animation, can't slide off" half-state in
acdream — because our acdream port of step_up_slide / cliff_slide is
incomplete for grounded-on-steep movement — the L.4 slide-tangent
deviation from commit b1af56e is restored as the pragmatic ship state.
The deviation: when an airborne sphere hits a polygon whose normal Z
is below FloorZ (≈ 0.6642, slope > ~49°), project the move along the
steep face to remove the into-wall displacement, set CollisionNormal +
SlidingNormal, return Slid. Body never gets ContactPlane on the steep
poly, never gets the half-state, slides off the slope under gravity's
tangent contribution.
Retail-strict requires the deeper step_up_slide / cliff_slide audit
(filed under #32). Until that lands, slide-tangent is the right
deviation — produces user-acceptable "slide off the roof" behavior.
═══════════════════════════════════════════════════════════
Test status: 833/833 green.
Refs:
acclient_2013_pseudo_c.txt:283950 (CPhysicsObj::update_object)
acclient_2013_pseudo_c.txt:273231-273239 (Phase 3 reset path)
acclient_2013_pseudo_c.txt:274467-274475 (OBJECTINFO::kill_velocity)
acclient_2013_pseudo_c.txt:323783-323821 (BSPTREE::find_collisions Path 6)
Closes#35. Updates #32 with L.4/L.5 status.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Independent code review by an external agent (2026-04-27) flagged
that SkyRenderer.EnsureMeshUploaded only ever called
_dats.Get<GfxObj>(...) — every 0x020xxx Setup ID returned null and
got cached as an empty submesh list, silently dropping every
Setup-backed sky object across the Dereth Region. In Rainy DG3
alone that's 6 dropped SkyObjects (0x02000714, 0x02000BA6 ×2,
0x02000588 ×4, 0x02000589 ×3 across various time-of-day windows).
Verbatim from retail's CelestialPosition struct at acclient.h:35451:
struct CelestialPosition {
IDClass<...> gfx_id;
IDClass<...> pes_id; // particle scheduler
float heading; float rotation;
Vector3 tex_velocity;
float transparent; float luminosity; float max_bright;
unsigned int properties;
};
Per the named retail decomp, CPhysicsObj::InitPartArrayObject (decomp
~280484) dispatches gfx_id by type prefix: type 6 → direct GfxObj,
type 7 → Setup via CPartArray::CreateSetup (decomp ~287490) which
walks Setup.Parts. Mirror that here: detect 0x020xxxxx in
EnsureMeshUploaded, route to a new EnsureSetupUploaded helper that
flattens via SetupMesh.Flatten (existing Phase-2 utility) and bakes
each part's transform into the vertex positions before upload.
Sky setups don't animate in any way that affects the static-mesh
visual we render here.
Probe extension: also added the Diffuse column to RainMeshProbe's
sky-surface audit so the (Type, Translucency, Luminosity, Diffuse)
quadruple is visible on every flag-bit row.
Visual impact at verification launch: not observable. The Setup
objects in Rainy DGs appear to be tiny placeholder meshes existing
mainly to anchor PES emitters. The dynamic "aurora-like" sheen the
user observes in retail comes from the PES particle layer, which
remains unimplemented (issue #28). Keeping this fix because the
geometry path is now decomp-correct and provides foundation for
the eventual PES wiring.
Issue #29 filed for the residual cloud-density gap. 1227 tests pass.
Added per-Surface dump that decodes Type bits and prints whether the
LUMINOUS (0x40) flag is set on each. Targets all 27 sky surface IDs
referenced by Holtburg's Region — every dome variant (0x010015EE/F0/F1/F2),
the inner sky/star sheet (0x010015EF), sun (0x01001F67/0x01001348), moon
(0x01001F6A), every cloud variant (0x01004C35..0x01004C3A, 0x010015B6),
and rain (0x01004C42/0x01004C44 — control row).
Result: zero of the 27 surfaces have the LUMINOUS bit set. The previous
SkyRenderer comment that claimed dome+clouds carried the bit was wrong;
the differentiator between "self-lit texture passthrough" and
"ambient+diffuse-tinted" sky meshes is purely the Surface.Luminosity
FLOAT (1.0 dome/sun/moon, 0.0 stars/clouds, 0.1484 rain). This fed
directly into the emissive-default fix in the next commit.
Bonus finding: cloud surface 0x08000023 has Translucency=0.25 (not 0)
which the Translucency plumbing fix in the next commit will also pick
up — clouds will render at 75% opacity, matching retail's curr_alpha
derivation (D3DPolyRender::SetSurface at 0x59c767).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sibling of StarsProbe/WeatherEnumerator. Targets GfxObjs 0x01004C42 and
0x01004C44 (the two rain cylinders). For each: dumps the Surface raw
record (Type bits, Translucency, Luminosity, Diffuse, ColorValue,
OrigTextureId), every polygon's SidesType + Stippling + hasPos/hasNeg
emission flags (mirroring GfxObjMesh.Build's neg-side rule), and the
final GfxObjMesh.Build() submesh+index counts.
Built per independent code-review §5: "Run one targeted probe... if one
cylinder has more than 48 indices per side-equivalent, fix the
duplicate-side/cull behavior together with the surface-opacity uniform."
Probe results (rain_mesh_probe.log, not committed):
Surface 0x080000C5: Type=0x10112 (Base1Image|Translucent|Alpha|Additive),
Translucency=0.5000, Luminosity=0.1484, OrigTextureId=0x050016A6.
Polygons: all 8 are Stippling=Positive, SidesType=None, hasNeg=False.
Build output: 1 submesh, 24 verts, 48 indices = 8 walls × 2 tris × 3.
→ SINGLE-SIDED (the duplicate-side hypothesis is disconfirmed).
Confirmed: the rim brightness excess is purely from Translucency not
being plumbed (acdream draws rain at full alpha=1.0 instead of retail's
0.5). Bonus finding: surface.Luminosity=0.1484 is also ignored by the
renderer's `effEmissive = (luminosity > 0) ? luminosity : sub.SurfLuminosity`
fallback (the local `luminosity` defaults to 1.0 so the fallback never
fires) — but that's keyed on the LUMINOUS flag bit (0x40), which the rain
surface does NOT have. Filed as follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>