acdream/docs/ISSUES.md
Erik 449e9c3540 docs(issues): close #27 (cloud parity) — DONE-via-Fix-2
Cloud rendering parity with retail confirmed visually under Phase 0 of
the #27 fix plan: launched acdream with no DG override (LCG-picked
matches retail's pick), compared cloud coverage / color / edges /
movement at the same in-game time. User verdict: "Cloud and colors look
correct."

The original #27 observation from earlier in this session was a
side-effect of the broken `effEmissive=1.0` default that saturated every
sky mesh's vTint to white. That bug, plus the orthogonal `surface.Translucency`
plumbing gap, were both repaired in commit 4678b3e:
  - Fix 1 (Translucency): cloud surface 0x08000023 has Translucency=0.25,
    now plumbed end-to-end → clouds at 75% opacity instead of 100%.
  - Fix 2 (Luminosity): cloud surfaces have Luminosity=0.0, so post-fix
    they run through `vTint = ambient + sun·N·L` instead of saturating
    to white — clouds pick up the keyframe time-of-day tint.

User also flagged that acdream's clock is "a few minutes ahead" of retail
(sun higher on the horizon at the same wall-clock moment). That is the
existing #3 (`Client clock drifts from retail after ~10 minutes —
periodic TimeSync missing`), reproducing exactly as documented. Out of
scope for the sky-fixes branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 13:18:02 +02:00

520 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
## #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.
---
## #2 — Lightning visual not wired (dat-baked PES triggers)
**Status:** OPEN
**Severity:** MEDIUM
**Filed:** 2026-04-25
**Component:** weather / sky / vfx
**Description:** Retail's Rainy DayGroup in the Dereth Region dat contains 12+ `SkyObject` entries with non-zero `PesObjectId` and narrow visibility windows (570 ms at keyframe-boundary moments) that drive PhysicsScript-authored flash + thunder effects. We render the sky meshes but ignore the PES path, so no lightning flashes appear during storms. The fragment-shader flash bump on `uFogParams.z` is already wired in `sky.frag` — only the CPU-side PES→runner wire is missing.
**Root cause / status:** Research complete. Implementation is: in `SkyRenderer.Render`, detect visibility-window entry on any SkyObject with `obj.PesObjectId != 0`, call `PhysicsScriptRunner.Play(pesObjectId, ownerId: sky-owner, anchorPos: camera)`, and route any `SetFlash` / `Sound` hooks from the script into `uFogParams.z` + audio.
**Files:**
- `src/AcDream.App/Rendering/Sky/SkyRenderer.cs` — add per-SkyObject PES dispatch inside the visibility loop
- `src/AcDream.Core/Vfx/PhysicsScriptRunner.cs` — already shipped (Phase 6a); exposes `Play(scriptId, entityId, anchorWorldPos)`
- `src/AcDream.Core/Lighting/SceneLightingUbo.cs``FogParams.Z` is the flash slot; needs a sink that bumps it and decays
- `src/AcDream.App/Rendering/Shaders/sky.frag` — flash bump already wired (`rgb += flash * vec3(1.5, 1.5, 1.8)`)
**Research:**
- `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.
---
## #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 (02400 m) are calibrated for terrain; sky meshes are authored at radii 105014271 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 occasionally renders an aurora-borealis-style "northern lights" effect in the sky during certain weather/time conditions. acdream renders no aurora at all.
**Root cause / status:** Unknown — the mechanism hasn't been investigated. Aurora is NOT in the visible SkyObject lists (`tools/StarsProbe` shows the standard 7-object Sunny/Clear/Cloudy DayGroup composition, with extra weather objects in Rainy groups). Hypotheses: (a) it's a special PES on a low-probability DayGroup not yet enumerated; (b) it's a separate shader path not driven by `Region.SkyInfo`; (c) it requires a specific weather/time combo we haven't triggered; (d) it's an entirely separate `EnvironChangeType` system we don't decode.
**Files:** Unknown.
**Research:** None yet. Probably needs a retail-decomp grep for "aurora", "northern", a 360° survey of DayGroup PES contents, and possibly a deepdive into the `LScape::weather_enabled` and `EnvironChange*` paths.
**Acceptance:** When retail shows an aurora at a specific in-game time / weather, acdream shows a visually-comparable effect at the same time.
---
---
# Recently closed
## #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.
-->