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

@ -540,7 +540,8 @@ internal sealed class FrameRootCompositionPhase
new PrivateEntityViewportFrameGroup(
live.PaperdollPresenter,
live.CreatureAppraisalPresenter,
live.ChargenPreviewController),
live.ChargenPreviewController,
live.SummaryPreviewController),
retainedGameplayUi,
// The ImGui developer-tools frontend was removed at Campaign V
// slice V11; this optional hook is unbound until a follow-up

View file

@ -1007,6 +1007,11 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
DatStringResolver.ComputeHash(key));
}
},
SetName: late.GameRuntime.CharacterCreationSetName,
AcknowledgeRejection: late.GameRuntime.CharacterCreationAcknowledgeRejection,
RandomizeCharacter: late.GameRuntime.CharacterCreationRandomizeCharacter,
RandomizeAppearance: late.GameRuntime.CharacterCreationRandomizeAppearance,
RandomizeClothing: late.GameRuntime.CharacterCreationRandomizeClothing,
OpenOnStart: d.Options.OpenCharacterCreationOnStart)
: null);
RetailUiRuntime runtime = lease.Mount(

View file

@ -241,6 +241,28 @@ internal sealed class DeferredGameRuntimeStateCommands
Invoke((commands, generation) =>
commands.CharacterCreation.SetShade(generation, slot, value));
// ── Campaign CC slice CC5: Summary page + RandomizeCharacter commands ──
public RuntimeCommandResult CharacterCreationSetName(string name) =>
Invoke((commands, generation) =>
commands.CharacterCreation.SetName(generation, name));
public RuntimeCommandResult CharacterCreationAcknowledgeRejection() =>
Invoke((commands, generation) =>
commands.CharacterCreation.AcknowledgeRejection(generation));
public RuntimeCommandResult CharacterCreationRandomizeCharacter() =>
Invoke((commands, generation) =>
commands.CharacterCreation.RandomizeCharacter(generation));
public RuntimeCommandResult CharacterCreationRandomizeAppearance() =>
Invoke((commands, generation) =>
commands.CharacterCreation.RandomizeAppearance(generation));
public RuntimeCommandResult CharacterCreationRandomizeClothing() =>
Invoke((commands, generation) =>
commands.CharacterCreation.RandomizeClothing(generation));
// ── Campaign FA slice FA4: fellowship page commands ─────────────────
// Same "capture view+commands under one generation" shape as every
// method above — a displaced session (reconnect mid-click) can never

View file

@ -132,6 +132,11 @@ internal sealed record LivePresentationResult(
// a leased composition resource, mirroring PaperdollViewportRenderer).
ChargenPreviewRenderer? ChargenPreviewRenderer,
ChargenPreviewController? ChargenPreviewController,
// Campaign CC slice CC5: the Summary page's own gmCG3DView instance —
// a SEPARATE leased renderer/controller pair, same split reasoning as
// the Appearance preview fields immediately above.
ChargenPreviewRenderer? SummaryPreviewRenderer,
ChargenPreviewController? SummaryPreviewController,
WbFrustum EnvCellFrustum,
EnvCellRenderer? EnvCellRenderer,
LandblockPresentationPipeline LandblockPipeline,
@ -1102,6 +1107,82 @@ internal sealed class LivePresentationCompositionPhase
+ "time — the Appearance page's zoom/rotate controls and "
+ "3D preview will not function this session.");
}
// Campaign CC slice CC5: the Summary page's OWN gmCG3DView instance
// (gmCGSummaryPage::InitializePage @0x0047bbf0, confirmed a SEPARATE
// instance from the Appearance page's own during the CC6b-MOUNT
// review) — same one-shot binding shape as the Appearance preview
// immediately above (AP-221's own disposition applies here too: a
// DAT/resource read not ready on this exact composition frame means
// the Summary preview stays permanently unbound for the session,
// same tracked follow-up as the Appearance preview). No zoom/rotate
// control surface is wired — retail's Summary page has no such
// buttons (only <c>StartAnimation</c>'s idle loop and a fixed 180°
// heading), so this controller's ZoomIn/RotateClockwise etc. simply
// never get called.
CompositionAcquisitionScope.CompositionAcquisitionLease<
ChargenPreviewRenderer>? summaryPreviewLease = null;
ChargenPreviewController? summaryPreviewController = null;
if (dispatcherLease.Resource is { } summaryDispatcher
&& interaction.RetainedUi?.Runtime.SummaryPreviewViewportWidget is { } summaryViewport)
{
var summaryCamera = new ChargenPreviewCamera();
summaryPreviewLease = scope.Acquire(
"summary preview viewport",
() => new ChargenPreviewRenderer(
worldPassScope
?? throw new InvalidOperationException(
"The graphics backend must publish a world pass scope."),
host.GpuDevice,
host.GpuFrameLifetime,
summaryDispatcher,
foundation.SceneLighting!,
foundation.TextureCache,
foundation.MeshAdapter!,
camera: summaryCamera),
static value => value.Dispose());
IUiViewportRenderer? previousSummaryRenderer = summaryViewport.Renderer;
summaryViewport.Renderer = summaryPreviewLease.Resource;
bindings.AdoptRelease(
"summary preview viewport target",
() =>
{
if (ReferenceEquals(summaryViewport.Renderer, summaryPreviewLease.Resource))
summaryViewport.Renderer = previousSummaryRenderer;
});
var summaryCatalog = new AcDream.Content.CharGen.ChargenAppearanceCatalog(content.Dats);
summaryPreviewController = new ChargenPreviewController(
summaryPreviewLease.Resource,
summaryCamera,
new RetailChargenPreviewFrameView(
summaryViewport,
new RetailSummaryPreviewPageVisibility(interaction.RetainedUi.Runtime)),
content.Dats,
content.AnimationLoader,
summaryCatalog,
summaryCatalog,
d.DatLock);
interaction.RetainedUi.Runtime.SummaryPreviewControl = summaryPreviewController;
bindings.AdoptRelease(
"summary preview control",
() =>
{
if (ReferenceEquals(
interaction.RetainedUi.Runtime.SummaryPreviewControl,
summaryPreviewController))
{
interaction.RetainedUi.Runtime.SummaryPreviewControl = null;
}
});
}
else if (dispatcherLease.Resource is not null && interaction.RetainedUi is not null)
{
Console.WriteLine(
"[UI] summary preview viewport unavailable at composition "
+ "time — the Summary page's 3D preview will not function "
+ "this session.");
}
Fault(LivePresentationCompositionPoint.PrivateCreatureViewportsCreated);
var envCellFrustum = new WbFrustum();
@ -1410,6 +1491,8 @@ internal sealed class LivePresentationCompositionPhase
creatureAppraisalPresenter,
chargenPreviewLease?.Resource,
chargenPreviewController,
summaryPreviewLease?.Resource,
summaryPreviewController,
envCellFrustum,
envCellLease.Resource,
landblockPipeline,