docs(issues): #8/#9/#11 filed; #10 wired (KillerNotification)

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>
This commit is contained in:
Erik 2026-04-25 17:39:47 +02:00
parent 0a429a980c
commit 567078803f
4 changed files with 113 additions and 0 deletions

View file

@ -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<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 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@@<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

View file

@ -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 =>

View file

@ -57,6 +57,14 @@ public sealed class CombatState
/// <summary>An attack commit completed (0x01A7). WeenieError = 0 on success.</summary>
public event Action<uint /*attackSeq*/, uint /*weenieError*/>? AttackDone;
/// <summary>
/// Fires when the server confirms the player landed a killing blow
/// (GameEvent <c>KillerNotification (0x01AD)</c>). Event payload is
/// the victim's display name + their server GUID. Used by killfeed UI
/// (future panel) and any plugin scoring kill counts.
/// </summary>
public event Action<string /*victimName*/, uint /*victimGuid*/>? 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);
/// <summary>
/// Server confirmation that the player landed a killing blow on a
/// target. Wire source: GameEvent <c>KillerNotification (0x01AD)</c>
/// — the parser at <c>GameEvents.ParseKillerNotification</c> shipped
/// alongside victim/defender notifications but was never registered
/// for dispatch until 2026-04-25 (per ISSUES.md #10).
/// </summary>
public void OnKillerNotification(string victimName, uint victimGuid)
=> KillLanded?.Invoke(victimName, victimGuid);
public void OnEvasionDefenderNotification(string attackerName)
=> EvadedIncoming?.Invoke(attackerName);

View file

@ -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()
{