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>
63 KiB
acdream — known issues + small deferred features
Rolling tactical list. What goes here:
- Bugs: user-visible defects we've observed but haven't fixed yet.
- Small deferred features: work that fits in one or two commits. Anything larger should be a named Phase in the roadmap.
What does NOT go here:
- Large multi-commit work → add a Phase to the roadmap instead.
- Ideas / wishlist →
docs/plans/. - Design questions → open a
docs/research/*.mdnote.
Conventions
- Sequential integer IDs (
#1,#2, …). Commits that close an issue reference the ID in the message (e.g.fix #3: periodic TimeSync parsing). StatusisOPEN,IN-PROGRESS, orDONE. DONE items move to the Recently closed section at the bottom with closed-date + commit SHA.- Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending.
- Promoting to a Phase: mark as
DONE (promoted to Phase X)+ commit SHA where the Phase entry landed.
Template
Copy this block when adding a new issue:
## #NN — Short title
**Status:** OPEN
**Severity:** HIGH | MEDIUM | LOW
**Filed:** YYYY-MM-DD
**Component:** e.g. sky, physics, net, ui
**Description:** One paragraph — what's wrong or what's missing.
**Root cause / status:** What we know so far. Empty if unknown.
**Files:** Path references with approximate line numbers.
**Research:** Links to `docs/research/*.md` if applicable.
**Acceptance:** How we'll know it's fixed.
Active issues
#39 — Run↔Walk cycle transition not visible on observed player remotes (acdream-as-observer)
Status: OPEN Severity: MEDIUM (visible animation desync; not a correctness/wire bug) Filed: 2026-05-03 Component: physics / motion / animation
Description: When observing a remote-driven player character through acdream and the actor toggles Shift while keeping a direction key held (Run↔Walk demote/promote), the visible leg cycle does NOT update on the observer side. Body position eventually corrects via UpdatePosition hard-snaps (causing visible position blips), but the animation cycle stays at whatever it was last set to (Run sticks; Walk sticks).
Observation matrix:
| Observer | Actor | Cycle Run↔Walk | Z on slopes |
|---|---|---|---|
| Retail | Retail | ✓ | ✓ |
| Retail | Acdream | ✓ | ✓ |
| Acdream | Acdream | ✓ | ✗ (only with env-var path) |
| Acdream | Retail | ✗ | ✗ |
Root cause / status:
ACE only broadcasts a fresh UpdateMotion (UM) when the wire's
ForwardCommand byte changes — i.e. on direction-key state changes
(W press, W release). Toggling Shift while W is held changes
ForwardSpeed and HoldKey but NOT ForwardCommand, so ACE does
NOT broadcast a UM for the demote/promote. The speed change DOES
propagate via UpdatePosition (position-delta velocity changes
between Run-pace and Walk-pace), confirmed via [VEL_DIAG]
serverSpeed varying ~2.5 m/s (walk) ↔ ~9 m/s (run).
Retail's inbound code uses UP-derived velocity to refine the visible
cycle when no UM tells it. Acdream has the equivalent function —
ApplyServerControlledVelocityCycle in GameWindow.cs:3274 — but
it's gated if (IsPlayerGuid(serverGuid)) return; for player
remotes, exactly the case where the gap matters.
(Earlier hypothesized as H2 in the 2026-05-03 four-agent investigation but marked refuted because the [UPCYCLE] diag never fired — that was BECAUSE of the gate; un-gating reveals it firing per UP, which is the correct behavior.)
Fix sketch (~10 lines): un-gate ApplyServerControlledVelocityCycle
for player remotes when currentMotion is a locomotion cycle
(Run/Walk/Sidestep/Backward). UMs still drive direction-key changes
authoritatively; UP-derived velocity refines the speed bucket within
the same direction. Add a LastUMUpdateTime grace window (e.g.
500ms) so UMs win when fresh.
Files:
src/AcDream.App/Rendering/GameWindow.cs:3274—ApplyServerControlledVelocityCycle(the gateif (IsPlayerGuid(serverGuid)) return;to remove with conditions)src/AcDream.App/Rendering/GameWindow.cs:3640-3660— call site (already passes through with HasServerVelocity from synthesized UP-deltas)src/AcDream.Core/Physics/ServerControlledLocomotion.cs:54-76—PlanFromVelocitythresholds (may need re-tuning if banding is observed)
Research:
docs/research/2026-05-03-remote-anim-cycle/investigation-prompt.md— full background of the four-agent investigation- This session's diagnostic logs at
tools/diag-logs/walkrun-A1b-*.log(UM_RAW, FWD_WIRE, SETCYCLE traces) confirming ACE's wire pattern
Acceptance:
- Observer in acdream watching a retail-driven character toggle Shift while holding W: visible leg cycle switches Run↔Walk within ~200ms of the wire change.
- No regression on the working cases (acdream-on-acdream, retail observers, idle↔Run, idle↔Walk).
- No spurious cycle thrashing during turning while running (ObservedOmega doesn't trigger velocity-bucket changes).
#40 — ACDREAM_INTERP_MANAGER=1 env-var path regressed (staircase + blips)
Status: OPEN (do-not-enable; pending L.3 follow-up rebuild) Severity: N/A (gated; default behavior unaffected) Filed: 2026-05-03 Component: physics / motion (per-tick remote prediction)
Description: The ACDREAM_INTERP_MANAGER=1 per-frame remote tick
introduced by commit e94e791 (L.3.1+L.3.2 Task 3) is a regression and
should not be enabled. Two visible symptoms:
-
Z staircase on slopes: observed remotes running up/down hills sink into rising terrain or float over receding terrain, then snap to correct Z at each
UpdatePositionarrival. Body never follows the terrain mesh between UPs. -
Position blips during steady-state motion: XY drifts unconstrained between UPs, then UP hard-snaps cause visible jumps.
Both symptoms ABSENT when env-var unset (default legacy path).
Root cause: the env-var path was designed to mirror retail
CPhysicsObj::MoveOrTeleport (acclient @ 0x00516330). MoveOrTeleport
is retail's network-packet entry point — minimal work. The per-frame
physics tick is retail's update_object (FUN_00515020) — full chain
including apply_current_movement → UpdatePhysicsInternal →
Transition::FindTransitionalPosition (collision sweep). The legacy
path mirrors update_object correctly. The env-var path stripped the
collision sweep on a wrong assumption that this was "more retail-
faithful" — it was the opposite.
Commit B (039149a, 2026-05-03) ported ResolveWithTransition into the
env-var path, but the symptom persisted because the env-var path also
clears body.Velocity for grounded remotes (no Euler integration of
horizontal motion → sweep input is the catch-up offset only, which
itself stair-steps because UPs are sampled at ~1 Hz).
Files:
src/AcDream.App/Rendering/GameWindow.cs:6042-6260— env-var per-frame branchsrc/AcDream.App/Rendering/GameWindow.cs:6260+— legacy per-frame branch (works)src/AcDream.Core/Physics/PositionManager.cs— class itself is retail-faithful (port of CPositionManager::adjust_offset), only the integration was wrong
Research:
- This session's
2026-05-03chronological commit log + visual verification docs/research/2026-05-03-remote-anim-cycle/investigation-prompt.mdfor the four-agent investigation that traced this
Fix path (separate L.3 follow-up phase, NOT this session):
The PositionManager class is correct retail-port. Re-integrate it as
ADDITIVE refinement on top of the working legacy chain (small
correction toward queued server positions, applied AFTER
apply_current_movement + UpdatePhysicsInternal + collision sweep)
— not as a REPLACEMENT for them. Match retail's actual update_object
chain ordering: position_manager::adjust_offset runs after the
primary motion + collision resolution.
Acceptance:
- New per-tick path enabled via env-var (or default after stabilization) produces the same smooth slope motion + zero blips as the legacy path.
- Inbound
UpdatePositionqueue catch-up nudges body toward server authoritative position without overriding terrain Z snap or causing position blips. - Verification: side-by-side vs legacy default in 2-client setup, identical visible behavior.
#38 — Chase camera + player feel "30 fps" since L.5 physics-tick gate
Status: OPEN Severity: MEDIUM (gameplay-feel regression; not a correctness bug) Filed: 2026-05-01 Component: rendering / physics / camera
Description: User reports that running around in third-person / chase camera feels less smooth than it did before the L.5 physics-tick work. FPS counter still reads 60+, but the motion of the player character + camera looks like it's updating at ~30 fps.
Root cause / status:
Almost certainly the L.5 _physicsAccum gate in
PlayerMovementController.cs (lines ~448-456). Retail integrates
physics at 30 Hz (MinQuantum = 1/30 s); we ported that faithfully so
collision behavior matches. Side effect: _body.Position only updates
on physics ticks, i.e. every 33 ms. Render runs at 60+ Hz but the
chase camera follows _body.Position directly — so the visible
position changes in 33 ms steps, even though we render at 60+ FPS.
First-person is less affected because the world rotates with Yaw (which
does update every render frame); third-person is hit hardest because
the character itself is the moving thing.
Retail in 2013 didn't see this because render was also ~30 fps — render rate ≈ physics rate. Our 60+ Hz render exposes the gap.
Discussion + fix options at the end of docs/research/2026-05-01-retail-motion-trace/findings.md
("Other things still don't have…" → camera smoothness discussion in
chat, not yet captured in the doc — TODO migrate the discussion in).
Recommended fix: render-time interpolation between physics ticks
(standard fixed-timestep + interpolated rendering pattern from Quake /
Source / Unreal). Snapshot _prevPhysicsPos and _currPhysicsPos at
each tick; render player + camera target at
Lerp(_prev, _curr, _physicsAccum / PhysicsTick). Cost: ~33 ms visual
latency between input and what you see (matches retail's perceived
latency anyway). Network outbound stays on the discrete tick value —
no wire change.
Quick confirmation test before any code change: temporarily set
PhysicsTick to 1.0/60.0 and see if chase camera feels smooth again.
If yes, gate is confirmed cause. (Don't ship that — it'd undo the L.5
collision fixes.)
Files:
src/AcDream.App/Input/PlayerMovementController.cs:172—PhysicsTickconstantsrc/AcDream.App/Input/PlayerMovementController.cs:448-456—_physicsAccumgatesrc/AcDream.App/Rendering/GameWindow.cs— wherever player render position + chase camera read_body.Position
Research:
- L.5 background:
memory/project_retail_debugger.md(the 30 Hz MinQuantum gate, the cdb trace evidence) - Discussed during 2026-05-01 motion-trace work
Acceptance:
- Chase-camera run-around at 60+ FPS feels as smooth as render rate suggests (no perceptual stepping)
- Network outbound (MoveToState / AutonomousPosition cadence + values) unchanged from current behavior
- Collision behavior unchanged (the L.5 wedge / steep-roof scenarios still resolve correctly)
- Observer view from a parallel retail client unchanged
#37 — Humanoid coat doesn't extend up to neck (visible "skin stub" between hair and coat)
Status: OPEN Severity: LOW (cosmetic; doesn't affect gameplay) Filed: 2026-05-01 Component: rendering / clothing / textures
Description: Every humanoid character (player + NPCs) wearing a coat shows a visible skin-colored region at the top of the coat where retail shows continuous coat fabric. From the back view: hair → skin stub → coat top. In retail: hair → coat collar (no exposed skin). This was originally reported as "head/neck protruding forward" — the apparent forward shift is an optical illusion caused by the missing coat collar.
Investigation 2026-05-01 (~3 hr session, conclusively ruled out many hypotheses):
What we ruled out:
- Animation source.
ACDREAM_USE_PLACEMENT_BASE=1(force chars toSetup.PlacementFrames[Resting]instead ofAnimation.PartFrames[0]) → stub still visible. - Backface culling / mesh winding.
ACDREAM_NO_CULL=1(disableglCullFaceentirely) → stub still visible. - Palette overlay (SubPalettes).
ACDREAM_NO_PALETTE_OVERLAY=1(skipComposePalette) → stub still visible (other colors broke as expected — confirms overlay was firing). Bug is NOT a body-skin SubPalette being mis-applied to coat fabric. - Bug source = part 16 (head).
ACDREAM_HIDE_PART=16→ head goes away, stub remains UNCHANGED (clean coat top with same shape). Stub is NOT from head GfxObj polygons. - Per-part placement frame Origin.
ACDREAM_NUDGE_Y=-0.1confirmed+Y = forwardin body-local; head Origin (0, 0.013, 1.587) places head correctly relative to spine. Math checks out.
What we confirmed (data is correct):
- Player Setup
0x02000001(Aluvian Male), 34 parts. - Server (ACE) sends
animParts=34 texChanges=12 subPalettes=10. - Part 9 (upper torso/coat) has gfx
0x0100120Dafter AnimPartChange. - Part 9 has 2 surfaces, BOTH covered by 2 TextureChanges
(
oldTex=0x050003D5→0x05001AFE,oldTex=0x050003D4→0x05001AFC). - Stub IS from part 9:
ACDREAM_HIDE_PART=9→ entire torso (including stub region) disappears. - Per-part composition formula (
Scale × Rotation × Translation) matches ACME'sStaticObjectManager.cs:256-258and retail decomp'sFrame::combineat0x00518FD0.
Investigation 2 (2026-05-04, 5 parallel agents + dat probes):
ALL of the obvious hypotheses ruled out:
- Byte-level decode primitive matches ACViewer. INDEX16/P8/DXT/BGRA paths are byte-identical.
- Polygon emission matches retail. All 43 polygons of gfx
0x0100120DareSidesType=0(ST_SINGLE), all surfaces areBase1Image— NO ST_DOUBLE polygons we'd be missing, NO surfaces lacking theType & 6bits that retail'sDrawPolyInternalskips. - Per-PART texture-override scoping is correct.
resolvedOverridesByPart[partIdx]gets per-MeshRef'd; not a global flat map (Agent 3's claim was wrong). - SubPalettes are full-size (Colors.Count=2048) palettes. Our
subPal.Colors[idx]indexing matches ACViewer'snewPalette.Colors[j + offset]. - The
*8wire un-pack is correctly single-applied (parser stores raw bytes; ComposePalette multiplies once).
The actual smoking gun (Investigation 2):
For +Acdream the server sends 10 SubPaletteSwap ranges that overlay palette indices:
[0..320), [576..1024), [1392..1488), [1728..1920). The complement — indices [320..576), [1024..1392), [1488..1728), [1920..2048) — is NOT overlaid. Base palette 0x0400007E at those indices contains the original red/skin tones (sampled values: 0x46 0x22 0x04, 0x4A 0x28 0x09, etc).
If the coat texture's UVs at the upper region map to texel-bytes whose palette index lands in one of those non-overlaid ranges, those pixels render with base-palette skin tones. That's the visible "skin stub at the top of the coat".
Working hypothesis: either
- ACE sends incomplete SubPalette ranges (retail-original would cover the full palette)
- Retail does additional client-side compute that ACE pre-resolves wrongly
- The base palette
0x0400007Eitself is supposed to have coat colors at those indices in retail's interpretation (different palette decode)
Next investigation (deferred):
- Diff ACE's
WorldObject_Networking.csCharGen ObjDesc construction against retail'sClothingTable::BuildObjDesc(acclient_2013_pseudo_c.txt:436261). Check if ACE actually walks every CloSubPaletteRange in the chosen PaletteTemplate, or skips some. - RenderDoc capture: confirm which texel/palette-index the upper-region polygons sample.
tools/InspectCoatTex/Program.csis the diagnostic harness — extend it.
Files (diagnostic env vars committed for next-session reuse):
src/AcDream.App/Rendering/InstancedMeshRenderer.cs:210-275—ACDREAM_NO_CULLenv varsrc/AcDream.App/Rendering/GameWindow.cs—ACDREAM_HIDE_PART=Nhides specific humanoid part;ACDREAM_DUMP_CLOTHING=1dumps AnimPartChanges + TextureChanges + per-part Surface chain coverage.src/AcDream.App/Rendering/TextureCache.cs:159-204—DecodeFromDatsis the texture decode entry. Compare againstreferences/WorldBuilder-ACME-Edition/.../TextureHelpers.cs.
Reproduction:
$env:ACDREAM_LIVE = "1"; $env:ACDREAM_DEVTOOLS = "1"
# normal launch — visible from chase camera looking at +Acdream's back
Stub is visible on +Acdream and on every NPC humanoid (Pathwarden, Town Crier, Shopkeeper Renald, etc.).
Acceptance: Side-by-side retail + acdream rendering of +Acdream shows coat extending up to chin level on both. No exposed skin between hair and coat.
#L.1 — Hotbar UI panel
Status: OPEN Severity: MEDIUM Filed: 2026-04-26 (deferred from Phase K) Component: ui / hotbar
Description: Number keys 1-9 are bound to UseQuickSlot_1..9
actions but no panel exists. Actions fire (visible via the [input]
console log) but produce no visible result. Phase L feature: drag-drop
hotbar with up to 5 bars × 9 slots, drag spell/skill icons to slots,
key activates the slot's contents. Server-side: CreateShortcutToSelected
(action 0x0A9 in retail motion table) sends a UseSelected on slot
fire.
Files: src/AcDream.UI.Abstractions/Panels/Hotbar/ (TBC).
Acceptance: Drag an item or spell into slot 1, press 1, server
responds as if the user clicked the item.
#L.2 — Spellbook favorites panel
Status: OPEN Severity: MEDIUM Filed: 2026-04-26 (deferred from Phase K) Component: ui / magic
Description: In MagicCombat scope, 1-9 should fire
UseSpellSlot_1..9 (distinct from hotbar). Requires a small UI to
pin favorite spells + a spellbook tab nav. Cross-references issue
#L.3 (combat-mode dispatch).
#L.3 — Combat-mode tracking + scope-aware Insert/PgUp/Delete/End/PgDn dispatch
Status: OPEN Severity: MEDIUM Filed: 2026-04-26 (deferred from Phase K) Component: input / combat
Description: Insert/PgUp/Delete/End/PgDn mean different things in
melee / missile / magic combat modes (per retail keymap MeleeCombat /
MissileCombat / MagicCombat blocks). Phase K has the bindings and the
scope stack; what's missing: CombatState.CurrentMode field +
listener for the server-side SetCombatMode packet (likely 0x0053 or
similar — confirm against ACE source). When mode arrives, push the
appropriate scope; when leaving combat, pop.
#L.4 — F-key panels: Allegiance / Fellowship / Skills / Attributes / World / SpellComponents
Status: OPEN Severity: LOW Filed: 2026-04-26 (deferred from Phase K) Component: ui
Description: Retail F3-F6, F8-F12 toggle UI panels for various
character data. Phase K has the bindings (ToggleAllegiancePanel,
ToggleFellowshipPanel, ToggleSpellbookPanel,
ToggleSpellComponentsPanel, ToggleAttributesPanel,
ToggleSkillsPanel, ToggleWorldPanel, ToggleInventoryPanel); the
panels themselves don't exist. Each is its own design feature.
Inventory (F12) is the most-requested.
#L.5 — Floating chat windows (Alt+1-4)
Status: OPEN Severity: LOW Filed: 2026-04-26 (deferred from Phase K) Component: ui / chat
Description: Alt+1..4 toggle four floating chat windows in retail.
Phase K binds the actions; ChatPanel currently is a single window.
Floating windows would need filtered-by-channel-type chat tail
rendering.
#L.6 — UI layout save/load (saveui / loadui / lockui)
Status: OPEN Severity: LOW Filed: 2026-04-26 (deferred from Phase K) Component: ui
Description: Retail had @saveui <name>, @loadui <name>,
@lockui commands for persisting ImGui-style window layouts. ImGui
has built-in LoadIniSettingsFromMemory /
SaveIniSettingsToMemory — wire these to per-named-layout files,
plus chat-command parsing for the @ prefixes.
#L.7 — Joystick / gamepad bindings
Status: OPEN Severity: LOW Filed: 2026-04-26 (deferred from Phase K) Component: input
Description: Retail keymap declares 11 Joystick devices in the
Devices block but no actions are bound by default. acdream uses
Silk.NET keyboard+mouse only. Adding Silk.NET joystick support + a
JoystickInputSource adapter would unlock controller play.
KeyChord.Device byte already supports values >1, so the binding
side is ready.
#L.8 — Plugin / scripting / macro input subscription
Status: OPEN Severity: MEDIUM Filed: 2026-04-26 (deferred from Phase K) Component: plugin / input
Description: CLAUDE.md goal: "Build acdream's plugin API to
support scripting/macros for player automation." Plugins should be
able to register custom actions (with namespaced IDs like
mymacro.heal-rotation) and subscribe to InputAction events. Phase K
foundation supports this via the multicast InputDispatcher; what's
missing is the plugin-API surface.
#32 — Retail edge-slide / cliff-slide / precipice-slide incomplete
Status: IN-PROGRESS Severity: HIGH Filed: 2026-04-29 Component: physics / collision
Description: When walking along walls, roof edges, cliff edges, or failed step-down boundaries, retail often slides along the boundary. acdream still hard-blocks or accepts too much in several of these cases.
Root cause / status: Tracked under Phase L.2c. Wall-adjacent
step_up_slide now feels acceptable in live testing. Local/remote movement
passes the retail-default EdgeSlide flag. The first precipice-slide slice now
preserves terrain/BSP walkable polygon vertices and runs the retail back-probe
before SPHEREPATH::precipice_slide; edge-slide Slid / Adjusted results
now feed the TransitionalInsert retry loop instead of being reverted by outer
validation, and a synthetic diagonal terrain-boundary test covers tangent
motion. ACDREAM_DUMP_EDGE_SLIDE=1 now reports whether a failed step-down had
polygon context.
L.4/L.5 update 2026-04-30: A retail debugger trace (cdb attached to v11.4186 acclient.exe — see #35) confirmed that retail does NOT wedge on the steep-roof scenario that produces the wedge in our acdream port. Three concrete findings:
- Retail's
OBJECTINFO::kill_velocityrarely fires in normal play — gated onlast_known_contact_plane_valid, which our L.2.4 proximity guard tends to clear before steep-poly hits land. Retail trace: 0 kill_velocity hits across 40,960 update_object calls. Our Phase 3 reset path now matches retail's gate (only kills when valid). - Retail integrates physics at 30Hz (
MinQuantum = 1/30 s); render is 60+ Hz. UpdatePhysicsInternal/update_object ratio = 0.61. We ported this gate as L.5 inPlayerMovementControllervia_physicsAccum. Render still runs at 60+ Hz; only the physics integration step is 30Hz. - The remaining wedge cause — body's pre-position drifts to the
polygon's tangent and gravity's tangent component into surface
produces a stable retain-collide-revert loop — is a downstream
consequence of retail's grounded-on-steep escape chain
(
step_sphere_up→step_up_slide→cliff_slide) being incompletely ported. Live test confirmed retail-strict Path 6 produces "lands on roof in falling animation, can't slide off" half-state because that chain doesn't produce smooth descent.
Pragmatic ship-state: BSPQuery Path 6 keeps the L.4 slide-tangent deviation (project-along-steep-face-and-return-Slid) for steep-poly airborne hits. It produces user-acceptable "slide off the roof" behavior at the cost of departing from retail's Path 6 → SetCollide → Path 4 → Phase 3 reset chain. Retail-strict requires the step_up_slide / cliff_slide audit below; until that lands, slide-tangent is the right deviation.
Remaining gaps: real-DAT building-edge fixtures, fuller cliff_slide
coverage, NegPolyHit dispatch, and the retail-strict
step_up_slide / cliff_slide audit (filed for follow-up). Named retail
anchors include CTransition::edge_slide, CTransition::cliff_slide,
SPHEREPATH::precipice_slide, and SPHEREPATH::step_up_slide.
Files: src/AcDream.Core/Physics/TransitionTypes.cs,
src/AcDream.Core/Physics/BSPQuery.cs,
tests/AcDream.Core.Tests/.
Research: docs/plans/2026-04-29-movement-collision-conformance.md,
docs/research/2026-04-30-precipice-slide-pseudocode.md.
Acceptance: Synthetic and real-DAT tests cover wall-slide, roof-edge slide, cliff/precipice slide, failed step-up/step-down, and the jump-clears-edge case.
#35 — [DONE 2026-04-30] Retail debugger toolchain (cdb + PDB GUID matching)
Status: DONE Severity: N/A (infrastructure) Filed + closed: 2026-04-30 Component: tooling / research
Description: When the question is "what does retail actually DO at runtime?" — wedges, animation flicker, geometry-specific bugs where the decomp is correct but the visible behavior is mysterious — there was no way to attach a debugger to a live retail acclient.exe and trace it. This issue tracks the toolchain that closed that gap.
What shipped:
tools/pdb-extract/check_exe_pdb.py— reads any PE's CodeView entry and reportsMATCH/MISMATCH (expected GUID = …)against ourrefs/acclient.pdb. Always run before attaching cdb.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 (answer: v11.4186, Sept 2013 EoR).- CLAUDE.md "Retail debugger toolchain" section — full workflow:
cdb path, sample
.cdbscript, PowerShell wrapper pattern, watchouts (PDB name conventions,;parsing, kill-target-on-detach behavior, high-hit-rate lag). - Step
-1added to the development workflow — "ATTACH cdb TO RETAIL (when behavior is the question, not code)". Tells future sessions: when guessing has failed twice in a row, don't keep guessing.
Discoveries this toolchain enabled (closed in same session):
- Retail integrates physics at 30Hz (
UpdatePhysicsInternal/update_objectratio = 0.61). Drove the L.5 fix in PlayerMovementController. OBJECTINFO::kill_velocityrarely fires in normal play (gated on last_known_contact_plane_valid). Our acdream port now matches.- Retail does NOT wedge on the steep-roof scenario. Confirmed our L.4 slide-tangent deviation in Path 6 is necessary until the retail step_up_slide / cliff_slide chain audit lands.
Files: tools/pdb-extract/check_exe_pdb.py,
tools/pdb-extract/dump_pdb_info.py, CLAUDE.md,
memory/project_retail_debugger.md.
Acceptance: Future sessions can attach cdb to a live retail client in under 5 minutes by following the CLAUDE.md workflow.
#36 — Sky-PES dispatch port (consolidates #2 / #28 / #29 visual gaps)
Status: OPEN Severity: MEDIUM (aesthetic feature-parity, but addresses a cluster of bugs) Filed: 2026-04-30 Component: sky / weather / particles
Description: Three open sky bugs (#2 lightning, #28 aurora, #29 cloud density) all trace back to the same missing infrastructure: retail's sky-PES (Particle Effect Script) dispatch chain. We have it now from a 2026-04-30 cdb live trace.
What retail does (live trace evidence):
Trace over 24,576 GameSky::Draw frames:
GameSky::Draw = 24,576 (60 Hz render rate)
GameSky::UseTime = 12,288 (30 Hz — half rate, MinQuantum)
GameSky::CreateDeletePhysicsObjects = 12,288 (also 30 Hz)
CPhysicsObj::CallPES = 372 (~150/min average)
CallPESHook::Execute = 372 (1:1 with CallPES)
CreateParticleHook::Execute = 62 (15 at cell load + 47 burst at transition)
CPhysicsObj::create_particle_emitter = 62 (matches CreateParticleHook)
Three findings:
- Retail has persistent particle emitters on celestial / sky objects. Created at cell load (15 initial) and dynamically as conditions change (the trace caught a +47 burst on a region/weather/time transition).
- The PES script-hook system (
CallPESHook::Execute→CPhysicsObj::CallPES) drives those emitters periodically, ~150 times per minute on average. - Earlier research said "GameSky doesn't read pes_id" — correct in scope, but missed that the dispatch chain runs through the script- hook system, not from inside GameSky directly. Cell/region/weather handlers schedule PES script hooks; those hooks call into CallPES.
Decomp anchors:
CallPESHook::Execute@0x00526e20— script-hook action that fires CallPESCreateParticleHook::Execute@0x00526ec0— particle-creation hookCPhysicsObj::CallPES@0x00511af0CPhysicsObj::create_particle_emitter@0x0050f360GameSky::CreateDeletePhysicsObjects@0x005073c0LongNIHash<ParticleEmitter>instance — emitter registryCelestialPosition.pes_id@ struct offset +0x004 — populated bySkyDesc::GetSkybut consumed downstream ofGameSky(via the hook system, not GameSky itself)
Implementation outline:
- Decomp dive: read
CallPESHook::Execute,CreateParticleHook::Execute,CPhysicsObj::CallPES, andGameSky::CreateDeletePhysicsObjects(and any cell/region weather handlers that spawn the dynamic 47). - Identify what triggers
CreateParticleHookfor sky objects — is it insideCreateDeletePhysicsObjects, the region/weather change handler, or somewhere else? - Port the persistent-emitter creation path: when a cell loads or weather/time changes, instantiate the appropriate ParticleEmitters on celestial objects.
- Port the PES timeline driver — periodic dispatch from a script
timeline into our equivalent
CallPES. - Port the actual PES script execution (rate of emission, particle parameters, etc.) into our particle system.
- Live verify with cdb during specific weather windows: aurora at dusk on Rainy DayGroup, lightning during storm.
Files (likely):
src/AcDream.App/Rendering/Sky/SkyRenderer.cs— emitter wiringsrc/AcDream.Core/World/SkyDescLoader.cs— already parses pes_idsrc/AcDream.Core/Particles/*— particle system foundationsrc/AcDream.App/Rendering/ParticleRenderer.cs— visual layer
Live-trace verification plan (next cdb session): Reattach to retail
during a specific aurora moment, log this pointer + pes_id arg on
every CallPES invocation, log the GfxObj being attached on every
create_particle_emitter. That tells us EXACTLY which celestial
objects retail PES-drives and with which IDs.
Acceptance: During the same in-game time/weather where retail shows aurora-style light play (Rainy DayGroup, dusk/dawn windows), acdream shows comparable colored sky effects. Cloud sheets look as dense / purple as retail. Lightning flashes appear during storm windows.
Closes-when-done: #28, #29, partially #2 (lightning may need additional flash-shader work).
#33 — Live entity collision shape collapses to one cylinder
Status: OPEN Severity: MEDIUM Filed: 2026-04-29 Component: physics / entities
Description: Live world entities do not yet use exact retail
CSphere / CCylSphere shape semantics. Several paths collapse the entity to
a simplified root-centered cylinder or fallback radius, which is not enough for
retail object and creature collision parity.
Root cause / status: Tracked under Phase L.2d. Requires auditing object
shape extraction, Setup.Radius fallback, building object identity, and live
entity broadphase records against named retail.
Files: src/AcDream.Core/Physics/CollisionPrimitives.cs,
src/AcDream.Core/Physics/ShadowObjectRegistry.cs,
src/AcDream.Core/Physics/PhysicsDataCache.cs.
Research: docs/plans/2026-04-29-movement-collision-conformance.md.
Acceptance: Live object collision uses the appropriate retail sphere or cylsphere data where available. Tests prove at least one multi-shape object and one live creature case no longer use the single-cylinder fallback.
#2 — Lightning visual mismatch (sky PES path disproved)
Status: OPEN Severity: MEDIUM Filed: 2026-04-25 Component: weather / sky / vfx
Description: Lightning/storm sky visuals still do not match retail. A 2026-04-28 named-retail recheck disproved the prior assumption that SkyObject.PesObjectId drives sky-render flash particles: SkyDesc::GetSky copies the field into CelestialPosition.pes_id, but GameSky::CreateDeletePhysicsObjects, GameSky::MakeObject, and GameSky::UseTime never read it.
Root cause / status: Open again. The sky-PES path is non-retail and must stay disabled for normal rendering. The remaining mismatch likely lives in the sky/weather mesh material path, the lightning/fog flash path, or another weather subsystem outside GameSky; do not reintroduce per-SkyObject PES playback without new decompile evidence.
Files:
src/AcDream.App/Rendering/Sky/SkyRenderer.cs— sky/weather mesh draw, material state, pre/post splitsrc/AcDream.App/Rendering/Shaders/sky.frag— flash/fog/lightning coloration pathsrc/AcDream.Core/World/SkyDescLoader.cs— keepPesObjectIdparsed for diagnostics, not render playback
Research:
docs/research/2026-04-28-pes-pseudocode.md— C.1 correction:CelestialPosition.pes_idcopied but ignored by GameSkydocs/research/2026-04-23-sky-pes-wiring.md— earlier decompile trace reached the same no-sky-PES conclusiondocs/research/2026-04-23-lightning-real.md(decompile trace + dat discovery)docs/research/2026-04-23-physicsscript.md(runtime semantics)docs/research/2026-04-23-lightning-crossfade.md(crossfade mechanism)
Acceptance: During a Rainy DayGroup's storm window, visible flashes appear in the sky at the dat-scripted moments, the fragment-shader flash bump briefly brightens the scene, and (later, once thunder audio is wired) a thunder clap plays with a short propagation delay.
See also #36 (Sky-PES dispatch port) — the lightning visuals likely route through the same PES-hook chain that drives aurora and cloud-density. Most of #2's storm-flash visuals will be unblocked by the #36 port.
#3 — Client clock drifts from retail after ~10 minutes (periodic TimeSync missing)
Status: OPEN Severity: MEDIUM Filed: 2026-04-25 Component: net / sky
Description: Our WorldTimeService.DayFraction syncs with the server once at login via ConnectRequest + TimeSync, then advances from the local wall-clock. Retail receives periodic TimeSync refreshes (header flag 0x1000000) carrying a fresh PortalYearTicks double and re-anchors its clock. Without those, acdream's keyframe state drifts from retail's over 10+ minutes — observed during the 2026-04-24 sky-color debug sessions where retail was at DayFraction 0.976 while acdream was at 0.634.
Root cause / status: Mechanism is well-understood (see research). WorldTimeService.SyncFromServer(double) already exists — we just need to detect the periodic flag in the packet header and call it whenever a fresh tick arrives.
Files:
src/AcDream.Core.Net/WorldSession.cs— header-flag parsing; currently only the initial sync is consumedsrc/AcDream.Core/World/WorldTimeService.cs—SyncFromServer(double ticks)ready; needs caller wiring
Research: docs/research/deepdives/r12-weather-daynight.md §TimeSync (line ~563). References retail packet-header flag 0x1000000 carrying PortalYearTicks double.
Acceptance: Probe retail via tools/RetailTimeProbe and acdream's ACDREAM_DUMP_SKY log at the same wall-clock moment after a 20-minute session without re-login; abs(acdream.DayFraction - retail.DayFraction) < 0.01.
#13 — PlayerDescription trailer past enchantments (options / shortcuts / hotbars / desired_comps / spellbook_filters / options2 / gameplay_options / inventory / equipped)
Status: OPEN Severity: LOW (no current user-visible bug; future panels will need the data) Filed: 2026-04-25 Component: net / player-state
Description: PlayerDescriptionParser walks through enchantments (Phase H, 2026-04-25). The trailer beyond that — Options1 / Shortcuts / HotbarSpells (8 lists) / DesiredComps / SpellbookFilters / Options2 / GameplayOptions blob / Inventory / Equipped — is not yet parsed. Required for future Spellbook UI panel, hotbar UI, inventory UI, character options panel.
Root cause / status: Holtburger events.rs:462-625 has the full layout. The trickiest piece is gameplay_options — a variable-length opaque blob; holtburger uses a heuristic forward search (find_inventory_start_after_gameplay_options) for plausibly-aligned inventory-count + GUID pairs to find the inventory start. Other sections are well-formed.
Files:
src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs— extendParsedrecord + walker.tests/AcDream.Core.Net.Tests/PlayerDescriptionParserTests.cs— add fixtures per section.src/AcDream.Core.Net/GameEventWiring.cs— routeparsed.Inventory+Equippedto ItemRepository.
Research: holtburger events.rs:462-625; references/actestclient/TestClient/messages.xml.
Acceptance: All sections of a real-world PlayerDescription parse to completion (no truncation). New tests cover synthetic fixtures per section. ItemRepository.Count after login > 0.
#4 — Sky horizon-glow disabled (fog-mix skipped on sky meshes)
Status: OPEN Severity: LOW (aesthetic feature-parity, not regression from pre-session state) Filed: 2026-04-25 Component: sky
Description: Phase 8.1 (commit 593b76f) disabled the fog-mix on sky meshes to fix the "entire dome swallowed by fog color" regression. Dereth's keyframe FogEnd values (0–2400 m) are calibrated for terrain; sky meshes are authored at radii 1050–14271 m so every sky pixel was past FogEnd, saturated to uFogColor, destroying stars / moon / dome texture. Disabling the mix restored visibility but we lost retail's horizon-glow effect (gradient from clear zenith to fog-tinted horizon band at dusk/dawn).
Root cause / status: Three competing hypotheses, none pinned down: (a) retail uses a different fog range for sky than terrain; (b) retail applies fog with an elevation-angle weighting rather than linear distance; (c) retail's sky meshes don't participate in the global fog and the "horizon glow" comes from a different atmospheric-scatter path. Need to identify retail's actual sky-fog behaviour before re-enabling with correct parameters.
Files:
src/AcDream.App/Rendering/Shaders/sky.frag— line ~55,rgb = mix(uFogColor.rgb, rgb, vFogFactor)currently commented outsrc/AcDream.App/Rendering/Shaders/sky.vert— lines 109-114,vFogFactorcomputation
Research: docs/research/2026-04-23-sky-fog.md. Partial; doesn't pin the sky-specific fog path.
Acceptance: At dusk in Holtburg, the sky dome shows a clear zenith and a warm fog-tinted horizon band that matches retail's appearance, with stars / moon / sun / clouds all still visible at their correct brightnesses elsewhere in the frame.
#28 — Aurora ("northern lights") effect not rendered
Status: OPEN Severity: LOW (aesthetic feature-parity) Filed: 2026-04-26 Component: sky / vfx
Description: Retail renders a dynamic colored "light play" effect in the sky during certain Rainy/Cloudy DayGroup time windows. The user describes it as aurora-borealis-style. acdream renders no comparable effect.
Root cause / status: Open again. The prior root cause was wrong: CelestialPosition.pes_id exists in the retail header and is populated by SkyDesc::GetSky, but named retail GameSky code does not read it during sky object creation, update, or draw. A 2026-04-28 C.1 experiment that played those PES ids produced colored blobs/wash that did not match retail's broad aurora-like rays, and the path is now debug-only behind ACDREAM_ENABLE_SKY_PES=1.
Retail header at acclient.h line 35451 still documents the copied field:
struct CelestialPosition {
IDClass<...> gfx_id;
IDClass<...> pes_id; // ← particle scheduler ID
float heading; float rotation;
Vector3 tex_velocity;
float transparent; float luminosity; float max_bright;
unsigned int properties;
};
StarsProbe confirmed Dereth Rainy DayGroup 3 carries multiple PES-bearing entries (verified 2026-04-27). Sample for the user's observed Warmtide-Rainy state:
| OI | Gfx | PES | Active window | Notes |
|---|---|---|---|---|
| 5 | 0x02000714 | 0x330007DB | always | low-rate background |
| 7 | 0x02000BA6 | 0x33000453 | 0.03–0.19 | early morning |
| 17 | 0x02000589 | 0x3300042C | 0.27–0.91 | active during user's screenshot |
acdream's geometry half is now wired (commit landing 2026-04-27 — EnsureSetupUploaded walks Setup.Parts for 0x020xxx IDs). The remaining dynamic visual half is not SkyObject.PesObjectId; likely suspects are sky/weather mesh material state, texture transform/blending, or a separate weather/lightning subsystem outside GameSky.
Implementation outline:
- Keep
SkyObject.PesObjectIdparsed for diagnostics only. - Compare retail/acdream material state for the active sky/weather GfxObj/Setup ids (
0x02000588,0x02000589,0x02000714,0x02000BA6). - Trace the named retail sky/weather draw path for texture transforms, translucency, diffusion, luminosity, and any non-GameSky weather effect dispatch.
- Only add a new runtime visual path once the decompile has an actual caller.
Decomp pointers:
SkyDesc::GetSkynamed retail0x00501ec0— copiesSkyObject.default_pes_objectintoCelestialPosition.pes_id.GameSky::CreateDeletePhysicsObjectsnamed retail0x005073c0— creates/updates sky objects fromgfx_id, does not readpes_id.GameSky::MakeObjectnamed retail0x00506ee0— callsCPhysicsObj::makeObject(gfx_id, 0, 0), no PES.GameSky::UseTimenamed retail0x005075b0— updates frame/luminosity/diffusion/translucency, no PES.
Files:
src/AcDream.Core/World/SkyDescLoader.cs— carriesPesObjectIdfor diagnostics.src/AcDream.App/Rendering/Sky/SkyRenderer.cs— likely material/texture-transform parity work.src/AcDream.App/Rendering/GameWindow.cs— sky-PES playback remains debug-only, disabled by default.
Acceptance: When retail shows aurora-style light play at a specific in-game time / weather, acdream shows a visually-comparable effect at the same time.
See #36 (filed 2026-04-30) — a live cdb trace confirmed retail's aurora rendering uses the script-hook PES dispatch chain (CallPESHook::Execute → CPhysicsObj::CallPES) on persistent particle emitters, with a cell-load population (15 initial emitters) plus dynamic spawning on region/weather/time transitions (caught a +47 burst). Implementation work consolidated under #36.
#29 — Cloud surface 0x08000023 still appears thinner than retail despite blend-mode + Setup fixes
Status: OPEN Severity: LOW (aesthetic feature-parity) Filed: 2026-04-27 Component: sky / clouds
Description: User screenshot comparison showed acdream's clouds let too much sun through; retail's are denser and have a purpleish tint. Two follow-up fixes landed without visible improvement:
TranslucencyKindExtensions.FromSurfaceTypenow applies retail's Translucent-override atD3DPolyRender::SetSurface(decomp 425246-425260) — surface0x08000023(Type=0x10114=B1ClipMap | Translucent | Alpha | Additive) is now correctly classified asAlphaBlendinstead ofAdditive.SkyRenderer.EnsureSetupUploadednow loads0x020xxxxxSetup IDs (e.g.0x02000588,0x02000589,0x02000714,0x02000BA6) which were silently dropped. Setup parts are flattened viaSetupMesh.Flattenand uploaded with their per-part transform baked into vertex positions.
Despite both being decomp-correct fixes, the user reports no observable visual change in dual-client comparison. Two follow-up hypotheses:
- The Setup objects are tiny placeholder meshes (one
0x010001ECpart each) that exist mainly to anchor a PES emitter — the cloud "density" / "purple sheen" the user perceives is entirely the PES particle layer, not the static mesh. - The cloud surface might still be rendering correctly per its dat data, and what looks "thicker" in retail is the additional aurora-like PES sheen overlaid on top.
If hypothesis (a) is correct, this issue effectively rolls into #28 — the PES rendering work would resolve both.
Files:
src/AcDream.Core/Meshing/TranslucencyKind.cs— Translucent overridesrc/AcDream.App/Rendering/Sky/SkyRenderer.cs—EnsureSetupUploaded
Acceptance: Cloud sheets look as dense/purple as retail in dual-client side-by-side. May require #28 (PES) to land first.
See #36 (filed 2026-04-30) — confirmed via live cdb trace: retail's cloud density comes from the same PES-driven particle-emitter chain as aurora. Implementation consolidated there.
Recently closed
#31 — [DONE 2026-04-29] Low outdoor cell id can go stale after transition movement
Closed: 2026-04-29
Commit: (this commit)
Resolution: ResolveWithTransition now refreshes outdoor cell ownership
from the resolved world position while the sphere sweep runs. Intra-landblock
24m outdoor seams update the low cell id, and full-cell callers crossing a
landblock seam get the destination landblock prefix plus the correct outdoor
low cell.
#34 — [DONE 2026-04-29] Missing routine local/server correction diagnostic
Closed: 2026-04-29
Commit: (this commit)
Resolution: Added ACDREAM_DUMP_MOVE_TRUTH=1, which logs local resolved
position/contact/cell, outbound movement fields, server UpdatePosition echo,
and local/server correction delta for the player in grep-friendly
move-truth OUT / move-truth ECHO lines.
#30 — [DONE 2026-04-29] AutonomousPosition contact byte is too often grounded
Closed: 2026-04-29
Commit: (this commit)
Resolution: GameWindow now derives the movement contact byte from
MovementResult.IsOnGround and passes it explicitly to both MoveToState.Build
and AutonomousPosition.Build. Added packet tests proving both builders encode
an explicit airborne contact byte.
#27 — [DONE 2026-04-26] Cloud meshes appeared missing or faint vs retail
Closed: 2026-04-26
Commit: 4678b3e fix(sky): apply per-Surface Translucency + Luminosity for retail-faithful weather
Resolution: Resolved as a side-effect of the Bug A fix. The original observation came from a session where every sky mesh got effEmissive = 1.0 (saturated vTint to white), which made stars/clouds look full-bright instead of time-of-day-tinted. Fix 2 corrected the emissive default to sub.SurfLuminosity so cloud surfaces (Lum=0.0) now run through the ambient+diffuse vertex-lit path and pick up keyframe tint. Fix 1 separately plumbed surface.Translucency to the shader, picking up the 0.25 translucency on cloud surface 0x08000023 (75% opacity). Visual verification under Phase 0 of the followup plan: clouds and colors now match retail at LCG-picked DayGroups across the day cycle.
#1 — [DONE 2026-04-26] Rain falls only to horizon, not to the player's feet
Closed: 2026-04-26
Commits: 3e0da49 (sky pass split + retail -120m Z offset), 4678b3e (Surface.Translucency + Luminosity correctness), d95a8d2 (legacy emitter delete)
Resolution: Two-part fix. First, rain rendering was completely re-architected to match retail's LScape::draw pattern at 0x00506330 — sky pass before the landblock loop (RenderSky), weather pass after (RenderWeather). Weather meshes now overlay terrain instead of being painted over. Camera anchored inside the rain cylinder via the retail-correct -120m Z offset (constant 0xc2f00000 in GameSky::UpdatePosition at 0x00506dd0). Second, the per-Surface Translucency float (rain = 0.5) and Luminosity float (rain = 0.1484) were both being ignored by the renderer; plumbed end-to-end so streaks contribute at retail-correct intensity instead of 6.7× too bright. Legacy camera-attached particle emitter (UpdateWeatherParticles + BuildRainDesc + BuildSnowDesc) deleted; world-space mesh is the only path now. Snow rides the same fix automatically. Filed alongside two follow-up issues from the visual-verify session: #27 (cloud rendering parity), #28 (aurora/northern lights).
#26 — [DONE 2026-04-26] Stars rendered as a square in one corner of the sky
Closed: 2026-04-26
Commit: 7b88fde fix(sky): drive wrap mode from mesh UV range — fixes Bug B (stars-as-square)
Resolution: SkyRenderer's wrap-mode heuristic was GL_CLAMP_TO_EDGE unless TexVelocity != 0, which mis-classified the inner sky/star layer 0x010015EF (UVs in [0.398, 4.602], TexVel=0). Most of the dome sampled the texture's edge texels; only the small region where UVs fell in [0,1] showed actual texture content. Fixed by computing NeedsUvRepeat per submesh from the actual UV range during GfxObjMesh.Build() and driving the wrap-mode choice from that flag plus the existing scrolling check. Outer dome 0x010015EE/F0/F1/F2 (UVs strictly in [0,1]) keeps CLAMP_TO_EDGE so no seam regression. Probe tools/StarsProbe/ (commit 991fb9a) committed alongside as the diagnostic that found this.
#25 — [DONE 2026-04-26] Phase K.3 — Settings panel + click-to-rebind UI
Closed: 2026-04-26
Commit: (this commit)
Resolution: SettingsPanel with click-to-rebind UX (modal capture
via InputDispatcher.BeginCapture, Esc cancels, conflict prompt with
Yes/No, draft / Save / Cancel semantics), F11 toggle + ImGui
MainMenuBar entry, per-action / per-section / reset-all-defaults
buttons. Roadmap + ISSUES + memory crib + CLAUDE.md updated.
#24 — [DONE 2026-04-26] Phase K.2 — auto-enter player mode + MMB mouse-look
Closed: 2026-04-26
Commit: af74eac
Resolution: Auto-enter player mode at login (one-shot guard
reusing the existing Tab handler logic); MMB-hold mouse-look
(CameraInstantMouseLook — cursor-locked camera + character yaw
drive together); Tab → ChatPanel.FocusInput(); DebugPanel
"Toggle Free-Fly Mode" button.
#23 — [DONE 2026-04-26] Phase K.1c — retail-default keymap + JSON persistence
Closed: 2026-04-26
Commit: da18910
Resolution: ~149 retail-faithful bindings byte-precise to
docs/research/named-retail/retail-default.keymap.txt;
%LOCALAPPDATA%\acdream\keybinds.json with merge-over-defaults
migration; acdream debug F-keys relocated to Ctrl+F*.
#22 — [DONE 2026-04-26] Phase K.1b — cut handlers over to dispatcher
Closed: 2026-04-26
Commit: 256e962
Resolution: Drop the legacy mouse-X-character-yaw path; fix
WantCaptureMouse gating; single input path via the multicast
InputDispatcher.
#21 — [DONE 2026-04-26] Phase K.1a — input architecture skeleton
Closed: 2026-04-26
Commit: 84512d3
Resolution: Action enum, multicast InputDispatcher with scope
stack, KeyChord / Binding / KeyBindings, Silk.NET adapters;
parallel to existing handlers (no behavior change).
#20 — [DONE 2026-04-25] CombatChatTranslator — retail-faithful combat-text formatters
Closed: 2026-04-25
Commit: 3d26c8e
Resolution: Retail-faithful combat-text formatters into ChatLog ("You hit drudge for 50 slashing damage"). Subscribes to CombatState's DamageTaken / DamageDealtAccepted / EvadedIncoming / MissedOutgoing / AttackDone / KillLanded events; templates ported verbatim from holtburger panels/chat.rs:221-308.
#19 — [DONE 2026-04-25] TurbineChat codec (0xF7DE) + ChatChannelInfo
Closed: 2026-04-25
Commit: ca968fc
Resolution: Full 0xF7DE codec with three payload variants (EventSendToRoom, RequestSendToRoomById, Response), UTF-16LE strings with variable-length prefix, SetTurbineChatChannels (0x0295) parser, unified ChatChannelInfo (Legacy + Turbine variants), TurbineChatState. Note: ACE doesn't run a TurbineChat server — codec is ready for retail-server-emulating setups.
#18 — [DONE 2026-04-25] Holtburger inbound chat parity + Windows-1252 codec
Closed: 2026-04-25
Commit: ff5ed9e
Resolution: EmoteText (0x01E0) / SoulEmote (0x01E2) / ServerMessage (0xF7E0) / PlayerKilled (0x019E) parsers + WeenieError routing through GameEventWiring. Global codec switch from Encoding.ASCII to Encoding.GetEncoding(1252); matches retail + holtburger; accented names round-trip correctly.
#17 — [DONE 2026-04-25] ChatPanel input field + slash commands
Closed: 2026-04-25
Commit: f14296c
Resolution: ChatPanel gains Enter-to-submit input field; ChatInputParser recognises /say /t /tell /r /g /f /a /m /p /v /cv /lfg /trade /role /society /olthoi; ChatVM tracks LastIncomingTellSender for /r reply.
#16 — [DONE 2026-04-25] LiveCommandBus + WorldSession chat senders
Closed: 2026-04-25
Commit: 8e6e5a0
Resolution: Real ICommandBus impl + WorldSession.SendTalk / SendTell / SendChannel wrappers + SendChatCmd record + ChannelResolver legacy-id mapping per holtburger.
#15 — [DONE 2026-04-25] DebugPanel migration
Closed: 2026-04-25
Commit: 56037a4
Resolution: Migrates the 473-LOC StbTrueTypeSharp DebugOverlay to an ImGui DebugPanel with collapsing-headers + checkbox diagnostics + combat-event tail. Deletes DebugOverlay.cs; TextRenderer + BitmapFont kept for future HUD-in-world (D.6 damage floaters, name plates).
#14 — [DONE 2026-04-25] IPanelRenderer widget extension
Closed: 2026-04-25
Commit: b131514
Resolution: Adds 14 widget signatures (TextColored / Checkbox / Combo / InputTextSubmit / BeginTable / etc.) to IPanelRenderer + ImGuiPanelRenderer impl. Foundation for I.2 DebugPanel and I.4 ChatPanel input.
#7 — [DONE 2026-04-25] PlayerDescription parser stops after spells (enchantment block parsed)
Closed: 2026-04-25
Commit: feat(net): #7 PlayerDescriptionParser — enchantment block walker + StatMod flow
Resolution: Extended PlayerDescriptionParser past the spell block to parse the Enchantment trailer per holtburger events.rs:462-501. Added EnchantmentEntry record with full wire payload (16 fields including the StatMod triad — type/key/val) + EnchantmentBucket (Multiplicative / Additive / Cooldown / Vitae per EnchantmentMask). Parsed now exposes IReadOnlyList<EnchantmentEntry> Enchantments. GameEventWiring routes each entry through the new Spellbook.OnEnchantmentAdded(ActiveEnchantmentRecord) overload with StatModType / StatModKey / StatModValue / Bucket populated. 2 new parser tests cover the enchantment block schema + Vitae singleton.
The remaining trailer sections (options / shortcuts / hotbars / inventory / equipped) are not yet parsed; filed as #13. Stopping after enchantments is intentional — it covers the highest-value section (issue #6 lights up) and avoids the heuristic gameplay_options walker that #13 needs.
#12 — [DONE 2026-04-25] Capture full Enchantment wire payload (StatMod) on ActiveEnchantmentRecord
Closed: 2026-04-25
Commit: feat(net): #7 PlayerDescriptionParser — enchantment block walker + StatMod flow
Resolution: Closed alongside #7 in the same commit. ActiveEnchantmentRecord extended with optional StatModType, StatModKey, StatModValue, Bucket fields. Spellbook got an OnEnchantmentAdded(ActiveEnchantmentRecord) overload that accepts the full record. EnchantmentMath.GetMod aggregator now consumes the StatMod data: multiplicative bucket (1) → multiplier ×= val; additive bucket (2) → additive += val; vitae bucket (8) → multiplier ×= val (applied last, matching retail CEnchantmentRegistry::EnchantAttribute semantics). 5 new EnchantmentMath StatMod-aware tests cover: multiplicative buffs aggregate, additive buffs sum, stat-key mismatch is filtered out, vitae applies multiplicatively, family-stacking picks the higher spell-id buff.
ParseMagicUpdateEnchantment (the live-update opcode 0x02C2) is not yet extended — it still uses the 4-field summary. That's a separate refactor; PlayerDescription's enchantment block is the load-bearing path for issue #6, and that's now flowing.
#6 — [DONE 2026-04-25 architecture; data flowing as of #12] Vital max ignores enchantment buffs + vitae
Closed: 2026-04-25
Commit: feat(player): #6 fold enchantment buffs into vital max via EnchantmentMath
Resolution: Ported CEnchantmentRegistry::EnchantAttribute (PDB 0x00594570) as EnchantmentMath.GetMod(IEnumerable<ActiveEnchantmentRecord>, SpellTable, statKey) returning (Multiplier, Additive). Family-stacking dedup via SpellTable.Family (only one buff per family bucket wins, by highest spell-id as a generation proxy). Spellbook.GetVitalMod(statKey) delegates. LocalPlayerState.GetMaxApprox reworked to apply (unbuffed × mult) + add with retail's min-vital clamp (>= 5 if base ≥ 5 else >= 1, matches CreatureVital::GetMaxValue at PDB 0x0058F2DD). Stat-key constants (MaxHealth=1, MaxStamina=3, MaxMana=5) verified against docs/research/named-retail/acclient.h line 37287-37301.
Architecture in place; data still flat. Until ISSUES.md #12 lands the wire-format extension that captures StatMod (type/key/val) on ActiveEnchantmentRecord, the per-enchantment modifier value isn't aggregated yet — EnchantmentMath.GetMod returns Identity (1.0, 0.0) for every stat key. Once #12 wires the data, the existing aggregator + formula light up automatically. Live +Acdream Stam/Mana percent will continue to read ~95% until #12 lands.
6 new EnchantmentMathTests cover: empty list returns Identity, no-table-entries returns Identity, stat-key constants match ACE enum, Identity is (1, 0), family-stacking dedup, family=0 (no-bucket) treated as separate.
#11 — [DONE 2026-04-25] Spell metadata loader (spells.csv → SpellTable)
Closed: 2026-04-25
Commit: feat(spells): #11 SpellTable — hydrate metadata from spells.csv at startup
Resolution: Added SpellMetadata record + SpellTable CSV loader (hand-rolled RFC 4180-ish parser for the quoted Description column with embedded commas). Wired into Spellbook constructor as optional metadata source; Spellbook.TryGetMetadata(spellId, out) returns the static record when found. GameWindow loads data/spells.csv from bin output at construction (file copied via <None Include> in AcDream.App.csproj from docs/research/data/spells.csv). Falls back to SpellTable.Empty + console warning if the file is missing (e.g. tooling contexts). 10 new tests covering: empty table, header-only, simple row, quoted description with commas, blank lines skipped, bad spell-id rows skipped, lookup hit/miss, RFC 4180 escaped-quote parsing.
#9 — [DONE 2026-04-25] Address-correction sweep on acclient_function_map.md
Closed: 2026-04-25
Commit: docs(research): #9 sweep acclient_function_map.md against PDB symbols
Resolution: Wrote tools/pdb-extract/check_function_map.py that cross-checks 63 hand-curated entries against docs/research/named-retail/symbols.json. Findings: zero entries matched address-and-name exactly (confirms ~0x800-0xC10 byte delta vs the binary that produced our Ghidra chunks — different build revision). 38 entries corrected by PDB name lookup; 25 entries either lack PDB symbol records (inlined / non-public) or had wrong class assignments (e.g. 0x5387C0 claimed as CTransition::find_collisions was actually CPolygon::polygon_hits_sphere). Updated acclient_function_map.md with corrected addresses, kept legacy addresses in a "Was" column for traceability, added a top-of-file sweep summary.
#10 — [DONE 2026-04-25] Wire KillerNotification (0x01AD)
Closed: 2026-04-25
Commit: docs(issues): #8/#9/#11 filed; #10 wired (KillerNotification)
Resolution: Orphan parser at GameEvents.ParseKillerNotification existed but was never registered for dispatch in GameEventWiring.cs. Added a combat.OnKillerNotification(victimName, victimGuid) method on CombatState that fires a new KillLanded event, then registered the handler. One-line dispatch + 12-line CombatState method + one regression test fixture in GameEventWiringTests.
#8 — [DONE 2026-04-25] pdb-extract tool: PDB → symbols.json + types.json
Closed: 2026-04-25
Commit: tools(pdb-extract): #8 PDB -> symbols.json + types.json sidecar
Resolution: Pure-Python (no deps) MSF 7.00 PDB parser at tools/pdb-extract/pdb_extract.py. Reads refs/acclient.pdb (Sept 2013 EoR build), extracts S_PUB32 records from the symbol stream + named class/struct types from TPI, and writes JSON sidecars to docs/research/named-retail/:
symbols.json— 18,366 named functions (address+ demangledname+ rawmangled)types.json— 5,371 named class/struct records (name+size+kind)
Best-effort MSVC C++ demangler handles the common ?Method@Class@@<sig> patterns + ctors (??0) + dtors (??1); operator overloads and vtables left mangled. Spot-check verified: CEnchantmentRegistry::EnchantAttribute resolves to 0x00594570 exactly as the discovery agent reported. Runtime <1s.
Regen workflow: py tools/pdb-extract/pdb_extract.py refs/acclient.pdb. The committed JSON outputs are stable + ~3 MB combined; ripgrep/jq on them is faster than re-parsing.
#5 — [DONE 2026-04-25] VitalsPanel stamina/mana bars always null
Closed: 2026-04-25
Commit: feat(player): #5 PlayerDescription parser — Stam/Mana via attribute block
Resolution: First attempt (commit d42bf57) used AppraiseInfoParser for PlayerDescription (0x0013) — wrong wire format. ACE source confirmed via GameEventPlayerDescription.WriteEventBody: PlayerDescription is hand-written (DescriptionPropertyFlag-driven property hashtables, vector flags, attribute block, skills, spells, options/inventory tail) — distinct from IdentifyObjectResponse (0x00C9)'s AppraiseInfo.Write. Pivoted to a real port: new PlayerDescriptionParser.cs that walks property hashtables (Int32/Int64/Bool/Double/String/Did/Iid + Position) gated on the property flags, then reads vector flags + has_health + the attribute block where vitals 7/8/9 carry ranks/start/xp/current. Also redesigned LocalPlayerState to track per-vital snapshots (replacing the sentinel-API of attempt 1) plus per-attribute snapshots, with GetMaxApprox applying the retail formula vital.(ranks+start) + attribute_contribution (Endurance/2 for Health, Endurance for Stamina, Self for Mana). Live verified: +Acdream shows three bars; ~95% reading on Stam/Mana traced to active buff multipliers (filed as #6). Wire-port also added PrivateUpdateVital (0x02E7) + PrivateUpdateVitalCurrent (0x02E9) for delta updates per holtburger UpdateVital. ~700 LOC C#, 30+ new tests.