Files four new issues created by the 2026-04-25 PDB-discovery sprint:
#8 (DONE 2026-04-25) — pdb-extract tool, shipped 69d884a
#9 (OPEN) — function-map address-correction sweep
(Phase E will close)
#10 (DONE 2026-04-25) — wire KillerNotification (0x01AD); orphan
parser at GameEvents.ParseKillerNotification
existed but was never registered. This commit
adds CombatState.OnKillerNotification +
KillLanded event, registers the dispatcher
handler, and adds a regression test.
#11 (OPEN) — spell metadata loader (spells.csv → SpellTable)
(Phase F will close)
Code change is minimal — three lines of dispatch + a 12-line
CombatState method with a typed event for future killfeed UI.
818 tests passing (+1 KillerNotification).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
18 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
#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.cs—UpdateWeatherParticles(~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 (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.
#6 — Vital max ignores enchantment buffs + vitae
Status: OPEN Severity: LOW (3-5% accuracy gap on a HUD bar) Filed: 2026-04-25 Component: ui / player-state
Description: LocalPlayerState.GetMaxApprox computes the unenchanted base max for HP/Stam/Mana — vital.(ranks+start) + attribute_contribution with retail's hardcoded coefficients (Endurance/2, Endurance, Self). Live test shows bars at ~95% when buffs are presumably active (server character is +Acdream, GM-marker char with likely buff stack). Holtburger's calculate_vital_current adds × multiplier + additive from the active enchantment list — that's the missing 5%.
Root cause / status: Need to fold Spellbook.ActiveEnchantments into the max calc. Holtburger's magic.rs aggregates by EnchantmentTypeFlags::SECOND_ATT masked with the vital id. The same data already arrives via MagicUpdateEnchantment events that we wire into Spellbook.
Files:
src/AcDream.Core/Player/LocalPlayerState.cs—GetMaxApproxreturns the base; extend to also callSpellbookfor vital-typed enchantment aggregation.src/AcDream.Core/Spells/Spellbook.cs— needs an aggregator helper similar to holtburger'sget_vital_multiplier/get_vital_additive.
Research: holtburger crates/holtburger-world/src/player/stats_calc.rs:91-111, magic.rs get_enchantment_multiplier / additive.
Acceptance: A +Acdream login shows Stam/Mana percent within 1% of retail's reading once any active buff multipliers are applied. (HP already at 100% indicates the unbuffed Health formula is already correct on its own.)
#7 — PlayerDescription parser stops after spells (options/inventory/equipped not extracted)
Status: OPEN Severity: LOW (Issue #5 needed only the early sections; later panels will need the rest) Filed: 2026-04-25 Component: net / player-state
Description: Current PlayerDescriptionParser walks through Attribute / Skill / Spell vector-flag blocks per holtburger but stops before the trailing options / shortcuts / hotbars / desired_comps / spellbook_filters / options2 / gameplay_options / inventory / equipped sections. Future panels (hotbar, inventory, character options) need that data; the parser will need extension. Holtburger's events.rs:462-625 has the full layout; the messy parts are gameplay_options (variable-length opaque blob requiring heuristic skip) and desired_comps.
Root cause / status: Just a scope decision — port-the-easy-bits-first approach. Reference shape lives in holtburger's full unpack; the only complex piece is find_inventory_start_after_gameplay_options (heuristic alignment search).
Files:
src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs— extendParsedrecord + walker.tests/AcDream.Core.Net.Tests/PlayerDescriptionParserTests.cs— add coverage with synthetic payloads of the trailing sections.
Research: holtburger crates/holtburger-protocol/src/messages/player/events.rs:462-625 (full unpacker including the heuristic find_inventory_start_after_gameplay_options).
Acceptance: All sections of a real-world PlayerDescription parse to completion — verified via a packet capture or by feeding synthetic test fixtures covering every flag combination.
#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.
#9 — Address-correction sweep on acclient_function_map.md
Status: OPEN Severity: LOW (per-developer convenience; gets us correct symbol→address mapping for ~71 hand-curated entries) Filed: 2026-04-25 Component: docs / research
Description: The hand-curated function map at docs/research/acclient_function_map.md has ~71 entries with addresses derived from older Ghidra chunk inspection. The PDB-extracted docs/research/named-retail/symbols.json (Sept 2013 EoR build) is now the authoritative name-source. Several entries point at mid-function offsets rather than function starts. E.g. our FUN_005111d0 = UpdatePhysicsInternal — actual start in PDB is 0x510700. Need a sweep that verifies + corrects all hand-curated rows.
Root cause / status: PDB built from a slightly different revision (~0xC00 byte delta on some functions); legacy Ghidra-derived addresses don't all line up. Match by name (PDB names are ground truth) and record the corrected address.
Files:
docs/research/acclient_function_map.md— corrections in-place.docs/research/named-retail/symbols.json— name→address lookup source.
Acceptance: Spot-check 10 entries across all sections — each row's address matches symbols.json for the named function. Mismatches annotated (corrected from FUN_xxx, was mid-body).
#11 — Spell metadata loader (spells.csv → SpellTable)
Status: OPEN Severity: LOW (unblocks issue #6's stacking aggregation; also adds tooltip / icon / school metadata for future panels) Filed: 2026-04-25 Component: core / spells
Description: docs/research/data/spells.csv (3,956 spells × 35 cols) has all the per-spell metadata the existing Spellbook lacks: Name, School, Family (buff stacking bucket), IconId, Mana, Duration, IsDebuff, IsFellowship, Description. Need a SpellTable loader that hydrates a Dictionary<uint, SpellMetadata> at startup so Spellbook.TryGetMetadata(spellId, out) works.
Root cause / status: Issue #6 (vital max ignores enchantment buffs) needs Family to do correct stacking aggregation (only one buff per family wins; highest generation). That field comes only from spells.csv.
Files:
src/AcDream.Core/Spells/SpellMetadata.cs(new record).src/AcDream.Core/Spells/SpellTable.cs(new loader).src/AcDream.App/Rendering/GameWindow.cs(load at OnLoad).src/AcDream.App/AcDream.App.csproj(<None Update>to copy CSV to bin output).src/AcDream.Core/Spells/Spellbook.cs(accept optionalSpellTable, exposeTryGetMetadata).
Acceptance: Launch with ACDREAM_DEVTOOLS=1 shows console line spells: loaded 3956 entries from spells.csv. Spellbook.TryGetMetadata(spellId, out) returns valid record for active enchantment lookups.
Recently closed
#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.