Updated #28 (aurora effect) from "unknown root cause" to "PES particles attached via CelestialPosition.pes_id". Includes the verbatim retail header struct, the StarsProbe-confirmed list of PES-bearing entries in Dereth Rainy DG3 (notably PES 0x3300042C active 0.27-0.91, which is the user's Warmtide screenshot), the implementation outline, and decomp pointers to CPhysicsObj::InitPartArrayObject + CPartArray::CreateSetup. Filed #29 for the residual cloud-density gap that remained after this session's Translucent-override fix (commit375065b) and Setup wiring (commit646ccca). Two follow-up hypotheses captured — likely rolls into #28 once PES rendering lands.
34 KiB
acdream — known issues + small deferred features
Rolling tactical list. What goes here:
- Bugs: user-visible defects we've observed but haven't fixed yet.
- Small deferred features: work that fits in one or two commits. Anything larger should be a named Phase in the roadmap.
What does NOT go here:
- Large multi-commit work → add a Phase to the roadmap instead.
- Ideas / wishlist →
docs/plans/. - Design questions → open a
docs/research/*.mdnote.
Conventions
- Sequential integer IDs (
#1,#2, …). Commits that close an issue reference the ID in the message (e.g.fix #3: periodic TimeSync parsing). StatusisOPEN,IN-PROGRESS, orDONE. DONE items move to the Recently closed section at the bottom with closed-date + commit SHA.- Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending.
- Promoting to a Phase: mark as
DONE (promoted to Phase X)+ commit SHA where the Phase entry landed.
Template
Copy this block when adding a new issue:
## #NN — Short title
**Status:** OPEN
**Severity:** HIGH | MEDIUM | LOW
**Filed:** YYYY-MM-DD
**Component:** e.g. sky, physics, net, ui
**Description:** One paragraph — what's wrong or what's missing.
**Root cause / status:** What we know so far. Empty if unknown.
**Files:** Path references with approximate line numbers.
**Research:** Links to `docs/research/*.md` if applicable.
**Acceptance:** How we'll know it's fixed.
Active issues
#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 (5–70 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 loopsrc/AcDream.Core/Vfx/PhysicsScriptRunner.cs— already shipped (Phase 6a); exposesPlay(scriptId, entityId, anchorWorldPos)src/AcDream.Core/Lighting/SceneLightingUbo.cs—FogParams.Zis the flash slot; needs a sink that bumps it and decayssrc/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 consumedsrc/AcDream.Core/World/WorldTimeService.cs—SyncFromServer(double ticks)ready; needs caller wiring
Research: docs/research/deepdives/r12-weather-daynight.md §TimeSync (line ~563). References retail packet-header flag 0x1000000 carrying PortalYearTicks double.
Acceptance: Probe retail via tools/RetailTimeProbe and acdream's ACDREAM_DUMP_SKY log at the same wall-clock moment after a 20-minute session without re-login; abs(acdream.DayFraction - retail.DayFraction) < 0.01.
#13 — PlayerDescription trailer past enchantments (options / shortcuts / hotbars / desired_comps / spellbook_filters / options2 / gameplay_options / inventory / equipped)
Status: OPEN Severity: LOW (no current user-visible bug; future panels will need the data) Filed: 2026-04-25 Component: net / player-state
Description: PlayerDescriptionParser walks through enchantments (Phase H, 2026-04-25). The trailer beyond that — Options1 / Shortcuts / HotbarSpells (8 lists) / DesiredComps / SpellbookFilters / Options2 / GameplayOptions blob / Inventory / Equipped — is not yet parsed. Required for future Spellbook UI panel, hotbar UI, inventory UI, character options panel.
Root cause / status: Holtburger events.rs:462-625 has the full layout. The trickiest piece is gameplay_options — a variable-length opaque blob; holtburger uses a heuristic forward search (find_inventory_start_after_gameplay_options) for plausibly-aligned inventory-count + GUID pairs to find the inventory start. Other sections are well-formed.
Files:
src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs— extendParsedrecord + walker.tests/AcDream.Core.Net.Tests/PlayerDescriptionParserTests.cs— add fixtures per section.src/AcDream.Core.Net/GameEventWiring.cs— routeparsed.Inventory+Equippedto ItemRepository.
Research: holtburger events.rs:462-625; references/actestclient/TestClient/messages.xml.
Acceptance: All sections of a real-world PlayerDescription parse to completion (no truncation). New tests cover synthetic fixtures per section. ItemRepository.Count after login > 0.
#4 — Sky horizon-glow disabled (fog-mix skipped on sky meshes)
Status: OPEN Severity: LOW (aesthetic feature-parity, not regression from pre-session state) Filed: 2026-04-25 Component: sky
Description: Phase 8.1 (commit 593b76f) disabled the fog-mix on sky meshes to fix the "entire dome swallowed by fog color" regression. Dereth's keyframe FogEnd values (0–2400 m) are calibrated for terrain; sky meshes are authored at radii 1050–14271 m so every sky pixel was past FogEnd, saturated to uFogColor, destroying stars / moon / dome texture. Disabling the mix restored visibility but we lost retail's horizon-glow effect (gradient from clear zenith to fog-tinted horizon band at dusk/dawn).
Root cause / status: Three competing hypotheses, none pinned down: (a) retail uses a different fog range for sky than terrain; (b) retail applies fog with an elevation-angle weighting rather than linear distance; (c) retail's sky meshes don't participate in the global fog and the "horizon glow" comes from a different atmospheric-scatter path. Need to identify retail's actual sky-fog behaviour before re-enabling with correct parameters.
Files:
src/AcDream.App/Rendering/Shaders/sky.frag— line ~55,rgb = mix(uFogColor.rgb, rgb, vFogFactor)currently commented outsrc/AcDream.App/Rendering/Shaders/sky.vert— lines 109-114,vFogFactorcomputation
Research: docs/research/2026-04-23-sky-fog.md. Partial; doesn't pin the sky-specific fog path.
Acceptance: At dusk in Holtburg, the sky dome shows a clear zenith and a warm fog-tinted horizon band that matches retail's appearance, with stars / moon / sun / clouds all still visible at their correct brightnesses elsewhere in the frame.
#28 — Aurora ("northern lights") effect not rendered
Status: OPEN Severity: LOW (aesthetic feature-parity) Filed: 2026-04-26 Component: sky / vfx
Description: Retail renders a dynamic colored "light play" effect in the sky during certain Rainy/Cloudy DayGroup time windows. The user describes it as aurora-borealis-style. acdream renders no comparable effect.
Root cause: PES (Particle Effect Schedule) particles attached to SkyObjects via the CelestialPosition.pes_id field. Retail header at acclient.h line 35451 (verbatim):
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 dynamic visual half — emitting and animating the PES particles — is unimplemented and provides the actual aurora look. Phase E.3 already has data-only PES support per memory crib project_session_2026_04_18.md; this issue requires the runtime + visual half.
Implementation outline:
- PES dat decode (already partially in
AcDream.Core.World.PesDataper Phase E.3). - PES emitter runtime — schedule, spawn, advect, color-cycle, expire each particle.
SkyRendererintegration — whenMakeObjectseespes_id != 0, spawn the PES at the SkyObject's celestial position.- PES vertex-sprite renderer — billboarded textured quads with additive blending and color cycling. Probably reuses the future general-purpose particle renderer (issue #L? — TBD).
Decomp pointers:
CPhysicsObj::InitPartArrayObjectdecomp ~280484 — dispatches type 7 to Setup loader.CPartArray::CreateSetupdecomp ~287490 — Setup → Parts → optional PES wiring.
Files:
src/AcDream.Core/World/SkyDescLoader.cs—SkyObjectDataneeds to carryPesObjectId(currently dropped on the floor).src/AcDream.App/Rendering/Sky/SkyRenderer.cs— needs a particle-emission step alongside the per-SkyObject mesh draw.
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.
#29 — Cloud surface 0x08000023 still appears thinner than retail despite blend-mode + Setup fixes
Status: OPEN Severity: LOW (aesthetic feature-parity) Filed: 2026-04-27 Component: sky / clouds
Description: User screenshot comparison showed acdream's clouds let too much sun through; retail's are denser and have a purpleish tint. Two follow-up fixes landed without visible improvement:
TranslucencyKindExtensions.FromSurfaceTypenow applies retail's Translucent-override atD3DPolyRender::SetSurface(decomp 425246-425260) — surface0x08000023(Type=0x10114=B1ClipMap | Translucent | Alpha | Additive) is now correctly classified asAlphaBlendinstead ofAdditive.SkyRenderer.EnsureSetupUploadednow loads0x020xxxxxSetup IDs (e.g.0x02000588,0x02000589,0x02000714,0x02000BA6) which were silently dropped. Setup parts are flattened viaSetupMesh.Flattenand uploaded with their per-part transform baked into vertex positions.
Despite both being decomp-correct fixes, the user reports no observable visual change in dual-client comparison. Two follow-up hypotheses:
- The Setup objects are tiny placeholder meshes (one
0x010001ECpart each) that exist mainly to anchor a PES emitter — the cloud "density" / "purple sheen" the user perceives is entirely the PES particle layer, not the static mesh. - The cloud surface might still be rendering correctly per its dat data, and what looks "thicker" in retail is the additional aurora-like PES sheen overlaid on top.
If hypothesis (a) is correct, this issue effectively rolls into #28 — the PES rendering work would resolve both.
Files:
src/AcDream.Core/Meshing/TranslucencyKind.cs— Translucent overridesrc/AcDream.App/Rendering/Sky/SkyRenderer.cs—EnsureSetupUploaded
Acceptance: Cloud sheets look as dense/purple as retail in dual-client side-by-side. May require #28 (PES) to land first.
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+ demangledname+ rawmangled)types.json— 5,371 named class/struct records (name+size+kind)
Best-effort MSVC C++ demangler handles the common ?Method@Class@@<sig> patterns + ctors (??0) + dtors (??1); operator overloads and vtables left mangled. Spot-check verified: CEnchantmentRegistry::EnchantAttribute resolves to 0x00594570 exactly as the discovery agent reported. Runtime <1s.
Regen workflow: py tools/pdb-extract/pdb_extract.py refs/acclient.pdb. The committed JSON outputs are stable + ~3 MB combined; ripgrep/jq on them is faster than re-parsing.
#5 — [DONE 2026-04-25] VitalsPanel stamina/mana bars always null
Closed: 2026-04-25
Commit: feat(player): #5 PlayerDescription parser — Stam/Mana via attribute block
Resolution: First attempt (commit d42bf57) used AppraiseInfoParser for PlayerDescription (0x0013) — wrong wire format. ACE source confirmed via GameEventPlayerDescription.WriteEventBody: PlayerDescription is hand-written (DescriptionPropertyFlag-driven property hashtables, vector flags, attribute block, skills, spells, options/inventory tail) — distinct from IdentifyObjectResponse (0x00C9)'s AppraiseInfo.Write. Pivoted to a real port: new PlayerDescriptionParser.cs that walks property hashtables (Int32/Int64/Bool/Double/String/Did/Iid + Position) gated on the property flags, then reads vector flags + has_health + the attribute block where vitals 7/8/9 carry ranks/start/xp/current. Also redesigned LocalPlayerState to track per-vital snapshots (replacing the sentinel-API of attempt 1) plus per-attribute snapshots, with GetMaxApprox applying the retail formula vital.(ranks+start) + attribute_contribution (Endurance/2 for Health, Endurance for Stamina, Self for Mana). Live verified: +Acdream shows three bars; ~95% reading on Stam/Mana traced to active buff multipliers (filed as #6). Wire-port also added PrivateUpdateVital (0x02E7) + PrivateUpdateVitalCurrent (0x02E9) for delta updates per holtburger UpdateVital. ~700 LOC C#, 30+ new tests.