# 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 | COMPLETE 2026-08-24 | docs/research/2026-08-24-advancement-wire-and-recompute.md — six inbound messages pinned byte-for-byte with 3-source agreement; live-at-inquiry recompute verdict verified by hand in Ghidra; RetailSkillFormula already ports 0x00591960 exactly | | CA2 | COMPLETE 2026-08-24 (`65430d4c`) | 0x02E3/0x02DD parsers + WorldSession events + router routing into LocalPlayerState; conformance tests incl. holtburger golden fixture; 0x02DF deliberately unparsed (no ACE producer) | | CA3 | COMPLETE 2026-08-24 | Live formula recompute (SkillFormulaBonusResolver over RetailSkillFormula) on attribute writes + fresh-train derivation; movement re-applied down the PD seam (PushMovementSkillTotals — Quickness raise → run speed, no relog); vitals bar pull-model verified; router behavior + fresh-train tests | | CA4 | COMPLETE 2026-08-24 | Optimistic ApplyLocalRaise layer DELETED; retail one-in-flight + ghost + server-authoritative flow ported per the pinned §5 pseudocode (AP-73 NARROWED — rejection-release semantics owed to CA5 live); train cost verified DAT-exact; specialize correctly has no panel send (gem + confirmation route, seams already present); provider contract tests rewritten | | CA5 | FIRST DRIVE 2026-08-24 (partial) + fix round `bce17b3c` | §1 PASSED from the log (run total 40→433 live, each raise re-applied; zero exceptions). Drive harvested #440 (trained row stuck until next click — row refresh now driven by the authoritative record) and closed #430 (tooltips could never mount — rows lacked the popup locator). OWED at the next drive: §2 Endurance→stamina fan-out, §3.2 AP-73 ghost probe, §5/§6 re-check (train row moves immediately now), tooltip hover-dwell visual, and the three feel answers (run speed, advancement chat lines, ghost cycle). |