# 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`) — 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. 8. **CC7 landmine (found in the CC1 review fix round, 2026-08-15):** ACE's `PlayerFactory.CreatePlayer` heritage-override branch (references/ACE/Source/ACE.Server/Factories/PlayerFactory.cs:184-211) over-deducts skill credits when specializing a skill the active heritage's own list prices. For a skill priced ONLY by the global SkillTable, ACE correctly computes the incremental specialize cost via `SkillBase.UpgradeCostFromTrainedToSpecialized` (= `SpecializedCost - TrainedCost`) and charges `TrainSkill(trainedCost) + SpecializeSkill(incrementalCost)` = the field's TOTAL, matching retail. But when the heritage's own list has an entry, ACE sets `specializedCost = skillGroup.PrimaryCost` directly — `PrimaryCost` is already the TOTAL cost to reach Specialized (acdream's own `ChargenSkillCost.PrimaryCost` convention, confirmed against retail) — and then still charges `TrainSkill(NormalCost) + SpecializeSkill(PrimaryCost)`, over-deducting by an extra `NormalCost` credits versus what retail's client computed and what the player agreed to spend. Practical impact for CC7's connected gate: a retail-legal character build that specializes a skill the ACTIVE HERITAGE prices (every one of the 13 installed heritages has exactly one such skill — see `ChargenTableReaderInstalledDatTests.InstalledHeritages_SkillCostFallbackCoversTheKnownUncostableSkillSet`) may be REJECTED by local ACE with `FailedToSpecializeSkill` even though acdream sent the byte-correct 0xF656 body. If CC7's gate hits this, it is an ACE-side bug reproduced from its own source, NOT an acdream wire or math defect — do not "fix" acdream's cost math to match ACE's over-deduction. Register: file an AD row if CC7 needs a documented workaround (e.g. picking a Specialized skill combination that avoids the heritage-priced skill for the connected gate) rather than silently adjusting acdream's send. ## 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 fix round complete | `04450041`, `459a87f2` | 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. **Review fix round (`459a87f2`):** F1 doc corrected — Custom IS template index 0 (the Adventurer row), per `gmCGProfessionPage::UpdateProfession @ 0x004821b0` (case 0 → button 0x100003d9 / `ID_CharGen_CustomText`) and `CharGenState::SetTemplate @ 0x005C5A60` (commits via `CharGenState::ApplyTemplate @ 0x005C5080`, i.e. selecting Custom resets sliders to the floor spread, it does not bypass templates); F2 two-tier skill-cost fallback implemented (`ChargenOptions.GlobalSkillCostsBySkillId` from portal.dat 0x0E000004, `ChargenSkillCreditMath` checks heritage list then global list) + installed-DAT completeness assertion recording reality: the global SkillTable prices 38/54 advancement skill ids, every one of the 13 heritages ships EXACTLY one heritage-specific override (always also present in the global table), and 16 skill ids are genuinely uncostable in both tiers (retail's -1 case) — see `ChargenTableReaderInstalledDatTests.InstalledHeritages_SkillCostFallbackCoversTheKnownUncostableSkillSet`; F3 every `ChargenTableReader` collection is now frozen at projection (`ToFrozenDictionary`/`ToArray`, matching `MagicCatalog`'s pattern) including both `ChargenOptions.Empty` dictionaries; F4 a reflection guard test (`ChargenNoChoriziteLeakTests`) pins the no-Chorizite-leak contract by walking every public `AcDream.Core.CharGen` member; F5 `HasAnyAppearanceOptions`'s doc reworded to state precisely what it proves (an OR across eight lists, omitting the three color lists) + a new installed-DAT gate records per-list reality — found COMPLETE, every gender of every heritage has non-empty lists across all eight plus the three color lists, even the sparse Gear Knight/Olthoi variants; F6 `TryGetHeritage`/`TryGetStarterArea` annotated `[MaybeNullWhen(false)]` (matching the house `EmptyDatReaderWriter` pattern), all affected call sites (more than the originally estimated five) fixed across both test projects. Filed CC7 risk item 8: ACE's `PlayerFactory` heritage-override branch over-deducts skill credits when specializing a heritage-priced skill (references/ACE/Source/ACE.Server/Factories/PlayerFactory.cs:184-211) — a retail-legal build may be rejected by local ACE at the CC7 connected gate; this is an ACE bug, not an acdream defect. | | 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 | — | | | |