feat(runtime): Campaign CC slice CC3 — RuntimeCharacterCreationState
Ports retail's CharGenState as the one Runtime-owned character-creation state machine, mirroring RuntimeCharacterSelectionState's exact pattern (snapshot/delta/event-stream, borrow-only view, generation-gated commands, one mutable owner, no App types). Every command ports a named retail function: SetHeritageGroup, SetGender, SetTemplate/ApplyTemplate (Custom = template 0, Olthoi force-lock), the six attribute setters plus GetAbsRemainingCredits/BalanceAttributes (retail's literal round-robin order and fairness cursor), SetSkillLevel plus ResetSkillLevels' free-skill baseline (reusing CC1's ChargenSkillCreditMath two-tier cost lookup verbatim), RandomizeStartArea, and DoFinish's complete gate sequence (empty name / unspent attribute credits / already-Pending / client-side roster-vs-slotCount cap). LiveSessionController gained a sibling IRuntimeCharacterCreationCommands implementation, a CreateCharacter wire hook, and a response handler that reuses existing machinery rather than inventing new paths: the Ok identity is appended to the roster via RuntimeCharacterSelectionState's own ApplyRoster, and the "log straight in" behavior reuses the private EnterSelectedCore. ILiveSessionLifecycleHost gained two default-no-op hooks (ApplyCharacterCreated/ApplyCreationFailed) so AcDream.App needs zero changes to keep compiling; wiring them to the status stream is a CC4 follow-up. Filed four divergence-register rows for the corners deliberately not ported: the FPU-unrecoverable FitTemplateToCharacter auto-detect (AP-207, ACE only reads the field for title text), the per-style color-count approximation (AP-208, CC1's model has no per-style palette data), the classID DAT-DID placeholder (AP-209, ACE ignores the field), and ApplyTemplate's atomic-vs-sequential attribute apply (AP-210). 34 new tests: full state-machine coverage (every Finish gate, every rejection-code mapping, duplicate-NameInUse tolerance, Olthoi lock, attribute balance/lock interaction, uncostable-skill rejection) plus a LiveSessionController integration suite proving the wire send is exactly 55 skill slots (decoded from a real WorldSession + GameMessageCapture) and the full Ok/rejection round trip through WorldSession.ProcessDatagram. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
f3ef7baae2
commit
9a84230c4f
9 changed files with 3043 additions and 5 deletions
File diff suppressed because one or more lines are too long
|
|
@ -230,7 +230,7 @@ the user gate.
|
|||
|---|---|---|---|---|
|
||||
| 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 | — | | | |
|
||||
| CC3 | IMPLEMENTED 2026-08-15 (unreviewed — Opus dual-lens review owed per campaign process) | `(HEAD — see git log)` | — | `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 / 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. On Ok: appends the identity to the roster by REUSING `RuntimeCharacterSelectionState.ApplyRoster` (read current entries via `View.Visit`, append, re-apply — no new roster-mutation primitive) and logs straight in by REUSING the private `EnterSelectedCore` (no second enter route) — `gmCharGenMainUI::Update @ 0x004E8460`'s per-frame name-scan is deliberately not re-implemented since the SAME `0xF643` Ok reply already carries the exact guid/name (an equivalent, not divergent, substitution). `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). Filed register rows AP-207 (FitTemplateToCharacter's FPU-unrecoverable auto-detect skipped — ACE only reads `TemplateOption` for title text), 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), AP-210 (`ApplyTemplate`'s per-attribute guarded sequential set approximated as one atomic replace). Tests: `tests/AcDream.Runtime.Tests/CharGen/RuntimeCharacterCreationStateTests.cs` (30 cases — every Finish gate, Ok/each-rejection-code response mapping, duplicate-NameInUse tolerance, Olthoi template lock, attribute-lock/balance interaction, uncostable-skill rejection, generation reset) + `.../Session/LiveSessionControllerCharacterCreationTests.cs` (4 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 roster append + auto-enter + `ApplyCharacterCreated`; the NameInUse round trip asserting `ApplyCreationFailed` + no roster/enter side effect; the local-refusal-never-touches-the-wire gate). Runtime 1701/0 (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. |
|
||||
| CC4 | — | | | |
|
||||
| CC5 | — | | | |
|
||||
| CC6a | — | | | |
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue