acdream/docs/plans/2026-08-15-character-creation-campaign.md
Erik 397ccd62cd fix(runtime): Campaign CC slice CC3 review-fix round — F1-F16
Opus dual-lens review of CC3's RuntimeCharacterCreationState passed on
retail fidelity but failed the controller integration: the post-create
log-straight-in indexed the CACHED wire CharacterList, which ACE never
resends after a create (it only appends server-side and replies Ok) —
with zero pre-existing characters this throws, with N it can silently
enter the WRONG character. The same stale-index problem corrupted every
pre-existing character's delete slot on roster re-sort. Fixes all four
blocking findings plus a credit-gate correctness bug (retail warns and
lets the user confirm through unspent credits; it does not force a full
spend) and eight lower-severity findings from the same review round.

F1 (blocking): WorldSession gained a guid-based EnterWorld(uint,string,
TimeSpan?) overload sharing EnterWorldCore with the index-based one;
ILiveSessionOperations gained a default EnterWorldByGuid method.
LiveSessionController factored EnterSelectedCore/the new
EnterCreatedCharacterCore through a shared EnterHighlightedCore so the
post-create enter sends by the exact guid the 0xF643 Ok reply carried,
never by a roster index.

F2 (blocking): RuntimeCharacterSelectionState gained a real
AppendCreatedCharacter primitive that preserves every existing entry's
ActiveIndex (a wire contract — SendDeleteCharacter sends it as the
CharacterSet slot) and assigns the new entry's from the pre-create wire
roster count, instead of round-tripping the post-create roster through
ApplyRoster's name-sort-and-renumber.

F3 (blocking): retail's DoFinish(this, arg2) gate is
"arg2 != 0 && remainingAtrbCredits > 0" — the ordinary click warns and
refuses, but the warning dialog's own confirm re-invokes DoFinish(this,
0), which sends anyway with credits unspent (ACE accepts this).
TryBeginFinish/Finish gained a confirmedUnspentCredits parameter; the
plan doc's "retail FORCES full spend" line is corrected in the same
commit.

F4 (blocking): a stale out-of-range template index surviving a heritage
switch to a heritage with fewer templates now clears to TemplateUnset,
matching ConstrainAllByHeritage's clamp.

F5/F9/F10: three register-row/doc citation corrections (AP-207's real
FitTemplateToCharacter call sites — a fourth one the original filing
also missed; the Slot field's real retail assignment source; AP-209's
classID branch table for Olthoi/OlthoiAcid). F6: ApplyCreationResponse
no longer publishes from inside the owner lock. F7: two new tests pin
BalanceAttributes' persistent donor cursor (successive-overspend
advance, Self-to-Strength wrap). F8: ResetSkillLevels' doc corrected to
retail's real both-costs->=0 gate. F11: the integration test fixture
captures guid-based enter calls and uses two pre-existing characters
whose wire order differs from alphabetical order, so the roster
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 (no retail DoFinish-layer counterpart). F13: narrowed
Finish's bare catch to InvalidOperationException/SocketException and
bound _scope to a local. F15: RandomizeStartAreaLocked leaves the start
area unchanged on an empty list instead of forcing -1, matching retail.

Runtime 1706/0 (was 1701), Core.Net unchanged at 994/0, full solution
Release build green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 15:10:43 +02:00

28 KiB
Raw Blame History

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. 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 retails 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 FIX ROUND COMPLETE 2026-08-15 (Opus dual-lens review found blocking findings F1-F4 on the controller integration, retail fidelity PASS; fix round addressed F1-F16; narrow re-review owed) 9a84230c (implementation), this fix round (see git log — sha unknowable pre-commit) Blocking findings fixed (see fix-round summary below); narrow re-review owed 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 @ 0x004EC160SetSlot(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
CC6b
CC7