Two parallel research passes assembled with a hand-verification ledger on every load-bearing claim. Wire (ACE + Chorizite + holtburger, field-for-field agreement on all six inbound layouts): PrivateUpdateAttribute 0x02E3, PrivateUpdateVital 0x02E7 (always Max-family vital ids 1/3/5), PrivateUpdateAttribute2ndLevel 0x02E9 (always current-family ids 2/4/6 — a parser must NOT treat the two as one id space), PrivateUpdateSkill 0x02DD (ushort ranks + the hardcoded adjustPP=1 pair, f64 lastUsedTime), PrivateUpdatePropertyInt 0x02CD (AvailableSkillCredits=24) and Int64 0x02CF (AvailableExperience=2). Ordered action->response chains for all four raise/train actions, including the retail quirk that an Endurance raise pushes only a HEALTH full-vital record and the client is expected to refresh stamina from it too. Specialize/untrain/reset have NO dedicated opcode — item-Use plus a confirmation round-trip reusing the same update messages. 0x02DF has no ACE producer (verified); CA2 skips it. Recompute (named-retail + live Ghidra): retail computes skills, vitals maxima and run rate LIVE at inquiry time — Set* are raw-storage writes, InqSkill re-derives from the attribute formula every call (verified in the decompile, including the z==0 early-out that IS the attribute-less Salvaging handling and the +10 augmentation adds), InqRunRate runs every motion tick, and UI refresh is a value-less observer notification. Two corrections to our own tree surfaced: PropertyString.cs's comment claims 0x02DD (it is 0x02D5 — doc-only, nothing dispatches on it), and SkillSnapshot.FormulaBonus is frozen at PlayerDescription parse — the stale-cache half of #431 that CA3 replaces with the live computation. RetailSkillFormula.TryCalculate already ports 0x00591960 exactly, so CA3 reuses it rather than porting anew. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
126 lines
6.9 KiB
Markdown
126 lines
6.9 KiB
Markdown
# 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 | — | |
|
|
| CA3 | — | |
|
|
| CA4 | — | |
|
|
| CA5 | — | |
|