acdream/docs/ISSUES.md
Erik 9567597814 docs(issues): close #26 (stars-as-square) + open #27 (clouds), #28 (aurora)
Bug B from the sky-investigation handoff is fixed in 7b88fde — file the
Recently closed entry. Two new observations from the visual-verify
session that the user flagged when they could finally see the sky
clearly: cloud coverage looks faint vs retail, aurora ("northern
lights") not rendered at all. Both LOW severity (aesthetic feature
parity, not gameplay-breaking) and out of scope for the current
worktree, which is heading to Bug A (foreground rain, #1) next per
docs/research/2026-04-26-sky-investigation-handoff.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 22:57:25 +02:00

32 KiB
Raw Blame History

acdream — known issues + small deferred features

Rolling tactical list. What goes here:

  • Bugs: user-visible defects we've observed but haven't fixed yet.
  • Small deferred features: work that fits in one or two commits. Anything larger should be a named Phase in the roadmap.

What does NOT go here:

  • Large multi-commit work → add a Phase to the roadmap instead.
  • Ideas / wishlist → docs/plans/.
  • Design questions → open a docs/research/*.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.


#1 — Rain falls only to horizon, not to the player's feet

Status: OPEN Severity: MEDIUM Filed: 2026-04-25 Component: weather / particles

Description: During Rainy DayGroups, rain particles are visible in the upper sky band but fade out before reaching the camera / ground level. Retail's rain falls all the way past the camera to the terrain.

Root cause / status: Unknown. Likely one of: (a) particle emitter volume too short in Z, (b) particle lifetime shorter than the time it takes to traverse emitter-top → ground, (c) emitter anchored in world-space so particles escape the player's reference frame as they fall, (d) camera-relative spawn origin is offset too high above the player.

Files:

  • src/AcDream.App/Rendering/GameWindow.csUpdateWeatherParticles (~line 4591)
  • src/AcDream.Core/Vfx/ParticleSystem.cs — emitter spawn config + lifetime integration

Research: docs/research/deepdives/r12-weather-daynight.md (rain mechanism — but does not pin volume / lifetime values).

Acceptance: Standing at 9,115 in Holtburg during a Rainy DayGroup, rain drops visibly fall all the way from the sky band past the camera to the ground level.


#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.csFogParams.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.csSyncFromServer(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.


#27 — Cloud meshes appear missing or faint compared to retail

Status: OPEN Severity: LOW (aesthetic feature-parity — doesn't break gameplay) Filed: 2026-04-26 Component: sky / clouds

Description: After fixing Bug B (#26 — stars-as-square), the user observed during visual verification that cloud coverage in the sky doesn't match retail. Cloud meshes are authored in the dat (e.g. 0x010015B6, 0x01004C35-0x01004C38, 0x01004C36 etc) and tools/StarsProbe confirms they're loaded into the SkyObject lists with non-zero TexVelocity (so they get GL_REPEAT correctly under the post-#26 code path). They're not strictly missing — they're rendered — but their visual presence falls short of retail.

Root cause / status: Unknown. Hypotheses: (a) cloud surfaces' alpha/blend mode is too subtle (cloud surface flags or shader path under-emphasise the texture); (b) cloud meshes positioned/scaled wrong relative to the dome so they're inside the dome and occluded; (c) DayGroup keyframe interpolation suppresses cloud transparency at certain times of day; (d) some cloud SkyObjects we should be rendering are filtered out by a Properties bit we mis-handle (Props=0x02 might mean something more than "cloud — render it"); (e) retail uses an additive cloud blend that our Translucency classifier doesn't apply.

Files:

  • src/AcDream.App/Rendering/Sky/SkyRenderer.cs — sky pass; check per-cloud blend / luminosity / transparency.
  • src/AcDream.Core/World/SkyDescLoader.cs — Properties bit decoding.
  • src/AcDream.App/Rendering/Shaders/sky.frag — cloud transparency math.
  • tools/StarsProbe/ — already dumps cloud GfxObj UVs + bounds; extend to dump per-DayGroup cloud surface flags.

Research: None yet. tools/StarsProbe output already enumerates which DayGroups reference which cloud meshes — start there.

Acceptance: Side-by-side launch of acdream and a retail client at the same ACDREAM_DAY_GROUP shows visually-comparable cloud coverage in the sky.


#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

#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.