feat(chargen): Campaign CC slice CC5 — Summary page, Finish flow, RandomizeCharacter port

Fills TS-82's Summary placeholder with a faithful port of gmCGSummaryPage
(name field with NameInputFilter + the retail commit-on-focus-lost/submit
dispatch + the >32-char ID_CharGen_NameTooLong reject-and-revert path, the
REAL three-row-template listbox confirmed against the installed EoR dat
before writing any page code, and Summary's own independent gmCG3DView
preview instance wired through a second ChargenPreviewController pair
mirroring the Appearance page's exact composition shape).

Ports CharGenState::RandomizeCharacter and its six sub-primitives into
RuntimeCharacterCreationState — not approximated: the RandInt/RollDice
semantics are independently confirmed from both the decompiled RNG bodies
and the CharGenStateVtbl union struct in acclient.h. Three consumers:
the chargen screen's open-roll (retiring AP-214's honest-blank deviation
and reproducing the Appearance page's gender-flip-on-init quirk), the
Summary page's Random button (behind the retail randomize-warning
confirm), and the Appearance page's Random button (narrowing AP-212 to
just Heritage/Profession/Town's still-approximated rolls and Skills'
still-unported RandomizeSkills).

Wires the Finish button (previously ghosted) with retail's NoName/
CreditWarning dialog pair, adds the F12 amendment's HeritageOrGenderUnset
local refusal to TryBeginFinish (register AP-223) as a defensive backstop
now that the screen-open roll normally makes it unreachable, and wires
the four ID_Character_Err_* rejection dialogs for the 0xF643 response
codes CC3 already parsed but nothing displayed.

Register: TS-82 retired, AP-214 retired, AP-212 narrowed, AP-223/224/225
filed (heritage/gender Finish refusal, Summary's two-bucket skill-list
narrowing, the 32-vs-33 name-length threshold reconciliation).

Runtime 1722/0 (was 1713), App 5240/3 skips (was 5223/3), Headless 166/0
unchanged, full solution Release build green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-16 00:01:07 +02:00
parent 6114b2dda2
commit 34e3a534be
22 changed files with 2217 additions and 79 deletions

View file

@ -21,6 +21,15 @@ internal static class RuntimeCharacterCreationStateFixture
public const uint ImpoverishedId = 90u;
public const uint MaleGenderKey = 1u;
/// <summary>Campaign CC slice CC5: <c>RandomizeCharacter</c>'s gender
/// roll (<c>RollDice(1,2)</c>) needs BOTH gender keys resolvable on the
/// four human heritages, or half of all random seeds would land on a
/// gender <see cref="ChargenHeritageOptions.GendersByKey"/> can't
/// resolve and silently leave appearance untouched (a real, harmless
/// <c>ConstrainAllByGender</c>-style fallback — but not what these tests
/// are pinning).</summary>
public const uint FemaleGenderKey = 2u;
/// <summary>str=10 end=10 coord=10 quick=10 focus=10 self=10 — the
/// budget-66 heritage leaves 6 credits unspent after this template,
/// matching retail's "Custom sits at the floor" finding.</summary>
@ -99,6 +108,16 @@ internal static class RuntimeCharacterCreationStateFixture
Footwear: [new ChargenGearOption("Boots", 4u, 303u)],
ClothingColors: [400u, 401u, 402u]);
// Same shape as `gender`, just the other GenderKey — every list is
// deliberately populated so a full RandomizeCharacter roll never
// finds an empty option list to skip.
var femaleGender = gender with { GenderKey = (int)FemaleGenderKey, Name = "Female" };
var bothGenders = new Dictionary<int, ChargenGenderOptions>
{
[(int)MaleGenderKey] = gender,
[(int)FemaleGenderKey] = femaleGender,
};
var aluvianTemplates = new List<ChargenTemplate>
{
new(
@ -129,7 +148,7 @@ internal static class RuntimeCharacterCreationStateFixture
SecondaryStartAreaIndices: [],
SkillCostsBySkillId: skillCosts,
Templates: aluvianTemplates,
GendersByKey: new Dictionary<int, ChargenGenderOptions> { [(int)MaleGenderKey] = gender });
GendersByKey: bothGenders);
var olthoiTemplates = new List<ChargenTemplate>
{
@ -163,6 +182,28 @@ internal static class RuntimeCharacterCreationStateFixture
Templates: olthoiTemplates,
GendersByKey: new Dictionary<int, ChargenGenderOptions> { [(int)MaleGenderKey] = gender });
// Campaign CC slice CC5: CharGenState::RandomizeCharacter @
// 0x005c6d80 rolls a heritage id uniformly in [1, hasToD?4:3] — the
// four HUMAN heritage groups (ChargenHeritageGroup.Aluvian..
// Viamontian). RandomizeCharacterLocked's tests need every one of
// those four ids resolvable, not just Aluvian, so the roll can never
// silently land on a missing heritage. Ids 2-4 mirror Aluvian's own
// shape (same gender/template data) — the roll target, not the
// template/skill-budget math, is what those tests exercise.
ChargenHeritageOptions MakeHumanHeritage(uint id, string name) => new(
id,
name,
IconId: 0u,
SetupId: 0x2000054u,
EnvironmentSetupId: 0u,
AttributeCredits: 66u,
SkillCredits: 50u,
PrimaryStartAreaIndices: [0, 1],
SecondaryStartAreaIndices: [],
SkillCostsBySkillId: skillCosts,
Templates: aluvianTemplates,
GendersByKey: bothGenders);
// A deliberately impoverished heritage — just enough skill credits
// to train SkillTrainSpecialize but never specialize it — so a
// TrySpecializeSkill affordability refusal is directly testable
@ -198,6 +239,12 @@ internal static class RuntimeCharacterCreationStateFixture
new Dictionary<uint, ChargenHeritageOptions>
{
[AluvianId] = aluvian,
[(uint)ChargenHeritageGroup.Gharundim] = MakeHumanHeritage(
(uint)ChargenHeritageGroup.Gharundim, "Gharu'ndim"),
[(uint)ChargenHeritageGroup.Sho] = MakeHumanHeritage(
(uint)ChargenHeritageGroup.Sho, "Sho"),
[(uint)ChargenHeritageGroup.Viamontian] = MakeHumanHeritage(
(uint)ChargenHeritageGroup.Viamontian, "Viamontian"),
[OlthoiId] = olthoi,
[ImpoverishedId] = impoverished,
},