Delivers the CC6a foundation half of the chargen 3D preview: the missing index->ObjDesc appearance factory the campaign plan's acdream-seams section named, plus a static-pose offscreen renderer following PrivateEntityViewportRenderer's proven paperdoll/appraisal architecture. Page mount, spin/color-wheel controls, and rotate/zoom behavior stay out of scope per the CC4-parallel worktree contract (CC6b, after CC4 merges). Core (src/AcDream.Core/CharGen/, pure, no Chorizite on public surfaces): ChargenAppearanceFactory.TryCompose ports gmCG3DView::Update @0x004EE9D0's ObjDesc rebuild in its exact decompiled order - base body, hair style, clothing in retail's own Headgear/Trousers/Shirt/Footwear order (not the UI tab order or the wire's field order, both of which differ), eyes (bald-aware), nose, mouth, then the unconditional skin subpalette, hair color, eye color. ChargenPalSetMath ports PalSet::GetPaletteID's shade-to-index formula, cross-checked three ways (decomp control flow, ACE's PaletteSet.GetPaletteID "Taken from acclient.c" citation, ACViewer's identical slider math). ChargenPalSet/ChargenClothingTable are pure projections behind IChargenPalSetSource/IChargenClothingTableSource so the factory itself never touches a dat. Content (src/AcDream.Content/CharGen/): ChargenAppearanceCatalog is the cached dat-backed implementation of those two source interfaces, mirroring ChargenTableReader's no-leak discipline. App (src/AcDream.App/Rendering/): ChargenPreviewRenderer is a third facade over PrivateEntityViewportRenderer beside PaperdollViewportRenderer and CreatureAppraisalViewportRenderer - no existing rendering file touched. ChargenPreviewCamera carries the four retail-verbatim per-heritage eye profiles from gmCGAppearancePage::Update @0x0047E8F0 (cross-checked against ZoomIn/ZoomOut's identical literals) plus the recovered rotation (3.0 s/revolution) and zoom-tween (0.6 s, reconstructed from the decompiler's garbled float literals - the plan's own "measure if it matters" note is resolved, not garbled beyond recovery). Rotation applies to the character model, not the camera, per gmCGAppearancePage::DoRotation. ChargenPreviewEntityBuilder resolves Setup/GfxObj/Surface/Animation itself (there is no live entity yet), reusing DatLiveEntityProjectionMaterializer's surface-override algorithm and RetailPaperdollPoseApplicator's held-pose technique, generalized to chargen's per-heritage rest-pose DID. Two register rows filed: TS-83 (the plan-named CC6a static-pose-vs-retail- idle-loop staging, CC6b to retire) and TS-82 (measured, not assumed - the un-ported clothing Setup-substitution fallback chain costs nothing for the 9 standard heritages with clothing UI, but Undead's default gear choices genuinely lack ClothingBaseEffects coverage for Undead's own body Setup). Tests: ChargenPalSetMathTests, ChargenAppearanceFactoryTests (hand-built fixtures), ChargenAppearanceCatalogInstalledDatTests (installed-DAT sweep, all 26 heritage/gender combinations, zero missing PalSet/ClothingTable ids), ChargenPreviewCameraTests, ChargenPreviewEntityBuilderTests (installed-DAT-gated, proves a real 34-part Aluvian mesh resolves). Core.Tests 4767/1 skip, Content.Tests 146/0, App.Tests 5121/6 skips - all pre-existing skips, zero failures, full solution Release build green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
258 lines
36 KiB
Markdown
258 lines
36 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 | — | | | |
|
||
| CC5 | — | | | |
|
||
| CC6a | CODE-COMPLETE 2026-08-15 (foundation only — narrowed scope per the CC4∥CC6a parallelism contract: no page mount, no spin/color-wheel controls, no rotate/zoom behavior; all deferred to CC6b after CC4 merges) | single commit, HEAD of `campaign-cc6a` | PENDING (Opus dual-lens not yet run this session) | **Index→ObjDesc factory** (`ChargenAppearanceFactory.TryCompose`, `src/AcDream.Core/CharGen/`, pure — no Chorizite types on its public surface, verified by the existing `ChargenNoChoriziteLeakTests` reflection guard, which walks the whole `AcDream.Core.CharGen` namespace and now covers these new types too): ports `gmCG3DView::Update @ 0x004EE9D0`'s ObjDesc rebuild in its EXACT decompiled append order — base body → hair style → **Headgear → Trousers → Shirt → Footwear** (verified from the decompiled control flow, NOT the UI tab order 5/6/7/8 or the CC2 wire's field order, both of which are headgear/shirt/trousers/footwear and would have been wrong) → eyes (bald-aware) → nose → mouth → skin subpalette (UNCONDITIONAL, no selection gate, unlike every other slot) → hair color → eye color. New pure Core types: `ChargenPalSet`/`ChargenPalSetMath` (shade→index), `ChargenClothingTable`/`ChargenClothingBaseEffect`/`ChargenClothingPaletteTemplate`/`ChargenClothingSubPaletteChoice` (pure ClothingTable projection), `IChargenPalSetSource`/`IChargenClothingTableSource` (DAT-touching work pushed behind these, implemented by the new Content-layer `ChargenAppearanceCatalog`, `src/AcDream.Content/CharGen/`, a cached dat reader mirroring `ChargenTableReader`'s discipline), `ChargenAppearanceSelection` (mirrors `RuntimeCharacterCreationAppearance`'s 14-index/6-shade shape field-for-field so CC6b's Runtime→Core mapping is a trivial copy — kept as a separate type since Core cannot depend on Runtime). **Palette resolution — three-way agreement, no guessing:** `PalSet::GetPaletteID`'s FPU-elided body (`(int)((count - 0.000001) * shade)`, clamped) is corroborated by ACE's `PaletteSet.GetPaletteID` (comment: "Taken from acclient.c"), ACViewer's identical `ClothingTableList.xaml.cs:97` slider math, AND the decomp's own control-flow shape. Skin/hair use `PalSet`+shade indirection (skin: `sex.SkinPalSet`; hair: `sex.HairColors[i]` is ITSELF a PalSet id — confirmed against `PlayerFactory.cs:96`); eye color is the ONE exception — a raw Palette id used directly with NO shade indirection (confirmed against `PlayerFactory.cs:100`'s `EyesPalette = sex.EyeColorList[eyeColor]`, no `GetPaletteID` call, unlike the two lines above it). Hard-coded overlay ranges recovered from the decomp's literal bytes: skin (real offset 0, count 192 → packed 0/24), hair (192/64 → packed 24/8), eyes (256/64 → packed 32/8) — all three independently cross-checked against `PaletteOverride`'s pre-existing `*8` packing doc comment. **Clothing dye resolution, installed-DAT-verified:** `CharGenState::GetHeadgearPaletteTemplateID`/Shirt/Trousers/Footwear (0x005C38F0-0x005C3980) each read a PER-SLOT cached array, but all four are populated from the SAME single `Sex_CG::ClothingColors` dat field — there is no per-slot color list in the schema at all. This CONFIRMS (not merely approximates, contra the original AP-208 framing) that CC3's shared-list design is exactly retail's own mechanism; live-DAT probe: Aluvian male `ClothingColors = {9,6,4,8,7,5,2,3,13}` and the "Cloth Cap" headgear's `ClothingSubPalEffects` keys include every one of those values directly. **Chargen preview renderer** (`ChargenPreviewRenderer`, `ChargenPreviewCamera`/`ChargenPreviewViewportCamera`, `ChargenPreviewEntityBuilder`, all new files under `src/AcDream.App/Rendering/`): follows `PrivateEntityViewportRenderer`'s exact architecture (offscreen target → texture table → `UiViewport` sprite later), a THIRD facade beside `PaperdollViewportRenderer`/`CreatureAppraisalViewportRenderer` — no existing file touched. `ChargenPreviewEntityBuilder.TryBuild` resolves Setup/GfxObj/Surface/Animation dat data itself (there is no live entity yet) using the SAME algorithms as `DatLiveEntityProjectionMaterializer` (surface-override resolution ported verbatim) and `RetailPaperdollPoseApplicator` (final-frame held pose), generalized to the per-heritage rest-pose DID retail actually uses (`m_didAnimationRest`: enum `0x10000005` for every standard heritage — the SAME id the paperdoll's own pose reads — `0x10000011` for Olthoi, `0x10000013` for OlthoiAcid, all resolved through master-map slot 7). **Camera** (`gmCGAppearancePage::Update @ 0x0047E8F0`, cross-checked against the identical literals in `ZoomIn`/`ZoomOut @ 0x0047CF00`/`0x0047D050`): four distinct default (zoomed-in) eye profiles across the 13 heritages — Olthoi (0,-1.85,1.85), OlthoiAcid (0,-3.05,2.75), Tumerok (0,-0.85,1.65), everyone else including Gearknight (0,-0.55,1.65) — direction always identity (zero yaw/pitch, same convention `DollCamera` already established); zoomed-OUT profiles also recorded for CC6b (Olthoi (0,-3.80,1.15), OlthoiAcid (0,-5.70,1.65), everyone else (0,-2.50,0.95) — no Tumerok special case on the OUT side). Rotation is NOT a camera property: retail's continuous-rotation button spins the CHARACTER (`CPhysicsObj::set_heading`), not the camera — CC6b's heading parameter belongs on the entity builder. **Constants recovered, not just cited (deliverable #4):** `RotationSecondsPerRevolution = 3.0` (clean in the decomp, no reconstruction needed) and `ZoomTweenDurationSeconds = 0.6` — the plan's own risk list flagged this SECOND constant as "decompiler-garbled"; it is NOT unrecoverable: reinterpreting the decompiler's garbled float literal as the raw low-32-bit store and pairing it with the (clean) high dword reconstructs the exact IEEE-754 double both at `DoZoomAnimation`'s reset-default site (→ 0.6) AND independently at `ZoomIn`/`ZoomOut`'s `-0.1` invalidation sentinel (→ exactly the textbook IEEE-754 bit pattern for -0.1, cross-confirming the reconstruction technique itself). **Register rows filed (same commit):** TS-83 (the CC6a static-pose-vs-retail-idle-loop staging, explicitly named by the plan, to be retired by CC6b) and TS-82 (a MEASURED, not assumed, scope cut — CC6a's composer does not port retail's ~8-branch clothing Setup-substitution chain; the installed-DAT catalog test proves this costs nothing for the 9 standard heritages whose UI shows clothing controls, but Undead's default headgear/trousers/footwear choices genuinely miss `ClothingBaseEffects` coverage for Undead's own live body Setup on both genders — a real, narrow, documented gap, not a "confirmed unreachable" overclaim). **Tests:** `ChargenPalSetMathTests` (10 cases, the shade-index formula), `ChargenAppearanceFactoryTests` (19 hand-built-fixture cases covering setup resolution, retail append order, bald-strip selection, unconditional skin, missing-dat diagnostics, out-of-range indices), `ChargenAppearanceCatalogInstalledDatTests` (installed-DAT sweep, all 26 heritage/gender combinations, zero missing PalSet/ClothingTable ids — PASSED live against the installed EoR dat), `ChargenPreviewCameraTests` (17 cases, every per-heritage literal + the two recovered constants), `ChargenPreviewEntityBuilderTests` (3 cases, installed-DAT-gated, proves a real Aluvian-male 34-part mesh + Olthoi's distinct pose DID both resolve without touching a live entity). Final counts this session: Core.Tests 4767/1 skip, Content.Tests 146/0 skips, App.Tests 5121/6 skips — all pre-existing skips, zero failures, full solution Release build green. |
|
||
| CC6b | — | | | |
|
||
| CC7 | — | | | |
|