Mounts gmCharGenMainUI (enum 0x10000039, root 0x100003CC) via CharacterCreationUiController/CharacterCreationUiMountCoordinator, cloning CharacterManagementUiController's recipe. Master shell ports SetProgressState @0x004e7a10 (Olthoi tab-hide + redirect) and ListenToElementMessage @0x004e9450 (Back/Next/Finish/Help/Exit/Random nav) verbatim, with free tab navigation over all six pages. Heritage, Profession, Skills, and Town pages bind to CC3's RuntimeCharacterCreationState commands; Appearance and Summary mount as content-inert placeholders for CC6b/CC5. Live-DAT probing (CharacterCreationLiveDatTests) found two widget- mapping surprises the decomp's DynamicCast hints don't predict: the Profession slider's value field imports as an editable UiField (wired for direct numeric entry), and the avail/health/stamina/mana/credits displays author as UIElement_Button hosts whose Type-12 value child is swallowed by UiButton.ConsumesDatChildren — substituted with the button's own Label. No new DatWidgetFactory widget types were needed. Threads the installed DAT's real ChargenOptions into Runtime via the new RuntimeCharacterCreationState.InstallOptions, called from ContentEffectsAudioCompositionPhase.Compose (mirrors InstallSpellMetadata's pattern); headless keeps ChargenOptions.Empty unchanged. Wires CC3's F14 status-hook gap (ApplyCharacterCreated/ ApplyCreationFailed) to SessionStatusWriter for both graphical and headless hosts, and adds the CharacterCreation view/command seam through CurrentGameRuntimeAdapter and DeferredGameRuntimeStateCommands alongside CharacterSelection's existing shape. Register: AD-101/102/103, AP-212/213, TS-82 filed for the auto-gender- select interim default, the omitted ToD-account gate, the button-Label widget substitution, the Random-button approximation, the flat-listbox Skills simplification, and the Appearance/Summary placeholders. Runtime 1713/0 (was 1707), App 5117/13 skips (was 5101/6), Headless 165/0 unaffected, full solution Release build green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
258 lines
38 KiB
Markdown
258 lines
38 KiB
Markdown
# 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(this, arg2)@236864`): trim+set name → empty name →
|
||
`ID_CharGen_NoNameWarning`, abort. **CORRECTED at the CC3 review-fix round
|
||
(F3) — the original line here (`remainingAtrbCredits > 0` → abort, "retail
|
||
FORCES full spend") was WRONG; retail does NOT force a full spend.** The
|
||
real gate is `arg2 != 0 && remainingAtrbCredits > 0`: the ordinary
|
||
Finish-button click passes `arg2 = 1` (@0x004E9579), and on unspent
|
||
credits shows `MakeCreditWarningDialog` and returns WITHOUT sending
|
||
(@0x004E91F2-0x004E9210) — but that dialog's own confirm handler
|
||
re-invokes `DoFinish(this, 0)` (@0x004E98BB), which SKIPS the credit check
|
||
entirely (`arg2 == 0`) and sends with the credits still unspent. ACE
|
||
accepts this — `ValidateAttributeCredits` only rejects a total that
|
||
EXCEEDS the max, never an under-spend. Then: 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.
|
||
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. **MEASURED 2026-08-15 (user-prompted — downgrades this
|
||
landmine to LATENT):** dumping the installed EoR DAT shows every one of
|
||
the 13 heritages' single override is skill 14 (Arcane Lore) at
|
||
NormalCost=0 / PrimaryCost=2, versus global TrainedCost=4 /
|
||
SpecializedCost=6. ACE's over-deduction equals NormalCost — which is
|
||
ZERO for the only heritage-priced skill — so ACE charges 0+2=2 and
|
||
retail's client computes 2: they AGREE, and no character build can
|
||
trigger the rejection with end-of-retail data. The formula bug in ACE's
|
||
heritage-override branch is real but unfireable here; it only matters
|
||
if a custom server ships a DAT whose heritage override has a nonzero
|
||
NormalCost. The earlier "may be REJECTED" inference was made from code
|
||
without measuring the data — the C4 closeout's observe-don't-infer
|
||
lesson, again. 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 | REVIEW-CLOSED 2026-08-15 | `04450041`, `cb4703e8` | CLOSED (fix round + narrow re-review; every citation independently re-derived) | 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 (`cb4703e8`):** 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. **Narrow re-review CLOSED:** the reviewer retro-graded F2 to HIGH (under the base commit 37 of 38 costable skills were charged zero) and confirmed the SkillBase.SpecializedCost->PrimaryCost mapping dodged the UpgradeCostFromTrainedToSpecialized trap. Residuals: R1 retail refunds +1 credit on a both-tier miss (port charges 0; unreachable via retail’s own skills listbox — NOTE FOR CC3 if any path ever exposes the 16 uncostable ids); R2 list downcast-mutability and R3 field-walking in the leak guard CLOSED at the merge-closeout commit (Array.AsReadOnly at every projection seam; GetFields walk added). Decomp fact for CC4: ApplyTemplate force-sets template_=0 for heritage 0xc/0xd — both Olthoi variants are hard-locked to Custom/template 0. |
|
||
| CC2 | REVIEW-CLOSED, MERGED 2026-08-15 (`55fc51ed`) | `5eaad2c8`, `e77ebf10`, `95e95bb6` | PASS then CLOSED (fix round: F1 latch-scope narrowing + overwrite pin test, F2 register AD-100, F3 ACE double-NameInUse note, F4 creationFailed{code,reason,name}, F5 pointer, retail-discriminator citations) | 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 | REVIEW-CLOSED 2026-08-15 | `9a84230c`, `397ccd62`, + the R1 closeout commit | CLOSED (dual-lens: retail fidelity PASS, architectural FAIL → F1-F16 fix round `397ccd62` → narrow re-review CLOSED, both lenses PASS. Re-review residual R1 — the cached wire count is stale by creates-since-last-CharacterList, so a SECOND create after a rejected enter got wire slot N instead of N+1 — fixed in the closeout commit: `LiveSessionController._createsSinceCharacterList` (reset on every fresh wire CharacterList apply + generation reset; applied only to the cached-wire branch — the display-roster fallback already counts prior appends), regression test `SecondCreate_AfterRejectedEnter_GetsTheNextWireSlot` drives create→Ok→rejected guid-enter→ReturnToSelection→second create and pins slots 0/1/2/3. R2: fix-round sha recorded here.) | `RuntimeCharacterCreationState` (new, `src/AcDream.Runtime/Session/`): full CharGenState mirror (heritage/gender/appearance/template/six attributes+locks/55-slot skill set/name/startArea/slot/verification state), mirroring `RuntimeCharacterSelectionState`'s exact pattern (snapshot/delta/event-stream/borrow-only view, generation-gated `Try*` internals). Ports `SetHeritageGroup`, `SetGender`, `SetTemplate`/`ApplyTemplate` (Custom = template 0, Olthoi force-lock), the six attribute setters + `GetAbsRemainingCredits` + `BalanceAttributes` (retail's literal str/end/coord/quick/focus/self round-robin order, cursor-based fairness), `SetSkillLevel` + `ResetSkillLevels`' three-way free-skill baseline (both two-tier cost lookups reuse CC1's `ChargenSkillCreditMath`/`ChargenSkillCost` verbatim — no duplicated math), `RandomizeStartArea`, and `DoFinish`'s complete gate sequence (empty name / unspent attribute credits [see F3 below] / already-Pending / client-side roster-vs-slotCount cap). `LiveSessionController` gained a sibling `IRuntimeCharacterCreationCommands` implementation (command family lands beside `IRuntimeCharacterSelectionCommands`, `IGameRuntimeCommands.CharacterCreation` added with the same default-throw shape as `CharacterSelection`), a `CharacterCreationState` property, `ILiveSessionOperations.CreateCharacter` (default method → `WorldSession.SendCharacterCreation`), and a `HandleCharacterCreationResponse` wire handler subscribed to `WorldSession.CharacterCreateResponseReceived` alongside the existing character-selection bindings. `ILiveSessionLifecycleHost` gained `ApplyCharacterCreated`/`ApplyCreationFailed` as DEFAULT interface methods (no-op) so `AcDream.App`'s existing host implementations keep compiling unchanged — wiring them to `SessionStatusWriter.CharacterCreated`/`CreationFailed` is left to CC4 (Runtime calls the hooks; the App-side forward is a future host-construction change; **F14: zero production call sites exist for these hooks until then — a headless bot cannot observe a create yet**). **Review fix round (this commit):** F1 (HIGH, blocking) the post-create log-straight-in no longer enters by roster INDEX — `WorldSession` gained a guid-based `EnterWorld(uint characterGuid, string accountName, TimeSpan?)` overload (refactored to share `EnterWorldCore` with the index-based overload) plus `ILiveSessionOperations.EnterWorldByGuid` (default method); `LiveSessionController` factored `EnterSelectedCore`/the new `EnterCreatedCharacterCore` through a shared `EnterHighlightedCore(sendEnterWorld)` — the cached wire `CharacterList` is stale for a just-created character by ACE design (ACE appends server-side and replies Ok with no CharacterList resend — `references/ACE/.../CharacterHandler.cs:170-172`), so an index-derived enter could throw (0 pre-existing characters) or enter the WRONG character (N pre-existing, display order ≠ wire order). F2 (HIGH, blocking) the post-create roster append no longer round-trips through `ApplyRoster` (which re-derives EVERY entry's `ActiveIndex` — a wire contract ACE indexes for delete, `CharacterHandler.cs:297` — from display/name-sort order); `RuntimeCharacterSelectionState` gained a real `AppendCreatedCharacter(characterId, name, wireIndex)` primitive that preserves every existing entry's `ActiveIndex` untouched and assigns the new entry's from the pre-create wire `CharacterList.Characters.Count` (0-based, read from the same cached source the index-enter path uses). F3 (MEDIUM-HIGH, blocking) the credit gate was NOT retail — `DoFinish(this, arg2)`'s real gate is `arg2 != 0 && remainingAtrbCredits > 0`: the ordinary click (`arg2=1`) warns-and-refuses, but the warning dialog's own confirm re-invokes `DoFinish(this, 0)`, which skips the check and sends with credits unspent (ACE accepts this). `TryBeginFinish`/`LiveSessionController.Finish`/`IRuntimeCharacterCreationCommands.Finish` gained a `confirmedUnspentCredits`/`confirmUnspentCredits` parameter (default `false` = retail's `arg2=1`) — the plan doc's own "retail FORCES full spend" line above (§Retail ground truth, Finish) was corrected in the same round. F4 (MEDIUM, blocking) a stale out-of-range template index surviving a heritage switch to a heritage with fewer templates now clears to `TemplateUnset` in `ApplyTemplateLocked`, mirroring `ConstrainAllByHeritage @ 0x005C65CC`'s `template_ >= count → template_ = 0xffffffff` clamp (previously it just returned, leaving the stale index to reach the wire). F5 (MEDIUM) AP-207's anchor was wrong (`SetAttribValue` never calls `FitTemplateToCharacter`) — corrected to the four real call sites, including a fourth the original filing also missed (`UpdateToDefaultAttributes @ 0x00482860`). F6 (MEDIUM) `ApplyCreationResponse`'s Pending/Undef branch no longer publishes from inside `lock(_gate)` — every branch now sets `kind` and a single `Publish` runs after the lock releases, matching every sibling method. F7 (MEDIUM) two new tests pin `BalanceAttributes`' persistent cursor: successive overspends absorb from different attributes, and the Self→Strength wrap. F8 (LOW) `ResetSkillLevels`' doc corrected — retail's real gate is BOTH costs `>= 0` (not "either tier"); the dictionary-presence equivalence is a CC1-established, installed-DAT-gated invariant, cited precisely. F9 (LOW) the `Slot` doc corrected — retail DOES assign it (`gmCharacterManagementUI::SelectCharacter @ 0x004EC160` → `SetSlot(GetSlot(...))`), just semantically stale (the last-selected PRE-EXISTING character's slot); conclusion (send 0) unchanged. F10 (LOW) AP-209's `classID` citation completed with the three heritage-dependent branch ids (ordinary/Olthoi/OlthoiAcid) plus admin variants. F11 the integration test fixture no longer stubs `EnterWorld` to a bare counter — it captures guid-based calls and the fixture now has two pre-existing characters whose wire order deliberately differs from alphabetical order, so the roster-preservation assertion actually exercises F2 instead of coinciding with it by accident. F12 filed register row AP-211 for the client-side `RosterFull` slot-cap refusal (acdream-side gate, no retail `DoFinish`-layer counterpart — same-commit rule). F13 `LiveSessionController.Finish`'s bare `catch {}` narrowed to `InvalidOperationException`/`SocketException` and `_scope` bound to a local after validation. F15 `RandomizeStartAreaLocked` now leaves `_startArea` unchanged on an empty list (matching retail's `if (var_9c > 0)` guard) instead of forcing `-1`. Filed register rows AP-207 (FitTemplateToCharacter's FPU-unrecoverable auto-detect skipped — ACE only reads `TemplateOption` for title text; anchor corrected this round), AP-208 (per-style color-count approximated by the shared gender-wide `ClothingColors` list — CC1's model has no per-style palette data), AP-209 (`classID` sent as a placeholder `0` — DAT DID lookup unavailable in Core, ACE ignores the field; branch table added this round), AP-210 (`ApplyTemplate`'s per-attribute guarded sequential set approximated as one atomic replace), AP-211 (this round — the `RosterFull` client-side slot-cap refusal). Tests: `tests/AcDream.Runtime.Tests/CharGen/RuntimeCharacterCreationStateTests.cs` (34 cases — every Finish gate including the F3 confirmed-credits path, the F4 stale-template clamp, the F7 cursor-advance/wrap pair, Ok/each-rejection-code response mapping, duplicate-NameInUse tolerance, Olthoi template lock, attribute-lock/balance interaction, uncostable-skill rejection, generation reset) + `.../Session/LiveSessionControllerCharacterCreationTests.cs` (5 cases — wire-send exactly 55 skill slots via a REAL `WorldSession` + `GameMessageCapture`, decoded byte-for-byte; the full Ok round trip via `WorldSession.ProcessDatagram` reflection asserting F1's guid-based enter + F2's ActiveIndex-preserving roster append + `ApplyCharacterCreated`; the NameInUse round trip asserting `ApplyCreationFailed` + no roster/enter side effect; the local-refusal-never-touches-the-wire gate; the F3 confirmed-unspent-credits send). Runtime 1706/0 (was 1701, was 1667), Core.Net unchanged at 994/0, full solution Release build green. OPEN for CC4+: `RuntimeCharacterCreationState`'s `ChargenOptions` currently defaults to `ChargenOptions.Empty` — threading the installed DAT's loaded options through `GameRuntime`/App startup is unresolved; the `Slot` field's real assignment source (which caller picks the target roster slot) has no decomp citation (ACE ignores it, non-load-bearing); `classID`'s real DAT-DID resolution (AP-209) if a non-ACE server ever needs it; the F14 zero-call-site status hooks. |
|
||
| CC4 | CODE-COMPLETE 2026-08-15 | this commit | Not yet reviewed (Opus dual-lens owed) | Screen shell + form pages (App layer). **Mount:** `CharacterCreationUiController`/`CharacterCreationUiMountCoordinator` (`src/AcDream.App/UI/Layout/`) clone `CharacterManagementUiController`'s recipe — enum `0x10000039` via `RetailDataIdResolver.Resolve(dats, ..., 5u)`, root `0x100003CC` (decomp-verified: `gmCharGenMainUI::gmCharGenMainUI @ 0x004e7eb0`, NOT the plan doc's earlier `0x100003cc`-adjacent guesses — confirmed live against the installed DAT, `[CC4-DAT] enum=0x10000039 -> DID=0x21000038`), fixed-canvas AD-98 treatment shared idempotently with char-management (never nulled on close, so char-management's own per-tick set survives). **Master shell:** progress bar `0x100003ce`, master page `0x100003d0` (state `0x10000025+page-1`), 6 page roots, 6 free-navigation tabs (`0x100003ef..f4`), nav buttons `0x100003c6..cb` — full decomp port of `gmCharGenMainUI::ListenToElementMessage @ 0x004e9450` (Back-at-Heritage→DoExit, Next capped at Summary, Finish Summary-only) and `SetProgressState @ 0x004e7a10` (the Olthoi Profession/Skills/Town tab-hide + forward/backward page redirect, keyed off the LIVE snapshot heritage id every call). Exit confirmation via `RetailDialogFactory.MakeConfirmation` + `ID_CharGen_ExitWarning` (table `0x23000002`, matching `DoExit @ 0x004e8650`); on confirm the screen just closes (visibility only — see AD-99's sibling precedent) rather than porting `gmEpilogueUI`. **Heritage page** (`CharacterCreationHeritagePage.cs`, decomp `InitializePage @ 0x00483a10` + the EXACT button-id→heritage-id map read off `ListenToElementMessage @ 0x00483860`, which is NOT numeric-order — e.g. `0x100005e8`→Tumerok(7)): all 13 buttons, composed description text (`ID_CharGen_Heritage_StartingSkills_Header/Body`, `ID_CharGen_Heritage_BonusSkills_Trained_Header` + per-heritage body — Shadowbound/Penumbraen share one string per the decomp's `case 5: case 0xa:`; Lugian/Olthoi/OlthoiAcid have no bonus-skills string in the retail table at all, confirmed by string-key absence, not guessed). Selecting a heritage ALSO auto-selects its lowest gender key (AD-101 — Appearance's real gender buttons are CC6b's). **Profession page** (`CharacterCreationProfessionPage.cs`, `InitializePage @ 0x00482d50` + `UpdateProfession @ 0x004821b0`'s template map, cited already on `ChargenTemplate`): 7 template buttons (Custom=index 0, the six presets NOT in id order), 6 attribute sliders with the exact e6/e7/e9/e8/ea/eb id↔attribute-id mapping (the documented 3/4 swap), avail/health/stamina/mana. Live-DAT probe found TWO widget-mapping surprises the decomp's `DynamicCast` calls don't predict: the slider's value display (`0x100002ef`) imports as `UiField` not `UiText` (retail's `NumberInputFilter`, `@0x00482e36`) — wired for direct numeric entry via `OnSubmit`, not just display; and all four avail/health/stamina/mana containers (and the Skills credits meter) author as `UIElement_Button` whose Type-12 value child is swallowed by `UiButton.ConsumesDatChildren` before ever becoming an addressable widget — substituted with the button's own `.Label` (AD-103). Health/Stamina/Mana formulas ported from `UpdateAttributeValues @ 0x00482450`: Health=Endurance/2 (int truncation — the decompiler elides the FPU divide at `_ftol2 @0x0048262b`, so the exact MSVC rounding mode is UNVERIFIED beyond well-established AC convention; flagged, not guessed-and-hidden), Stamina=Endurance, Mana=Self; Available=`RemainingAttributeCredits` directly (`UpdateCreditsMeter`-style, no formula). **Skills page** (`CharacterCreationSkillsPage.cs`, `InitializePage @ 0x00481dd0`): ONE flat listbox (AP-213, retail's four-bucket sorted `InsertEntrySorted`/`UpdateSkillEntry` model not ported) driven by CC3's `TrainSkill`/`SpecializeSkill`/`UntrainSkill` + the SAME two-tier `TryGetSkillCost` presence gate `RuntimeCharacterCreationState` uses (16 uncostable ids never listed, matching retail); credits meter via the AD-103 button-Label substitution; info panes `0x100003fb/fc` unbound (no info-pane content source this round). **Town page** (`CharacterCreationTownPage.cs`, `InitializePage @ 0x0047c6d0` + `SetTown @ 0x0047c360`'s literal index map): the four buttons map to LITERAL `startArea` indices (Sanamar→3, Holtburg→0, Yaraq→2, Shoushi→1 — not id order), composed "How To" + per-town description text. **Random** (`0x100003cb`, `DoRandom @ 0x004e7d70`): Heritage/Profession/Town approximated with a uniform pick over every valid option (AP-212 — no `RandomizeHeritageGroup`/`RandomizeTemplate` primitives exist); disabled outright on Skills (no `RandomizeSkills` primitive), Appearance (placeholder), Summary (CC5's warning dialog). **Options threading:** `RuntimeCharacterCreationState.InstallOptions(ChargenOptions)` (new, mirrors `RuntimeCharacterState.InstallSpellMetadata`→`Spellbook.InstallMetadata`'s "install immutable DAT metadata after construction, throw if already active" pattern) called from `ContentEffectsAudioCompositionPhase.Compose` (new `ChargenOptionsInstalled` composition point, right after `SpellMetadataInstalled`) via `IContentEffectsAudioCompositionFactory.LoadChargenOptions`/`InstallChargenOptions` — `ChargenTableReader.Load(dats)` threaded through the SAME DAT-open composition sequence spell metadata uses, always well before any session's `Begin()`. Headless is unaffected (`DirectGameRuntimeCommandAdapter`/`HeadlessSessionHost` never call `InstallOptions`, so headless bots keep the CC3-documented `ChargenOptions.Empty` default — matches the brief). **Status hooks:** `LiveSessionLifecycleBindings` gained optional `CharacterCreated`/`CreationFailed` delegates (default `null` — every pre-CC4 construction site keeps compiling); `LiveSessionLifecycleHost` now overrides both `ILiveSessionLifecycleHost` methods to forward them; `LiveSessionHostBindings` gained matching optional fields threaded through `LiveSessionHost`'s constructor; both `LiveSessionRuntimeFactory.Create` (App/graphical) and `HeadlessSessionHost` wire them to `SessionStatusWriter.CharacterCreated`/`CreationFailed`, closing CC3's F14 (zero call sites). **Deferred command seam:** `IGameRuntimeView.CharacterCreation` (new default-throw member, mirrors `CharacterSelection`), `GameRuntime.CharacterCreation` (passthrough to `Session.CharacterCreation`), `CurrentGameRuntimeAdapter`'s new `CharacterCreationProjection` (IsActive-gated view+command wrapper, mirrors `CharacterSelectionProjection`), `DeferredGameRuntimeStateCommands`'s new `CharacterCreation` view getter + 9 generation-capturing wrapper methods, and `CharacterCreationRuntimeBindings` wired in `InteractionRetainedUiComposition.cs` (`CharacterCreation:` sibling of `CharacterSelection:`, `ResolveText` backed by a fresh `DatStringResolver` per call under `d.DatLock`, `OpenOnStart` from the new `RuntimeOptions.OpenCharacterCreationOnStart` / `ACDREAM_OPEN_CHARGEN=1` env flag — the interim open seam since Create stays ghosted). **Widget types added to `DatWidgetFactory`: NONE** — every id resolves through EXISTING factory mappings (Button=1, Text/Field=12, Scrollbar=11, ListBox=5); the two "new" findings (editable-Field slider value, button-consumed credits/vitals children) are AUTHORED-DATA-DRIVEN outcomes of the existing factory logic, not new widget classes. **Register rows filed (same commit):** AD-101 (Heritage-page auto-gender-select interim default), AD-102 (Viamontian/Sanamar ToD-account-ownership gate omitted — acdream has no account/DLC signal), AD-103 (avail/health/stamina/mana/credits-meter UiButton-Label substitution for retail's swallowed Text-child overlays), AP-212 (Random button's uniform-pick approximation), AP-213 (Skills page flat-listbox simplification), TS-82 (Appearance/Summary placeholder pages, reachable via free tab nav, content-inert pending CC5/CC6a/CC6b). **Tests:** `tests/AcDream.App.Tests/UI/Layout/CharacterCreationLiveDatTests.cs` (7 cases, `ACDREAM_PROBE_LIVE_MOUNT=1`-gated — sweeps every master-shell/page id against the installed DAT and pins the two widget-mapping surprises above) + `CharacterCreationUiControllerTests.cs` (16 cases — hand-built layout fixture, no DAT: page switching, Olthoi tab-hide+redirect, Back/Exit/Random gating, exit-confirm/cancel, per-page command dispatch including the slider/field/skill-row/town-button paths) + `tests/AcDream.Runtime.Tests/CharGen/RuntimeCharacterCreationStateTests.cs` (+4 `InstallOptions` cases) + `tests/AcDream.Runtime.Tests/Session/LiveSessionLifecycleHostTests.cs` (+2 status-hook forwarding cases). Runtime 1713/0 (was 1707), App 5117/13 skips (was 5101/6, +16 new +7 gated-skip), Headless 165/0 unaffected, full solution Release build green. **OPEN for CC5/CC6a/CC6b:** the real Appearance-page gender buttons must retire AD-101's auto-select; Summary's Finish gate, name input, and randomize-warning dialog (currently Finish/Random both hard-disabled); Skills page info-panes `0x100003fb/fc` have no content source wired yet; the four-bucket sorted skill list (AP-213) and retail's exact Random algorithms (AP-212) remain unported if a future gate demands byte-exact parity; the Health/Stamina/Mana rounding-mode residual (see above) would need a live cdb byte trace to fully pin. |
|
||
| CC5 | — | | | |
|
||
| CC6a | — | | | |
|
||
| CC6b | — | | | |
|
||
| CC7 | — | | | |
|