diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 668603a..5c0e7fb 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -177,10 +177,73 @@ Copy this block when adding a new issue: --- +## #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` 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` (`` to copy CSV to bin output). +- `src/AcDream.Core/Spells/Spellbook.cs` (accept optional `SpellTable`, expose `TryGetMetadata`). + +**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` + 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 diff --git a/src/AcDream.Core.Net/GameEventWiring.cs b/src/AcDream.Core.Net/GameEventWiring.cs index f5b6a3e..e194f8b 100644 --- a/src/AcDream.Core.Net/GameEventWiring.cs +++ b/src/AcDream.Core.Net/GameEventWiring.cs @@ -107,6 +107,13 @@ public static class GameEventWiring var p = GameEvents.ParseAttackDone(e.Payload.Span); if (p is not null) combat.OnAttackDone(p.Value.AttackSequence, p.Value.WeenieError); }); + dispatcher.Register(GameEventType.KillerNotification, e => + { + // ISSUES.md #10 — orphan parser, never registered before. The + // server fires this after a player lands a killing blow. + var p = GameEvents.ParseKillerNotification(e.Payload.Span); + if (p is not null) combat.OnKillerNotification(p.Value.VictimName, p.Value.VictimGuid); + }); // ── Spells ──────────────────────────────────────────────── dispatcher.Register(GameEventType.MagicUpdateSpell, e => diff --git a/src/AcDream.Core/Combat/CombatState.cs b/src/AcDream.Core/Combat/CombatState.cs index 9308a13..93a5094 100644 --- a/src/AcDream.Core/Combat/CombatState.cs +++ b/src/AcDream.Core/Combat/CombatState.cs @@ -57,6 +57,14 @@ public sealed class CombatState /// An attack commit completed (0x01A7). WeenieError = 0 on success. public event Action? AttackDone; + /// + /// Fires when the server confirms the player landed a killing blow + /// (GameEvent KillerNotification (0x01AD)). Event payload is + /// the victim's display name + their server GUID. Used by killfeed UI + /// (future panel) and any plugin scoring kill counts. + /// + public event Action? KillLanded; + public readonly record struct DamageIncoming( string AttackerName, uint AttackerGuid, @@ -116,6 +124,16 @@ public sealed class CombatState public void OnEvasionAttackerNotification(string defenderName) => MissedOutgoing?.Invoke(defenderName); + /// + /// Server confirmation that the player landed a killing blow on a + /// target. Wire source: GameEvent KillerNotification (0x01AD) + /// — the parser at GameEvents.ParseKillerNotification shipped + /// alongside victim/defender notifications but was never registered + /// for dispatch until 2026-04-25 (per ISSUES.md #10). + /// + public void OnKillerNotification(string victimName, uint victimGuid) + => KillLanded?.Invoke(victimName, victimGuid); + public void OnEvasionDefenderNotification(string attackerName) => EvadedIncoming?.Invoke(attackerName); diff --git a/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs b/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs index dd0782f..cf73296 100644 --- a/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs +++ b/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs @@ -240,6 +240,31 @@ public sealed class GameEventWiringTests BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(p), xp); p += 4; } + [Fact] + public void WireAll_KillerNotification_FiresKillLandedOnCombatState() + { + // Issue #10 — orphan parser at GameEvents.ParseKillerNotification + // existed but was never registered for dispatch until 2026-04-25. + // Now wired: 0x01AD lands on CombatState.OnKillerNotification + + // fires the KillLanded event. + var (d, _, combat, _, _) = MakeAll(); + string? gotVictimName = null; + uint gotVictimGuid = 0; + combat.KillLanded += (name, guid) => { gotVictimName = name; gotVictimGuid = guid; }; + + // Wire shape: string16L victimName + u32 victimGuid + byte[] nameBytes = MakeString16L("Drudge"); + byte[] payload = new byte[nameBytes.Length + 4]; + Array.Copy(nameBytes, payload, nameBytes.Length); + BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(nameBytes.Length), 0x80001234u); + + var env = GameEventEnvelope.TryParse(WrapEnvelope(GameEventType.KillerNotification, payload)); + d.Dispatch(env!.Value); + + Assert.Equal("Drudge", gotVictimName); + Assert.Equal(0x80001234u, gotVictimGuid); + } + [Fact] public void WireAll_MagicPurgeEnchantments_CallsOnPurgeAll() {