# 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](plans/2026-04-11-roadmap.md).

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/*.md` note.

## Conventions

- Sequential integer IDs (`#1`, `#2`, …). Commits that close an issue reference the ID in the message (e.g. `fix #3: periodic TimeSync parsing`).
- `Status` is `OPEN`, `IN-PROGRESS`, or `DONE`. 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 gate `if (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` —
  `PlanFromVelocity` thresholds (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:

1. **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 `UpdatePosition` arrival. Body never follows
   the terrain mesh between UPs.

2. **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 branch
- `src/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-03` chronological commit log + visual verification
- `docs/research/2026-05-03-remote-anim-cycle/investigation-prompt.md`
  for 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 `UpdatePosition` queue 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` — `PhysicsTick` constant
- `src/AcDream.App/Input/PlayerMovementController.cs:448-456` — `_physicsAccum` gate
- `src/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 to
  `Setup.PlacementFrames[Resting]` instead of `Animation.PartFrames[0]`)
  → stub still visible.
- **Backface culling / mesh winding.** `ACDREAM_NO_CULL=1` (disable
  `glCullFace` entirely) → stub still visible.
- **Palette overlay (SubPalettes).** `ACDREAM_NO_PALETTE_OVERLAY=1`
  (skip `ComposePalette`) → 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.1` confirmed
  `+Y = forward` in 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 `0x0100120D` after 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's `StaticObjectManager.cs:256-258` and retail decomp's
  `Frame::combine` at `0x00518FD0`.

**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 `0x0100120D` are `SidesType=0` (ST_SINGLE), all surfaces are `Base1Image` — NO ST_DOUBLE polygons we'd be missing, NO surfaces lacking the `Type & 6` bits that retail's `DrawPolyInternal` skips.
- **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's `newPalette.Colors[j + offset]`.
- **The `*8` wire 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
1. ACE sends incomplete SubPalette ranges (retail-original would cover the full palette)
2. Retail does *additional* client-side compute that ACE pre-resolves wrongly
3. The base palette `0x0400007E` itself is supposed to have coat colors at those indices in retail's interpretation (different palette decode)

**Next investigation (deferred):**

- Diff ACE's `WorldObject_Networking.cs` CharGen ObjDesc construction against retail's
  `ClothingTable::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.cs` is the diagnostic harness — extend it.

**Files (diagnostic env vars committed for next-session reuse):**

- `src/AcDream.App/Rendering/InstancedMeshRenderer.cs:210-275`
  — `ACDREAM_NO_CULL` env var
- `src/AcDream.App/Rendering/GameWindow.cs` — `ACDREAM_HIDE_PART=N`
  hides specific humanoid part; `ACDREAM_DUMP_CLOTHING=1` dumps
  AnimPartChanges + TextureChanges + per-part Surface chain coverage.
- `src/AcDream.App/Rendering/TextureCache.cs:159-204` — `DecodeFromDats`
  is the texture decode entry. Compare against
  `references/WorldBuilder-ACME-Edition/.../TextureHelpers.cs`.

**Reproduction:**

```powershell
$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:
1. Retail's `OBJECTINFO::kill_velocity` rarely fires in normal play —
   gated on `last_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).
2. 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 in `PlayerMovementController` via
   `_physicsAccum`. Render still runs at 60+ Hz; only the physics
   integration step is 30Hz.
3. 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 reports `MATCH` / `MISMATCH (expected GUID = …)` against our
  `refs/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 `.cdb` script, PowerShell wrapper pattern, watchouts
  (PDB name conventions, `;` parsing, kill-target-on-detach behavior,
  high-hit-rate lag).
- **Step `-1` added 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_object`
  ratio = 0.61). Drove the L.5 fix in PlayerMovementController.
- `OBJECTINFO::kill_velocity` rarely 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:**
1. 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).
2. The PES script-hook system (`CallPESHook::Execute` →
   `CPhysicsObj::CallPES`) drives those emitters periodically, ~150
   times per minute on average.
3. 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 CallPES
- `CreateParticleHook::Execute` @ `0x00526ec0` — particle-creation hook
- `CPhysicsObj::CallPES` @ `0x00511af0`
- `CPhysicsObj::create_particle_emitter` @ `0x0050f360`
- `GameSky::CreateDeletePhysicsObjects` @ `0x005073c0`
- `LongNIHash<ParticleEmitter>` instance — emitter registry
- `CelestialPosition.pes_id` @ struct offset +0x004 — populated by
  `SkyDesc::GetSky` but consumed downstream of `GameSky` (via the
  hook system, not GameSky itself)

**Implementation outline:**
1. Decomp dive: read `CallPESHook::Execute`, `CreateParticleHook::Execute`,
   `CPhysicsObj::CallPES`, and `GameSky::CreateDeletePhysicsObjects`
   (and any cell/region weather handlers that spawn the dynamic 47).
2. Identify what triggers `CreateParticleHook` for sky objects — is it
   inside `CreateDeletePhysicsObjects`, the region/weather change handler,
   or somewhere else?
3. Port the persistent-emitter creation path: when a cell loads or
   weather/time changes, instantiate the appropriate ParticleEmitters
   on celestial objects.
4. Port the PES timeline driver — periodic dispatch from a script
   timeline into our equivalent `CallPES`.
5. Port the actual PES script execution (rate of emission, particle
   parameters, etc.) into our particle system.
6. 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 wiring
- `src/AcDream.Core/World/SkyDescLoader.cs` — already parses pes_id
- `src/AcDream.Core/Particles/*` — particle system foundation
- `src/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 split
- `src/AcDream.App/Rendering/Shaders/sky.frag` — flash/fog/lightning coloration path
- `src/AcDream.Core/World/SkyDescLoader.cs` — keep `PesObjectId` parsed for diagnostics, not render playback

**Research:**
- `docs/research/2026-04-28-pes-pseudocode.md` — C.1 correction: `CelestialPosition.pes_id` copied but ignored by GameSky
- `docs/research/2026-04-23-sky-pes-wiring.md` — earlier decompile trace reached the same no-sky-PES conclusion
- `docs/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 consumed
- `src/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` — extend `Parsed` record + walker.
- `tests/AcDream.Core.Net.Tests/PlayerDescriptionParserTests.cs` — add fixtures per section.
- `src/AcDream.Core.Net/GameEventWiring.cs` — route `parsed.Inventory` + `Equipped` to 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 out
- `src/AcDream.App/Rendering/Shaders/sky.vert` — lines 109-114, `vFogFactor` computation

**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:

```c
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:**
1. Keep `SkyObject.PesObjectId` parsed for diagnostics only.
2. Compare retail/acdream material state for the active sky/weather GfxObj/Setup ids (`0x02000588`, `0x02000589`, `0x02000714`, `0x02000BA6`).
3. Trace the named retail sky/weather draw path for texture transforms, translucency, diffusion, luminosity, and any non-GameSky weather effect dispatch.
4. Only add a new runtime visual path once the decompile has an actual caller.

**Decomp pointers:**
- `SkyDesc::GetSky` named retail `0x00501ec0` — copies `SkyObject.default_pes_object` into `CelestialPosition.pes_id`.
- `GameSky::CreateDeletePhysicsObjects` named retail `0x005073c0` — creates/updates sky objects from `gfx_id`, does not read `pes_id`.
- `GameSky::MakeObject` named retail `0x00506ee0` — calls `CPhysicsObj::makeObject(gfx_id, 0, 0)`, no PES.
- `GameSky::UseTime` named retail `0x005075b0` — updates frame/luminosity/diffusion/translucency, no PES.

**Files:**
- `src/AcDream.Core/World/SkyDescLoader.cs` — carries `PesObjectId` for 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:

1. `TranslucencyKindExtensions.FromSurfaceType` now applies retail's Translucent-override at `D3DPolyRender::SetSurface` (decomp 425246-425260) — surface `0x08000023` (Type=`0x10114` = `B1ClipMap | Translucent | Alpha | Additive`) is now correctly classified as `AlphaBlend` instead of `Additive`.
2. `SkyRenderer.EnsureSetupUploaded` now loads `0x020xxxxx` Setup IDs (e.g. `0x02000588`, `0x02000589`, `0x02000714`, `0x02000BA6`) which were silently dropped. Setup parts are flattened via `SetupMesh.Flatten` and 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 `0x010001EC` part 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 override
- `src/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` + demangled `name` + raw `mangled`)
- `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.

<!--
Example:

## #0 — [DONE 2026-04-24 · 593b76f] Sky cube edges visible as cross in daytime sky

**Closed:** 2026-04-24
**Commit:** `593b76f sky(phase-8.1): CLAUDE_TO_EDGE on static sky meshes`
**Resolution:** Switched to `GL_CLAMP_TO_EDGE` wrap mode for static sky
meshes; scrolling cloud layers kept `GL_REPEAT`. The 5 dome walls were
sampling opposite-edge pixels via UV wrap + LINEAR filtering, producing
visible seam lines that formed a cube outline across the view.
-->
