acdream/docs/plans/2026-08-24-character-advancement-campaign.md
Erik 20721ddada docs: open Campaign CA — character advancement retail parity (promotes #431)
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 <noreply@anthropic.com>
2026-08-24 13:13:20 +02:00

6.7 KiB

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