docs: Campaign CC plan — retail character creation, slices CC1-CC7
Recon-grounded (three parallel sweeps 2026-08-15): full retail flow (gmCharGenMainUI mode 0x1000000b, six ECGProgress pages with every widget id), byte-exact 0xF656 layout incl. the trailing checksum ACE never reads, 0xF643 create semantics (local roster append + retail logs straight in; no fresh CharacterList), the chargen DAT table shape, the preview rig (gmCG3DView over CreatureMode - Appearance/Summary pages only), and every acdream seam to reuse (generic layout resolver, fixed canvas, offscreen viewport pipeline, RuntimeCharacterSelectionState as the J-owner template, the 0xF643 correlation landmine). Slice order CC1 data / CC2 wire (parallel) then CC3 Runtime owner, CC4/CC5 form pages, CC6a/b appearance+preview staged, CC7 end-to-end + user gate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
0baebce262
commit
ee80138fba
1 changed files with 210 additions and 0 deletions
210
docs/plans/2026-08-15-character-creation-campaign.md
Normal file
210
docs/plans/2026-08-15-character-creation-campaign.md
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
# 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 | — | | | |
|
||||
| CC2 | — | | | |
|
||||
| CC3 | — | | | |
|
||||
| CC4 | — | | | |
|
||||
| CC5 | — | | | |
|
||||
| CC6a | — | | | |
|
||||
| CC6b | — | | | |
|
||||
| CC7 | — | | | |
|
||||
Loading…
Add table
Add a link
Reference in a new issue