acdream/docs/plans/2026-08-15-character-creation-campaign.md
Erik 70d52b0da2 docs: Campaign CC ledger — CC1 and CC2 implemented, reviews in flight
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 12:54:39 +02:00

210 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Campaign CC — retail character creation
**Status:** ACTIVE (started 2026-08-15)
**Goal (user-set):** the full retail creation flow against local ACE — Create
button through a new character entering the world, 3D preview live, rejections
showing retail's dialogs — then stop for the user gate.
**Branch:** `claude/acdream-launcher-credentials-4d2f7c`
**Process:** Campaign LA's, binding (Sonnet implements, Opus dual-lens reviews
per slice, retail decomp is the oracle, register rows with deviations,
build+test green per slice, commits tagged `Campaign CC`).
This plan embeds the 2026-08-15 recon facts (three parallel sweeps: retail
gmCG UI, chargen data+wire, acdream seams) so slices and future sessions need
no transcript access. `references/ACE` and `references/holtburger` are NOT in
this worktree (gitignored) — read them from the main checkout at
`C:\Users\erikn\source\repos\acdream\references\`.
## Retail ground truth (recon summary — cite these in code)
**Flow.** Create button (`0x100003A0`) → `QueueUIMode(0x1000000b)`
`gmCharGenMainUI` (acclient.h:56232): ONE root layout, enum `0x10000039` via
GetDIDByEnum table 5 (our generic `RetailDataIdResolver` handles this), pages
as children. `ECGProgress`: Heritage=1 → Profession=2 → Skills=3 →
Appearance=4 → Town=5 → Summary=6. Nav dispatch
`gmCharGenMainUI::ListenToElementMessage@237025`: Back `0x100003c6` (at
Heritage → DoExit), Next `0x100003c7`, Finish `0x100003c8` (Summary only),
Help `0x100003c9`, Exit `0x100003ca` (→ `ID_CharGen_ExitWarning` confirm),
Random `0x100003cb` (on Summary → randomize warning first). Tab buttons
`0x100003ef..f4` jump pages freely (not validation-gated). Page roots:
Heritage `0x100003d1`, Profession `0x100003d2`, Skills `0x100003d3`,
Appearance `0x100003d4`, Town `0x100003d5`, Summary `0x100003d6`; progress
bar `0x100003ce`, master page `0x100003d0`. Per-page child ids are in the
recon-cited ctors: Heritage `InitializePage@143731` (13 race buttons + text
`0x100003c4`), Profession `@143010` (6 attribute sliders `0x100003e6..eb`,
avail/health/stam/mana `0x100003e2..e5`, template buttons resolved in
`UpdateProfession@142180`: Custom `0x100003d9`, Bowhunter/Swashbuckler/
Lifecaster/Warmage/Wayfarer/Soldier `0x100003da..df`), Skills `@141911`
(listbox `0x100003f7`, credits `0x100002f3`, info `0x100003fb/fc`),
Appearance `@140032` (gender `0x100003a7/a8`, spins hair/eyes/nose/mouth/skin
`0x100003af..b3`, headgear/shirt/trousers/footwear `0x100003b5..b8`, zoom
`0x10000325/26`, rotate `0x10000323/24`, color wheel family
`0x1000030e..0x10000321`, viewport `0x100003bb`), Town `@137120` (Sanamar
`0x1000040b`, Holtburg `0x1000040d`, Yaraq `0x1000040e`, Shoushi
`0x1000040f`), Summary `@136566` (list `0x10000400`, name text `0x10000402`
with NameInputFilter, viewport `0x10000406`).
**CharGenState** (acclient.h:40074): the model our Runtime owner mirrors —
heritage/gender, appearance strips+styles+colors+shades (f64 shades),
template + 6 attributes + credit budgets + per-attribute locks, 55-slot
skill advancement array + skill credits, name[33], startArea, setupID,
verificationState. Writers per page in the recon (SetHeritageGroup recomputes
budgets + ApplyTemplate + RandomizeStartArea; SetGender reapplies clothing
and UpdateTrueFacePal).
**Finish** (`DoFinish@236864`): trim+set name → empty name →
`ID_CharGen_NoNameWarning`, abort; `remainingAtrbCredits > 0` → credit
warning, abort (retail FORCES full spend — ACE does not; we port the client
gate); verification state must be UNDEF (no double submit) → set PENDING →
`Proto_UI::SendCharGenResult@0x00546A70`.
**Wire 0xF656** (`ACCharGenResult::CG_Pack@0x005C7200`, byte-identical to
ACE's `CharacterCreateInfo.Unpack`): account String16L FIRST (outside the
body), then u32 constant 1, u32 heritage, u32 gender, u32×3 eyes/nose/mouth
strips, u32×2 hairColor/eyeColor, u32 hairStyle, u32×2 headgearStyle/Color,
u32×2 shirt, u32×2 trousers, u32×2 footwear, f64×6 skin/hair/headgear/shirt/
trousers/footwear shades, u32 templateNum, u32×6 attributes
(str/end/coord/quick/focus/self), u32 slot, u32 classID, u32 numSkills +
numSkills×u32 advancement classes (MUST be exactly 55 — ACE TERMINATES the
session on mismatch), String16L name, u32 startArea, u32 isAdmin, u32
isEnvoy(=ACE IsSentinel), u32 trailing checksum = sum of
heritage+gender+strips(3)+hairColor+eyeColor+hairStyle+headgearStyle+
shirtStyle+trousersStyle+footwearStyle+template+6 attributes (ACE never
reads it; we send it for byte fidelity). holtburger cross-check:
`character/types.rs:236` (stops before the checksum).
**Response 0xF643** (shared opcode with restore — LA7a's conditional parse is
reusable): codes Undef=0 Ok=1 Pending=2 NameInUse=3 NameBanned=4 Corrupt=5
DatabaseDown=6 AdminPrivilegeDenied=7. On Ok the payload is a
CharacterIdentity (guid, String16L name, u32 secondsGreyedOut) and NOBODY
sends a fresh CharacterList — retail appends the identity to its local
roster (`Handle_CharGenVerificationResponse@0x0055E8B0` case 1 →
`CharacterSet::AddIdentity`) and `gmCharGenMainUI::Update@236161` then
watches the set and calls `CPlayerSystem::LogOnCharacter` DIRECTLY when the
new name appears (logs straight in; only falls back to char management if it
never appears). Error dialogs: NameInUse→`ID_Character_Err_NameReserved`,
NameBanned→`ID_Character_Err_NameBanned`, Corrupt/DatabaseDown→
`ID_Character_Err_NameDBDown`, AdminPrivilegeDenied→
`ID_Character_Err_NameAdminDenied`, Pending/Undef→silent state reset (ACE
sends Pending for a disabled-Olthoi rejection — retail swallows it; port
as-is, register-note the quirk).
**Chargen DAT table** `0x0E000002`: readable TODAY via the
Chorizite.DatReaderWriter package (`dats.Get<CharGen>`) — zero in-tree
readers exist. ACE loaders (`ACE.DatLoader.FileTypes.CharGen` +
`HeritageGroupCG/SexCG/TemplateCG`) and retail serializers
(`ACCharGenData::Serialize@0x005C36D0`, `HeritageGroup_CG@0x005C2100`,
`Sex_CG@0x005C1600`, `Template_CG@0x005C0450`) define the shape: per
heritage → name/icon/setup/EnvironmentSetup/attribute+skill credits/start
areas/skills(costs)/templates(attrs+skills)/genders; per sex → scale, setup,
base palette, skin palset, base ObjDesc, and the option LISTS (hair styles/
colors, eye colors, eye/nose/mouth strips, headgear/shirt/pants/footwear,
clothing colors).
**3D preview** (`gmCG3DView`, Appearance `0x100003bb` + Summary `0x10000406`
ONLY — the other four pages have no viewport): preview body
`CPhysicsObj::makeObject(setupId)` (fallback HUMAN_SETUP_ID), rebuild on
change via ObjDesc (`ClothingTable::BuildObjDesc` per clothing slot + strips
+ PalSet skin/hair/eye subpalettes) applied with
`DoObjDescChangesFromDefault@242308`, one DISTANT_LIGHT (intensity 2.0),
idle animation loop at 30fps (`set_sequence_animation`), rest-pose freeze on
zoom-in, BUTTON-toggled continuous rotation (`DoRotation@137337`, 3.0
s/revolution, per-frame global-message-3 tick), zoom tween between
per-heritage camera positions (`Update@138974` hard-codes Olthoi vs
human-form camera offsets).
## acdream seams (build on these, do not reinvent)
- Layout mount: `RetailDataIdResolver.Resolve(dats, 0x10000039, 5)` +
`LayoutImporter` — fully generic. `DatWidgetFactory` already maps dat type
0xD → `UiViewport`. The char-management controller REFUSES viewports by
local policy (:212) — chargen gets its OWN controller; clone
`CharacterManagementUiMountCoordinator` + the bindings-record pattern.
- Fixed canvas: chargen is the same 800×600 flow screen — mount at authored
extent, `UiRoot.FixedCanvasSize` on activate (AD-98), dialogs center on
`EffectiveCanvasSize`. Live-DAT probe tests sweep ALL media ids
(`CharacterManagementLiveDatTests` pattern) and pin authored
justify/anchors.
- Preview pipeline: `PrivateEntityViewportRenderer` (offscreen target →
texture table → `UiViewport` sprite) is proven by paperdoll + appraisal;
cameras there are FIXED — chargen needs a heading-capable camera. NOTE:
`GlGpuDevice.RegisterExternalColorTexture` is a DELETED API that survives
only in stale doc comments — do not cite it. Appearance building:
`DollEntityBuilder.Build` is index-agnostic and pure (setup + resolved
palette/part ids), but the only existing factory reads a LIVE entity —
chargen needs a new index→dat→ObjDesc factory (SexCG.BaseObjDesc + strip
overlays + PalSet.GetPaletteID hues). Pose: paperdoll holds a static final
frame; retail chargen plays a live idle loop — see slice CC6 for the
staged approach.
- Runtime owner: mirror `RuntimeCharacterSelectionState` exactly (lifecycle/
snapshot/delta records, borrow-only view, generation-gated commands, one
mutable owner, no App types). Command family lands beside
`IGameRuntimeCommands.CharacterSelection`. Enter-after-create hooks the
existing `LiveSessionController.BeginEnter/CompleteEnter`.
- Wire plumbing: `WorldSession`'s dispatch chain routes EVERY 0xF643 through
`CharacterRestore.Parse` today with no request correlation — the KNOWN
LANDMINE. Creation requires an awaiting-request latch (create vs restore)
BEFORE its response arm lands. Outbound mirrors
`SendRestoreCharacter@2223`. Status writer: add `characterCreated` /
`creationFailed` events (update the pinned §LA1 contract text + the
Launcher.Core tailer + tests in lockstep).
## Slices
| Slice | Deliverable | Depends |
|---|---|---|
| CC1 | Chargen data layer: `CharGen` table reader → typed options model (heritages/sexes/appearance lists/templates/skills+costs/budgets/towns), Content/Core, live-DAT probes | — |
| CC2 | Wire: `CharacterCreate` 0xF656 builder (byte-exact incl. checksum), shared verification-response type (refactor from `CharacterRestore`), WorldSession request-correlation for 0xF643, send seam, status events + contract/tailer update | — |
| CC3 | `RuntimeCharacterCreationState`: full CharGenState mirror, per-page commands, retail client gates (full-spend, name, 55-slot invariant, client-side slot cap), verification latch, Ok → roster append + retail log-straight-in | CC1, CC2 |
| CC4 | Screen shell + form pages (App): mount (enum 0x10000039), master nav/tabs/progress, dialogs, Heritage + Profession + Skills + Town pages | CC1, CC3 |
| CC5 | Summary page: name input (NameInputFilter, `ID_CharGen_NameTooLong`), summary listbox, static summary viewport, Finish gates + full response/dialog handling | CC3, CC4 |
| CC6 | Appearance page + preview: index→ObjDesc factory, chargen preview renderer (offscreen, heading camera, rotate/zoom buttons), spin controls + color wheels; **staged:** CC6a static-pose preview (paperdoll-style held frame, register row for the missing idle loop), CC6b idle animation + zoom rest-freeze (retire the row) | CC1, CC4 |
| CC7 | End-to-end: Create button un-ghosts, full flow vs ACE shapes in tests, launcher payload cycle, connected checklist doc | all |
Parallelism: CC1 ∥ CC2 (disjoint: Content/Core vs Core.Net; separate
worktrees). CC4 ∥ CC6a after CC3. CC5 last before CC7.
## Risks / open items (from recon Unknowns)
1. 0xF643 create/restore correlation (CC2's first job; the restore doc
comment already warns).
2. 55-slot skill array: ACE terminates the session on mismatch — CC2/CC3
must make it structurally impossible to send anything else.
3. Slot cap is client-enforced only (ACE never checks on create) — honor
`slotCount` like retail's UI did.
4. Color-wheel/gradient widgets (`tagColorWheel`, GradCircle `0x1000030e`,
shade scroll) may need new widget types in `DatWidgetFactory` — CC6
scouts the authored layout first.
5. Retail unknowns to resolve during slices, never guess: the chargen
please-wait dialog context (decompiler-mislabeled field), the
AppearancePage gender-flip-on-init oddity (@140355 — verify live before
porting), `Method_CG` enums are empty in the header, ZoomIn tween
duration constant is decompiler-garbled (measure against retail if it
matters).
6. Viewport inside the fixed canvas: the offscreen target's pixel size vs
the canvas-scaled on-screen rect (render at scaled size for crispness or
authored size for fidelity) — decide in CC6a with the user gate as
arbiter.
7. `references/*` absent in worktrees (except WorldBuilder, uninitialized
submodule) — agents read ACE/holtburger from the MAIN checkout path.
## Review protocol
Per slice: implement → Opus dual-lens (architectural + retail fidelity — this
campaign is retail-heavy everywhere) → fixes → narrow re-review → DONE in
ledger. CC2's review adds wire-byte scrutiny (the LA7a precedent: the
reviewer decodes the binary); CC6's adds the visual-fidelity lens ahead of
the user gate.
## Ledger
| Slice | Status | Commits | Review | Notes |
|---|---|---|---|---|
| CC1 | implemented; review in flight | `04450041` | dispatched 2026-08-15 | Core model (no Chorizite leak) + Content projector; 31 math units + 6 installed-DAT gates (13 heritages). FINDING for CC3: each human heritage's "Adventurer" template IS retail's Custom entry point — attributes at the 10-floor (60/330), a real TemplateCG row, not a UI special case |
| CC2 | implemented (campaign-cc2); review in flight | `5eaad2c8` | dispatched 2026-08-15 | Byte-exact 0xF656 (19-term checksum vs CG_Pack accumulator), shared 0xF643 type, correlation latch, status events + contract amendment. Core.Net 993 / Runtime 1667 / Launcher.Core 323, Windows+WSL |
| CC3 | — | | | |
| CC4 | — | | | |
| CC5 | — | | | |
| CC6a | — | | | |
| CC6b | — | | | |
| CC7 | — | | | |