From 20721ddadac7afc8653d865c25f20cd8fcd53840 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 24 Aug 2026 13:13:20 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20open=20Campaign=20CA=20=E2=80=94=20char?= =?UTF-8?q?acter=20advancement=20retail=20parity=20(promotes=20#431)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The owner widened #431 into the full advancement family: real-time refresh of vitals maxima (attribute AND direct vital raises), derived skills, run speed under Quickness, attribute-less skills like Salvaging, and the untested train/specialize/respec flows. The promotion survey pinned the root cause: every outbound raise action (0x0044-0x0047) is wired — which is why the GUI 'works' — while the inbound private attribute/skill update family is parsed nowhere (only the vitals pair 0x02E7/0x02E9 is), so the server's post-raise truth never reaches LocalPlayerState and no recompute ever triggers. Plan doc carries the oracle targets (message family from ACE/Chorizite/holtburger, retail's recompute chain in named-retail, specialization/respec semantics) and five slices ending in a user-driven connected gate. #430 tooltips are explicitly sequenced after, on the #409 tooltip system. Co-Authored-By: Claude Fable 5 --- docs/ISSUES.md | 11 +- ...26-08-24-character-advancement-campaign.md | 126 ++++++++++++++++++ 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 docs/plans/2026-08-24-character-advancement-campaign.md diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 7422b455..6e7f66f7 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -417,7 +417,16 @@ the LayoutDesc importer does not mount yet. ## #431 — Raising an attribute does not refresh attribute-derived skills or run speed -**Status:** OPEN +**Status:** PROMOTED to Campaign CA +(`docs/plans/2026-08-24-character-advancement-campaign.md`, 2026-08-24) +after the owner widened the scope to the whole advancement family: vitals +maxima updating on attribute AND direct vital raises, real-time skill +refresh on both attribute and skill raises, run speed responding to +Quickness, attribute-less skills (Salvaging), plus the untested +train/specialize/respec flows. Root cause confirmed at promotion: the +outbound raise actions (0x0044–0x0047) are fully wired — the INBOUND +private attribute/skill update family is parsed nowhere (only the vitals +pair 0x02E7/0x02E9 is), so the recompute trigger never arrives. **Severity:** MEDIUM (visible stat incoherence during play) **Filed:** 2026-08-23 (owner report) **Component:** character state / stat chain / movement speed diff --git a/docs/plans/2026-08-24-character-advancement-campaign.md b/docs/plans/2026-08-24-character-advancement-campaign.md new file mode 100644 index 00000000..69b6fd6e --- /dev/null +++ b/docs/plans/2026-08-24-character-advancement-campaign.md @@ -0,0 +1,126 @@ +# Campaign CA — character advancement retail parity (#431 + the raise/train/specialize family) + +**Status:** ACTIVE 2026-08-24. Owner-directed scope; #431 promoted here. +**Milestone:** M4 — Live in the world. +**Issue anchors:** #431 (promoted), #430 tooltips (SEQUENCED AFTER, not in +this campaign — needs the #409 client-wide tooltip system). + +## Owner's report (2026-08-24, verbatim scope) + +The raise flow "works from the GUI and everything" — the guess is the +refresh side "is not wired correctly." Required behavior: + +1. Raising an attribute must update everything derived from it, in real + time: raising Endurance/Self/etc. must move the **vitals bar** maxima + (health/stamina/mana); raising a **vital directly** (secondary + attributes are XP-raisable too) must update the bar the same way. +2. **Skills must update in real time** both when their underlying + attributes raise and when the skill itself is raised with XP. +3. **Run speed must increase when Quickness is raised** — Run is + attribute-fed. +4. **Skills with no underlying attribute (e.g. Salvaging) need their own + handling** — no formula contribution, raise-only progression. +5. **Unknown/untested territory:** raising skills with XP, and + **specializing** skills; also the retail **respec flow** (quests / + item turn-ins that drop learned skills so a character can re-spec / + re-specialize). None of this has been exercised against ACE. + +## Current-state survey (2026-08-24, verified in source) + +- **Outbound: complete.** `CharacterActions` builds all four actions — + `RaiseVital 0x0044`, `RaiseAttribute 0x0045`, `RaiseSkill 0x0046`, + `TrainSkill 0x0047` — and `CharacterSheetProvider` wires the panel's + buttons to them through the Runtime command seam. This is why the GUI + "works": ACE accepts and applies the raises. +- **Inbound: the hole.** The ONLY private stat-update messages parsed + anywhere are the vitals pair `PrivateUpdateVital (0x02E7)` / + `PrivateUpdateVitalCurrent (0x02E9)`. The attribute and skill update + family ACE sends back after a raise is UNHANDLED — no parser, no + routing, nothing reaches `LocalPlayerState` (whose own doc says + attributes refresh "only at PlayerDescription / future + `PrivateUpdateAttribute`"). Post-raise, the client's attribute/skill + model is stale until the next full PlayerDescription (i.e. next login). +- **Consequences observed by the owner (#431):** derived skills don't + move when an attribute raises; run speed doesn't change with + Quickness. Both follow directly from the missing inbound family — the + recompute never triggers because the trigger never arrives. +- **Run-rate seam already exists:** + `PlayerMovementController.ApplyServerRunRate` (the #431 filing's own + pointer) — the wire echo path updates live run rate; what's missing is + driving it (and the formula-side skill totals) from stat updates. +- **Character state owner:** `RuntimeCharacterState` (J4.3) owns the + spellbook/local-player graph; new stat state routes through it, not + through a parallel store. + +## Oracle targets (CA1 — DO FIRST, no guessing) + +Per the mandatory workflow (grep named-retail → cross-reference ≥2 refs → +pseudocode → port → conformance): + +1. **The inbound message family.** Pin exact opcodes + layouts from ACE + (`GameMessagePrivateUpdateAttribute`, `...Attribute2ndLevel` (vitals), + `...Skill`, `...SkillLevel`, `...SkillAC` as ACE names them; their + sequence-number semantics) cross-checked against Chorizite.ACProtocol + and holtburger. Our `PrivateUpdateVital.cs` already cites ACE's + `GameMessagePrivateUpdateAttribute2ndLevel` naming — extend the same + treatment to the whole family. Also pin what ACE sends for + TrainSkill/specialize responses and for skill-credit changes. +2. **Retail's recompute chain.** In named-retail: how the client applies + an attribute update — which cached values recompute + (`CACQualities::InqSkill` / skill formula with attribute divisors from + SkillTable, max-vital formulas, run-rate refresh via the movement + system). The SkillTable formula fields we already load for chargen + (`ChargenSkillAdvancement`) are the same divisor data — verify the + in-world recompute uses identical math. +3. **Attribute-less skills.** SkillTable rows with no formula + (Salvaging & friends): confirm retail's display/derivation for them + (base = trained ranks + augmentation only). +4. **Training / specialization semantics.** Credits accounting, what the + 0x0047 response looks like, how specialization changes the formula + multiplier (specialized = ranks count differently), and what messages + carry it. +5. **Respec / untrain.** Identify the retail mechanism (quest/item-driven + skill refund) and what the CLIENT sees — expectation: server-driven + property/skill updates using the SAME inbound family, so no bespoke + client flow; verify rather than assume. Confirm ACE's implementation + surface for a test path. + +## Slices + +- **CA1 — oracle + research doc** (`docs/research/2026-08-24-advancement-wire-and-recompute.md`): + everything above, with decomp addresses and ACE file citations. Output: + the pinned message table + retail recompute pseudocode. +- **CA2 — inbound stat-update family.** Parsers for the + attribute/skill/(vital-level) private updates; routed as ordered deltas + into the J4 owners (`RuntimeCharacterState` / `LocalPlayerState`) + through the existing generation-gated seam. Conformance tests from ACE + byte layouts. +- **CA3 — derived recompute + real-time presentation.** One recompute + path (retail's formula) fed by CA2's deltas driving: character panel + skill rows, vitals bar maxima (attribute- and vital-raise both), + run-rate into `ApplyServerRunRate`'s seam, attribute-less skills + handled per oracle. Binding-seam tests (the #436 lesson: assert the + REAL composition binds the refresh, not just that VMs recompute). +- **CA4 — train/specialize/respec verification.** Live-vs-ACE for + TrainSkill + specialization (panel flow + credits), and the respec + path exercised as far as ACE supports; fixes as the oracle demands. +- **CA5 — connected gate script** (`docs/research/2026-08-24-campaign-ca-test-script.md`), + user-driven: raise Quickness → run speed visibly increases immediately; + raise Endurance/Self → vitals maxima move; direct vital raise; skill + raise; train; specialize; respec if ACE path exists. PASS = every + change visible without relog. + +**Sequenced after this campaign:** #430 skill/attribute tooltips (needs +the #409 client-wide tooltip surface; the CA1 oracle should still note +where retail sources its tooltip strings while it is in the +neighborhood). + +## Ledger + +| Slice | Status | Evidence | +|---|---|---| +| CA1 | — | | +| CA2 | — | | +| CA3 | — | | +| CA4 | — | | +| CA5 | — | |