feat(net): #7 PlayerDescriptionParser - enchantment block walker + StatMod flow
Extends PlayerDescriptionParser past the spell block to parse the
Enchantment trailer per holtburger events.rs:462-501 +
magic/types.rs:40. New EnchantmentEntry record carries the full
60-64 byte wire payload:
u16 spell_id, layer, spell_category, has_spell_set_id
u32 power_level
f64 start_time, duration
u32 caster_guid
f32 degrade_modifier, degrade_limit
f64 last_time_degraded
u32 stat_mod_type, stat_mod_key
f32 stat_mod_value
[u32 spell_set_id]?
+ EnchantmentBucket (Multiplicative / Additive / Cooldown / Vitae)
EnchantmentMask outer u32 selects which buckets follow; each bucket
(except Vitae) is u32 count + N records. Vitae is a singleton.
Parsed.Enchantments now exposed as IReadOnlyList<EnchantmentEntry>.
GameEventWiring routes each entry through Spellbook.OnEnchantmentAdded
with the full StatMod data + bucket. EnchantmentMath.GetMod consumes
StatMod records to produce real (Multiplier, Additive) per stat key:
Bucket 1 (Multiplicative): multiplier *= val
Bucket 2 (Additive): additive += val
Bucket 8 (Vitae): multiplier *= val (applied last)
Bucket 4 (Cooldown): skipped (not a vital mod)
ActiveEnchantmentRecord extended with optional StatModType /
StatModKey / StatModValue / Bucket fields. Existing 4-arg callers
stay compatible (defaults to null / 0). New OnEnchantmentAdded
overload accepts the full record from PlayerDescription path.
Tests: 7 new (834 -> 841):
- PlayerDescriptionParserTests (2): enchantment block schema with
multiplicative + additive buckets, Vitae singleton.
- EnchantmentMathTests (5): multiplicative buffs aggregate, additive
buffs sum, stat-key mismatch filters out, Vitae applied
multiplicatively, family-stacking picks higher spell-id.
Closes #7 (parser past spells, enchantment block parsed).
Closes #12 (StatMod flow architecture — data lights up #6's
aggregator). Files #13 (remaining trailer sections: options /
shortcuts / hotbars / desired_comps / spellbook_filters / options2 /
gameplay_options / inventory / equipped — needs the heuristic
gameplay_options walker per holtburger).
Note: ParseMagicUpdateEnchantment (live-update 0x02C2) NOT yet
extended — still uses 4-field summary. PlayerDescription is the
load-bearing path for #6; live updates can be folded in separately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b153bbe5ad
commit
bb5003a849
7 changed files with 447 additions and 58 deletions
|
|
@ -114,48 +114,29 @@ Copy this block when adding a new issue:
|
|||
|
||||
---
|
||||
|
||||
## #12 — Capture full Enchantment wire payload (StatMod) on ActiveEnchantmentRecord
|
||||
|
||||
**Status:** OPEN
|
||||
**Severity:** LOW (architecture for buff aggregation is in place via #6 / Phase G; this is the wire-data extension that lights it up)
|
||||
**Filed:** 2026-04-25
|
||||
**Component:** net / spells
|
||||
|
||||
**Description:** `ParseMagicUpdateEnchantment` currently reads only the first 16 bytes of the 0x02C2 payload (`SpellId, LayerId, Duration, CasterGuid`) and ignores the trailing fields including `_smod` (the StatMod triad). `EnchantmentMath.GetMod` consequently returns `(1.0, 0.0)` for every stat key — the architecture is wired (see `Spellbook.GetVitalMod`, `LocalPlayerState.GetMaxApprox`) but no actual buff modifier flows through. Until this lands, the `+Acdream` Stam/Mana percent will continue to read ~95% (vs. retail's 100%) when buffs are active.
|
||||
|
||||
**Root cause / status:** Holtburger `crates/holtburger-protocol/src/messages/magic/types.rs` documents the full 60-64 byte Enchantment wire format: `u16 spell_id, u16 layer, u16 spell_category, u16 has_spell_set_id, u32 power_level, f64 start_time, f64 duration, u32 caster_guid, f32 degrade_modifier, f32 degrade_limit, f64 last_time_degraded, u32 stat_mod_type, u32 stat_mod_key, f32 stat_mod_value, [u32 spell_set_id]?`. Note the wire format starts with `u16` not `u32` for spell_id / layer / category — our existing parser may be reading garbage. Verify against actual ACE traffic via `ACDREAM_DUMP_VITALS=1`.
|
||||
|
||||
**Files:**
|
||||
- `src/AcDream.Core.Net/Messages/GameEvents.cs` — extend `ParseMagicUpdateEnchantment` + `EnchantmentSummary`.
|
||||
- `src/AcDream.Core/Spells/Spellbook.cs` — extend `ActiveEnchantmentRecord` with `StatModType`, `StatModKey`, `StatModValue`; extend `OnEnchantmentAdded` arity.
|
||||
- `src/AcDream.Core/Spells/EnchantmentMath.cs` — uncomment the StatMod-aware aggregator inside `GetMod`.
|
||||
|
||||
**Research:** holtburger `messages/magic/types.rs:40` (`Enchantment::unpack`); `docs/research/named-retail/acclient.h` line 37406+ (`Enchantment` + `StatMod`).
|
||||
|
||||
**Acceptance:** With `+Acdream` logged in and standard self-buffs active, `LocalPlayer.StaminaPercent` rises from ~95% to a percent that matches retail's reading. New tests cover: parse round-trip with synthetic Enchantment payload; StatMod aggregation in `EnchantmentMath` (multiplicative + additive + family-stacking).
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## #7 — PlayerDescription parser stops after spells (options/inventory/equipped not extracted)
|
||||
## #13 — PlayerDescription trailer past enchantments (options / shortcuts / hotbars / desired_comps / spellbook_filters / options2 / gameplay_options / inventory / equipped)
|
||||
|
||||
**Status:** OPEN
|
||||
**Severity:** LOW (Issue #5 needed only the early sections; later panels will need the rest)
|
||||
**Severity:** LOW (no current user-visible bug; future panels will need the data)
|
||||
**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`.
|
||||
**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:** 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).
|
||||
**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 coverage with synthetic payloads of the trailing sections.
|
||||
- `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 `crates/holtburger-protocol/src/messages/player/events.rs:462-625` (full unpacker including the heuristic `find_inventory_start_after_gameplay_options`).
|
||||
**Research:** holtburger `events.rs:462-625`; `references/actestclient/TestClient/messages.xml`.
|
||||
|
||||
**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.
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -184,7 +165,27 @@ Copy this block when adding a new issue:
|
|||
|
||||
# Recently closed
|
||||
|
||||
## #6 — [DONE 2026-04-25 architecture; data follow-up #12] Vital max ignores enchantment buffs + vitae
|
||||
## #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<EnchantmentEntry> 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`
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue