# 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 `, `@loadui `, `@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. --- ## #30 — AutonomousPosition contact byte is too often grounded **Status:** OPEN **Severity:** HIGH **Filed:** 2026-04-29 **Component:** physics / net / movement **Description:** Outbound movement can claim grounded contact even when the local resolver result is uncertain or airborne. `AutonomousPosition.Build` defaults `lastContact` to 1, and the app path needs an audit to ensure `ResolveResult.IsOnGround` is what reaches the wire. **Root cause / status:** Tracked under Phase L.2b. This can make ACE accept movement that is not actually retail-valid and can hide edge/step-down bugs. **Files:** `src/AcDream.Core.Net/Messages/AutonomousPosition.cs`, `src/AcDream.Core.Net/Messages/MoveToState.cs`, `src/AcDream.App/Input/PlayerMovementController.cs`. **Research:** `docs/plans/2026-04-29-movement-collision-conformance.md`. **Acceptance:** Ground contact byte is derived from the current resolved movement result for both autonomous heartbeat and movement-state sends. Tests cover grounded, airborne, and failed-transition cases. --- ## #31 — Low outdoor cell id can go stale after transition movement **Status:** OPEN **Severity:** HIGH **Filed:** 2026-04-29 **Component:** physics / cells / movement **Description:** Local movement can cross 24m outdoor cell boundaries while the low cell id used for outbound full cell id remains stale. This can combine correct landblock high bits with the wrong outdoor-cell low byte. **Root cause / status:** Tracked under Phase L.2e. `CELLARRAY`, `CObjCell::find_cell_list`, adjacent-cell checks, and low-cell ownership are not fully ported. **Files:** `src/AcDream.Core/Physics/PhysicsEngine.cs`, `src/AcDream.Core/Physics/TransitionTypes.cs`, `src/AcDream.App/Input/PlayerMovementController.cs`. **Research:** `docs/plans/2026-04-29-movement-collision-conformance.md`. **Acceptance:** Crossing a 24m outdoor-cell seam updates the local resolved cell id and the outbound full cell id. Tests cover intra-landblock seams and landblock-edge seams. --- ## #32 — Retail edge-slide / cliff-slide / precipice-slide incomplete **Status:** OPEN **Severity:** HIGH **Filed:** 2026-04-29 **Component:** physics / collision **Description:** When walking along walls, roof edges, cliff edges, or failed step-down boundaries, retail often slides along the boundary. acdream still hard-blocks or accepts too much in several of these cases. **Root cause / status:** Tracked under Phase L.2c. Named retail anchors include `CTransition::edge_slide`, `CTransition::cliff_slide`, `SPHEREPATH::precipice_slide`, and `SPHEREPATH::step_up_slide`. **Files:** `src/AcDream.Core/Physics/TransitionTypes.cs`, `src/AcDream.Core/Physics/BSPQuery.cs`, `tests/AcDream.Core.Tests/`. **Research:** `docs/plans/2026-04-29-movement-collision-conformance.md`. **Acceptance:** Synthetic and real-DAT tests cover wall-slide, roof-edge slide, cliff/precipice slide, failed step-up/step-down, and the jump-clears-edge case. --- ## #33 — Live entity collision shape collapses to one cylinder **Status:** OPEN **Severity:** MEDIUM **Filed:** 2026-04-29 **Component:** physics / entities **Description:** Live world entities do not yet use exact retail `CSphere` / `CCylSphere` shape semantics. Several paths collapse the entity to a simplified root-centered cylinder or fallback radius, which is not enough for retail object and creature collision parity. **Root cause / status:** Tracked under Phase L.2d. Requires auditing object shape extraction, `Setup.Radius` fallback, building object identity, and live entity broadphase records against named retail. **Files:** `src/AcDream.Core/Physics/CollisionPrimitives.cs`, `src/AcDream.Core/Physics/ShadowObjectRegistry.cs`, `src/AcDream.Core/Physics/PhysicsDataCache.cs`. **Research:** `docs/plans/2026-04-29-movement-collision-conformance.md`. **Acceptance:** Live object collision uses the appropriate retail sphere or cylsphere data where available. Tests prove at least one multi-shape object and one live creature case no longer use the single-cylinder fallback. --- ## #34 — Missing routine local/server correction diagnostic **Status:** OPEN **Severity:** MEDIUM **Filed:** 2026-04-29 **Component:** physics / net / diagnostics **Description:** The client needs an opt-in diagnostic that logs local predicted position/contact/cell, outbound movement fields, server `UpdatePosition` echo, and correction delta. Current correction visibility is too focused on portal arrival and not enough on normal walking. **Root cause / status:** Tracked under Phase L.2a/L.2b. Without this probe, ACE's tolerance can hide local collision divergence. **Files:** `src/AcDream.Core.Net/WorldSession.cs`, `src/AcDream.App/Input/PlayerMovementController.cs`, `src/AcDream.App/Rendering/GameWindow.cs`. **Research:** `docs/plans/2026-04-29-movement-collision-conformance.md`. **Acceptance:** With the diagnostic enabled, a walking session logs local resolved placement, outbound cell/contact fields, server echo placement, and correction delta in a grep-friendly format. --- ## #2 — Lightning visual mismatch (sky PES path disproved) **Status:** OPEN **Severity:** MEDIUM **Filed:** 2026-04-25 **Component:** weather / sky / vfx **Description:** Lightning/storm sky visuals still do not match retail. A 2026-04-28 named-retail recheck disproved the prior assumption that `SkyObject.PesObjectId` drives sky-render flash particles: `SkyDesc::GetSky` copies the field into `CelestialPosition.pes_id`, but `GameSky::CreateDeletePhysicsObjects`, `GameSky::MakeObject`, and `GameSky::UseTime` never read it. **Root cause / status:** Open again. The sky-PES path is non-retail and must stay disabled for normal rendering. The remaining mismatch likely lives in the sky/weather mesh material path, the lightning/fog flash path, or another weather subsystem outside `GameSky`; do not reintroduce per-SkyObject PES playback without new decompile evidence. **Files:** - `src/AcDream.App/Rendering/Sky/SkyRenderer.cs` — sky/weather mesh draw, material state, pre/post split - `src/AcDream.App/Rendering/Shaders/sky.frag` — flash/fog/lightning coloration path - `src/AcDream.Core/World/SkyDescLoader.cs` — keep `PesObjectId` parsed for diagnostics, not render playback **Research:** - `docs/research/2026-04-28-pes-pseudocode.md` — C.1 correction: `CelestialPosition.pes_id` copied but ignored by GameSky - `docs/research/2026-04-23-sky-pes-wiring.md` — earlier decompile trace reached the same no-sky-PES conclusion - `docs/research/2026-04-23-lightning-real.md` (decompile trace + dat discovery) - `docs/research/2026-04-23-physicsscript.md` (runtime semantics) - `docs/research/2026-04-23-lightning-crossfade.md` (crossfade mechanism) **Acceptance:** During a Rainy DayGroup's storm window, visible flashes appear in the sky at the dat-scripted moments, the fragment-shader flash bump briefly brightens the scene, and (later, once thunder audio is wired) a thunder clap plays with a short propagation delay. --- ## #3 — Client clock drifts from retail after ~10 minutes (periodic TimeSync missing) **Status:** OPEN **Severity:** MEDIUM **Filed:** 2026-04-25 **Component:** net / sky **Description:** Our `WorldTimeService.DayFraction` syncs with the server once at login via `ConnectRequest + TimeSync`, then advances from the local wall-clock. Retail receives periodic `TimeSync` refreshes (header flag `0x1000000`) carrying a fresh `PortalYearTicks double` and re-anchors its clock. Without those, acdream's keyframe state drifts from retail's over 10+ minutes — observed during the 2026-04-24 sky-color debug sessions where retail was at DayFraction 0.976 while acdream was at 0.634. **Root cause / status:** Mechanism is well-understood (see research). `WorldTimeService.SyncFromServer(double)` already exists — we just need to detect the periodic flag in the packet header and call it whenever a fresh tick arrives. **Files:** - `src/AcDream.Core.Net/WorldSession.cs` — header-flag parsing; currently only the initial sync is consumed - `src/AcDream.Core/World/WorldTimeService.cs` — `SyncFromServer(double ticks)` ready; needs caller wiring **Research:** `docs/research/deepdives/r12-weather-daynight.md` §TimeSync (line ~563). References retail packet-header flag `0x1000000` carrying `PortalYearTicks double`. **Acceptance:** Probe retail via `tools/RetailTimeProbe` and acdream's ACDREAM_DUMP_SKY log at the same wall-clock moment after a 20-minute session without re-login; `abs(acdream.DayFraction - retail.DayFraction) < 0.01`. --- --- ## #13 — PlayerDescription trailer past enchantments (options / shortcuts / hotbars / desired_comps / spellbook_filters / options2 / gameplay_options / inventory / equipped) **Status:** OPEN **Severity:** LOW (no current user-visible bug; future panels will need the data) **Filed:** 2026-04-25 **Component:** net / player-state **Description:** `PlayerDescriptionParser` walks through enchantments (Phase H, 2026-04-25). The trailer beyond that — Options1 / Shortcuts / HotbarSpells (8 lists) / DesiredComps / SpellbookFilters / Options2 / GameplayOptions blob / Inventory / Equipped — is not yet parsed. Required for future Spellbook UI panel, hotbar UI, inventory UI, character options panel. **Root cause / status:** Holtburger `events.rs:462-625` has the full layout. The trickiest piece is `gameplay_options` — a variable-length opaque blob; holtburger uses a heuristic forward search (`find_inventory_start_after_gameplay_options`) for plausibly-aligned inventory-count + GUID pairs to find the inventory start. Other sections are well-formed. **Files:** - `src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs` — extend `Parsed` record + walker. - `tests/AcDream.Core.Net.Tests/PlayerDescriptionParserTests.cs` — add fixtures per section. - `src/AcDream.Core.Net/GameEventWiring.cs` — route `parsed.Inventory` + `Equipped` to ItemRepository. **Research:** holtburger `events.rs:462-625`; `references/actestclient/TestClient/messages.xml`. **Acceptance:** All sections of a real-world PlayerDescription parse to completion (no truncation). New tests cover synthetic fixtures per section. `ItemRepository.Count` after login > 0. --- --- ## #4 — Sky horizon-glow disabled (fog-mix skipped on sky meshes) **Status:** OPEN **Severity:** LOW (aesthetic feature-parity, not regression from pre-session state) **Filed:** 2026-04-25 **Component:** sky **Description:** Phase 8.1 (commit `593b76f`) disabled the fog-mix on sky meshes to fix the "entire dome swallowed by fog color" regression. Dereth's keyframe `FogEnd` values (0–2400 m) are calibrated for terrain; sky meshes are authored at radii 1050–14271 m so every sky pixel was past `FogEnd`, saturated to `uFogColor`, destroying stars / moon / dome texture. Disabling the mix restored visibility but we lost retail's horizon-glow effect (gradient from clear zenith to fog-tinted horizon band at dusk/dawn). **Root cause / status:** Three competing hypotheses, none pinned down: (a) retail uses a **different** fog range for sky than terrain; (b) retail applies fog with an **elevation-angle** weighting rather than linear distance; (c) retail's sky meshes **don't participate** in the global fog and the "horizon glow" comes from a different atmospheric-scatter path. Need to identify retail's actual sky-fog behaviour before re-enabling with correct parameters. **Files:** - `src/AcDream.App/Rendering/Shaders/sky.frag` — line ~55, `rgb = mix(uFogColor.rgb, rgb, vFogFactor)` currently commented out - `src/AcDream.App/Rendering/Shaders/sky.vert` — lines 109-114, `vFogFactor` computation **Research:** `docs/research/2026-04-23-sky-fog.md`. Partial; doesn't pin the sky-specific fog path. **Acceptance:** At dusk in Holtburg, the sky dome shows a clear zenith and a warm fog-tinted horizon band that matches retail's appearance, with stars / moon / sun / clouds all still visible at their correct brightnesses elsewhere in the frame. --- ## #28 — Aurora ("northern lights") effect not rendered **Status:** OPEN **Severity:** LOW (aesthetic feature-parity) **Filed:** 2026-04-26 **Component:** sky / vfx **Description:** Retail renders a dynamic colored "light play" effect in the sky during certain Rainy/Cloudy DayGroup time windows. The user describes it as aurora-borealis-style. acdream renders no comparable effect. **Root cause / status:** Open again. The prior root cause was wrong: `CelestialPosition.pes_id` exists in the retail header and is populated by `SkyDesc::GetSky`, but named retail `GameSky` code does not read it during sky object creation, update, or draw. A 2026-04-28 C.1 experiment that played those PES ids produced colored blobs/wash that did not match retail's broad aurora-like rays, and the path is now debug-only behind `ACDREAM_ENABLE_SKY_PES=1`. Retail header at `acclient.h` line 35451 still documents the copied field: ```c struct CelestialPosition { IDClass<...> gfx_id; IDClass<...> pes_id; // ← particle scheduler ID float heading; float rotation; Vector3 tex_velocity; float transparent; float luminosity; float max_bright; unsigned int properties; }; ``` `StarsProbe` confirmed Dereth Rainy DayGroup 3 carries multiple PES-bearing entries (verified 2026-04-27). Sample for the user's observed Warmtide-Rainy state: | OI | Gfx | **PES** | Active window | Notes | |----|-----|---------|----|----| | 5 | 0x02000714 | 0x330007DB | always | low-rate background | | 7 | 0x02000BA6 | 0x33000453 | 0.03–0.19 | early morning | | 17 | 0x02000589 | **0x3300042C** | **0.27–0.91** | **active during user's screenshot** | acdream's geometry half is now wired (commit landing 2026-04-27 — `EnsureSetupUploaded` walks `Setup.Parts` for `0x020xxx` IDs). The remaining dynamic visual half is not `SkyObject.PesObjectId`; likely suspects are sky/weather mesh material state, texture transform/blending, or a separate weather/lightning subsystem outside `GameSky`. **Implementation outline:** 1. Keep `SkyObject.PesObjectId` parsed for diagnostics only. 2. Compare retail/acdream material state for the active sky/weather GfxObj/Setup ids (`0x02000588`, `0x02000589`, `0x02000714`, `0x02000BA6`). 3. Trace the named retail sky/weather draw path for texture transforms, translucency, diffusion, luminosity, and any non-GameSky weather effect dispatch. 4. Only add a new runtime visual path once the decompile has an actual caller. **Decomp pointers:** - `SkyDesc::GetSky` named retail `0x00501ec0` — copies `SkyObject.default_pes_object` into `CelestialPosition.pes_id`. - `GameSky::CreateDeletePhysicsObjects` named retail `0x005073c0` — creates/updates sky objects from `gfx_id`, does not read `pes_id`. - `GameSky::MakeObject` named retail `0x00506ee0` — calls `CPhysicsObj::makeObject(gfx_id, 0, 0)`, no PES. - `GameSky::UseTime` named retail `0x005075b0` — updates frame/luminosity/diffusion/translucency, no PES. **Files:** - `src/AcDream.Core/World/SkyDescLoader.cs` — carries `PesObjectId` for diagnostics. - `src/AcDream.App/Rendering/Sky/SkyRenderer.cs` — likely material/texture-transform parity work. - `src/AcDream.App/Rendering/GameWindow.cs` — sky-PES playback remains debug-only, disabled by default. **Acceptance:** When retail shows aurora-style light play at a specific in-game time / weather, acdream shows a visually-comparable effect at the same time. --- ## #29 — Cloud surface 0x08000023 still appears thinner than retail despite blend-mode + Setup fixes **Status:** OPEN **Severity:** LOW (aesthetic feature-parity) **Filed:** 2026-04-27 **Component:** sky / clouds **Description:** User screenshot comparison showed acdream's clouds let too much sun through; retail's are denser and have a purpleish tint. Two follow-up fixes landed without visible improvement: 1. `TranslucencyKindExtensions.FromSurfaceType` now applies retail's Translucent-override at `D3DPolyRender::SetSurface` (decomp 425246-425260) — surface `0x08000023` (Type=`0x10114` = `B1ClipMap | Translucent | Alpha | Additive`) is now correctly classified as `AlphaBlend` instead of `Additive`. 2. `SkyRenderer.EnsureSetupUploaded` now loads `0x020xxxxx` Setup IDs (e.g. `0x02000588`, `0x02000589`, `0x02000714`, `0x02000BA6`) which were silently dropped. Setup parts are flattened via `SetupMesh.Flatten` and uploaded with their per-part transform baked into vertex positions. Despite both being decomp-correct fixes, the user reports no observable visual change in dual-client comparison. Two follow-up hypotheses: - The Setup objects are tiny placeholder meshes (one `0x010001EC` part each) that exist mainly to anchor a PES emitter — the cloud "density" / "purple sheen" the user perceives is entirely the PES particle layer, not the static mesh. - The cloud surface might still be rendering correctly per its dat data, and what looks "thicker" in retail is the additional aurora-like PES sheen overlaid on top. If hypothesis (a) is correct, this issue effectively rolls into **#28** — the PES rendering work would resolve both. **Files:** - `src/AcDream.Core/Meshing/TranslucencyKind.cs` — Translucent override - `src/AcDream.App/Rendering/Sky/SkyRenderer.cs` — `EnsureSetupUploaded` **Acceptance:** Cloud sheets look as dense/purple as retail in dual-client side-by-side. May require #28 (PES) to land first. --- --- # 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 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, 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 `` 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@@` 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.