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>
This commit is contained in:
Erik 2026-08-24 13:13:20 +02:00
parent 373d003f1b
commit 20721ddada
2 changed files with 136 additions and 1 deletions

View file

@ -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 (0x00440x0047) 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

View file

@ -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 | — | |