acdream/docs/plans/2026-08-24-character-advancement-campaign.md
Erik 51a7c99b94 docs: CA5 ledger — first drive partial results and the fix round
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 16:52:17 +02:00

8.4 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 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).