From 7da2a027d43aa48eef0e835eb1738ab79a1cb7c9 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 25 Apr 2026 16:42:24 +0200 Subject: [PATCH] =?UTF-8?q?feat(player):=20#5=20PlayerDescription=20parser?= =?UTF-8?q?=20=E2=80=94=20Stam/Mana=20via=20attribute=20block?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Visual-verified — Vitals window now shows three bars (HP/Stam/Mana) with live values. Closes ISSUES.md #5; ~95% reading on Stam/Mana traced to active buff multipliers, filed as #6. Why the rewrite The first attempt (commit d42bf57) routed PlayerDescription (0x0013) through AppraiseInfoParser, trusting a misleading xmldoc claim. Live diagnostics proved the format is wrong — ACE source (GameEventPlayerDescription.WriteEventBody) hand-writes a body distinct from IdentifyObjectResponse's AppraiseInfo: property hashtables gated on DescriptionPropertyFlag, vector-flag-gated attribute / skill / spell blocks, then a long options + inventory trailer. Vitals only arrive via the attribute block at login. Holtburger's events.rs:220-625 has the canonical client-side unpacker; this commit ports the early-section walker through spells. What landed PlayerDescriptionParser.cs (new — 350 LOC): Walks propertyFlags + weenieType, then property hashtables (Int32/Int64/Bool/Double/String/Did/Iid) + Position table — each gated on a property flag bit, header is `u16 count, u16 buckets`. Then vectorFlags + has_health + the attribute block (primary attrs 1..6 = 12 B each, vitals 7..9 = 16 B with `current`), then optional Skill + Spell tables. Stops cleanly before the options/shortcuts/hotbars/inventory trailer (filed as #7 — heuristic alignment search needed for gameplay_options). PrivateUpdateVital.cs (new — 95 LOC): Wire parsers for the GameMessage opcodes 0x02E7 (full snapshot) and 0x02E9 (current-only delta), per holtburger UpdateVital + UpdateVitalCurrent. WorldSession dispatches each to a session- level event the GameWindow forwards into LocalPlayerState. LocalPlayerState (full redesign): VitalKind (Health/Stamina/Mana) + AttributeKind (six primary). VitalSnapshot stores ranks/start/xp/current; AttributeSnapshot stores ranks/start/xp with `Current = ranks+start` per holtburger. GetMaxApprox computes the retail formula vital.(ranks+start) + attribute_contribution where the contribution is hardcoded from retail's SecondaryAttributeTable: Endurance/2 for Health, Endurance for Stamina, Self for Mana. Enchantment buffs not yet folded in (filed as #6). VitalIdToKind now accepts both ID systems (1..6 wire, 7..9 PD attribute block); AttributeIdToKind covers primary attrs 1..6. GameEventWiring: PlayerDescription handler. Walks parsed.Attributes, routes primary attrs (id 1..6) to OnAttributeUpdate and vitals (id 7..9) to OnVitalUpdate. Player's full learned spellbook also lands here. ACDREAM_DUMP_VITALS=1 traces every PD attribute + every PrivateUpdateVital(Current) opcode for diagnostics. WorldSession: Dispatch chain re-ordered — the diagnostic else-if for ACDREAM_DUMP_OPCODES=1 was originally placed before GameEventEnvelope.Opcode, which silently intercepted 0xF7B0 and broke UpdateHealth dispatch when the env var was set. Moved to the very end of the chain so it only fires for genuinely unhandled opcodes. (Diagnostic-only regression; production launches without the env var were unaffected.) Test deltas Added: - PlayerDescriptionParserTests (6 — empty header, full attribute block, partial flags, post-property-table walk, spell table) - PrivateUpdateVitalTests (7 — fixture round-trip, vital ID coverage, opcode rejection, truncation) - LocalPlayerStateTests rewritten (20 — VitalIdToKind + AttributeIdToKind theories, Endurance/Self formula coverage, delta semantics, change events) - GameEventWiringTests for PlayerDescription dispatch (2 — end-to-end populate + spellbook feed) Updated: - VitalsVMTests rephrased onto the new OnVitalUpdate API. Total: 765 → 817 tests passing. Diagnostics ACDREAM_DUMP_VITALS=1 — log every PD attribute extracted, every 0x02E7/0x02E9 dispatch. ACDREAM_DUMP_OPCODES=1 — log first occurrence of any unhandled GameMessage opcode (now correctly placed at end of chain). Visual verify $env:ACDREAM_DEVTOOLS = "1" dotnet run --project src\AcDream.App\AcDream.App.csproj -c Debug Vitals window shows three bars; HP at 100%, Stam/Mana at ~95% (the gap is buff enchantments — filed as #6 with the holtburger multiplier+additive aggregator pattern as the reference for the fix). Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 4 + docs/ISSUES.md | 46 +- src/AcDream.App/Rendering/GameWindow.cs | 7 + src/AcDream.Core.Net/GameEventWiring.cs | 78 ++- .../Messages/AppraiseInfoParser.cs | 16 +- .../Messages/PlayerDescriptionParser.cs | 464 ++++++++++++++++++ .../Messages/PrivateUpdateVital.cs | 105 ++++ src/AcDream.Core.Net/WorldSession.cs | 56 +++ src/AcDream.Core/Player/LocalPlayerState.cs | 306 +++++++++--- .../GameEventWiringTests.cs | 199 ++++---- .../PlayerDescriptionParserTests.cs | 235 +++++++++ .../PrivateUpdateVitalTests.cs | 121 +++++ .../Player/LocalPlayerStateTests.cs | 271 ++++++---- .../VitalsVMTests.cs | 24 +- 14 files changed, 1660 insertions(+), 272 deletions(-) create mode 100644 src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs create mode 100644 src/AcDream.Core.Net/Messages/PrivateUpdateVital.cs create mode 100644 tests/AcDream.Core.Net.Tests/PlayerDescriptionParserTests.cs create mode 100644 tests/AcDream.Core.Net.Tests/PrivateUpdateVitalTests.cs diff --git a/.gitignore b/.gitignore index 904fdf9..d3cbc8e 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,10 @@ references/ .claude/ launch.log launch-*.log +launch.utf8.log # ImGui auto-saved window/docking state (per-user, not source) imgui.ini + +# User-only download cache (per-developer, not source) +refs/ diff --git a/docs/ISSUES.md b/docs/ISSUES.md index c214820..668603a 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -114,6 +114,48 @@ Copy this block when adding a new issue: --- +## #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` — `GetMaxApprox` returns the base; extend to also call `Spellbook` for vital-typed enchantment aggregation. +- `src/AcDream.Core/Spells/Spellbook.cs` — needs an aggregator helper similar to holtburger's `get_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` — extend `Parsed` record + 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 @@ -142,8 +184,8 @@ Copy this block when adding a new issue: ## #5 — [DONE 2026-04-25] VitalsPanel stamina/mana bars always null **Closed:** 2026-04-25 -**Commit:** `feat(player): #5 LocalPlayerState — Stam/Mana wired through PlayerDescription` -**Resolution:** Added `AcDream.Core.Player.LocalPlayerState` (caches `CurrentStamina` / `MaxStamina` / `CurrentMana` / `MaxMana` from `PlayerDescription (0x0013)`'s `CreatureProfile`). Wired into `GameEventWiring.WireAll` as a new optional 6th parameter so back-compat preserved. `VitalsVM` constructor now takes an optional `LocalPlayerState`; when wired, `StaminaPercent` / `ManaPercent` read through to the cache (no VM-side caching) and `VitalsPanel` automatically draws the two extra bars. Edge cases covered: zero `Max*` returns `null` (no /0); current > max clamps to 1.0; partial profiles preserve previous good values rather than wiping them. 16 new tests (11 `LocalPlayerStateTests` + 3 `VitalsVMTests` + 2 `GameEventWiringTests`). +**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.