fix(chargen): Campaign CC CC5 review fix round — F1-F14
Opus dual-lens review of 34e3a534+a975efd1 returned architectural PASS-with-items / retail-fidelity FAIL. Every finding fixed: - F1 (BLOCKER): deleted CharacterCreationSummaryPage's dead _suppressNextFieldEvent latch. UiField.SetText never raises OnFocusLost/OnSubmit, so the latch never had anything genuine to suppress — it stayed armed until the player's own next real commit and silently ate their typed name. - F2: byte-re-derived gmCharGenMainUI::RecvNotice_ CharGenVerificationResponse @0x004e9030's jump table — Pending is an explicit switch case landing on the SAME NameDBDown label as Corrupt/DatabaseDown, and Undef/out-of-range falls through the function's own unsigned-underflow default arm to that identical label. Retail's dispatch has NO silent branch. ApplyCreationResponse now produces a real rejection for Pending/Undef instead of a silent reset; ReconcileDialogs maps them to NameDBDown. Corrects the wrong "retail swallows Pending" claim everywhere it was repeated (plan doc, Core.Net doc comment, Runtime doc comments). - F3: skill rows now use the key/value template with CharGenState::GetSkillScore @0x005C4B50 as the value (ported via the new RetailSkillFormula.CalculateChargenScore / ChargenSkillScoreResolver, wired through a new GetSkillScore binding), not template 0/name-only; bucket headers are unconditional. Writing this fix's own regression test surfaced a second, more severe bug: CharacterCreationSummaryPage never wired _list.TemplateResolver at all, so RebuildListbox has been a silent no-op since CC5 shipped — fixed by threading templateResolver through the page's constructor, matching every sibling UiTemplateListBox owner. - F4: added the missing _errorMessageDialogContext one-outstanding guard to the 0xF643 rejection dialog, matching MakeErrorMessageDialog's own guard @0x004e8cc4 and the other four sibling dialogs' shape (registered in CloseAllDialogs, suppress- callback checked). - F5: the Summary preview camera now seeds/re-derives retail's zoomed-OUT eye (byte-decoded (0,-2.5,0.95) at gmCGSummaryPage:: InitializePage ~0x0047bd14-0x0047bd44) instead of Appearance's zoomed-in default, via a new ChargenPreviewController useZoomedOutEye flag. - F6: retired AP-225 outright — re-derived the ListenToElementMessage length gate is NUL-inclusive, so MaxNameLength=32 was always byte-correct, not merely internally consistent. - F7: amended AP-221 to cover the Summary preview's duplicate one-shot-composition binding gap (CC5 duplicated the pattern instead of closing it). - F8: byte-decoded GetRandomReal @0x00563940's fmul operand at 0x007cd650 — an 8-byte double, not a 4-byte float — is EXACTLY 1.0/32767.0, not 1/32768. Added RollShadeLocked (_random.Next(32768) * (1.0/32767.0)) and switched all six shade rolls onto it. - F9: evaluated porting retail's exact empty-name-commit no-op (NUL-inclusive length==1 skips SetName entirely) and rejected it — it would fight the F1 field-sync model by spontaneously reverting an emptied field on the next unrelated revision bump. Kept the clear, documented the tradeoff, filed AP-227. - F11: filed AP-226 documenting retail's static pcProfessions/pcGender/ pcHeritage/pcTown label tables versus acdream's DAT-sourced labels, including the non-human-heritage-renders-bare-"Heritage:" retail quirk. - F12: added exclude-current determinism (count-2 lists), Random- clears-name, repeat-identical-rejection-reshows, and RebuildListbox content tests (the last one found F3's TemplateResolver bug). - F13: threaded an optional Random through GameRuntimeDependencies -> LiveSessionController -> RuntimeCharacterCreationState, matching the existing TimeProvider injection shape, closing the Slice-K determinism hazard on a bot-reachable Randomize* command family. - F14: RandomizeCharacterLocked now assigns _heritageId unconditionally before the TryGetHeritage gate, matching retail's SetHeritageGroup @0x005C67A0 (mHeritageGroup written before the DAT lookup). Gates: Runtime 1726/0 (was 1722/0), App 5242/3 skips (was 5240/3), Headless 166/0, Core.Net 993/994 (the one failure, NakEmissionTests LossSoak, is a known pre-existing flake — passes standalone), full solution Release build green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
a975efd1d5
commit
0c8e1e7df1
14 changed files with 720 additions and 141 deletions
|
|
@ -61,6 +61,13 @@ public sealed record CharacterCreationRuntimeBindings(
|
|||
/// <summary>CC5: the Appearance page's Random button on its Clothes
|
||||
/// sub-tab.</summary>
|
||||
Func<RuntimeCommandResult>? RandomizeClothing = null,
|
||||
/// <summary>CC5 review fix round, F3 (2026-08-16): the Summary page's
|
||||
/// skill-row VALUE — <c>CharGenState::GetSkillScore @ 0x005C4B50</c>,
|
||||
/// wired at composition time (<c>AcDream.App.Net.ChargenSkillScoreResolver</c>)
|
||||
/// so this UI-layer record stays free of a direct DAT/Chorizite
|
||||
/// dependency, matching <see cref="ResolveText"/>'s own shape.
|
||||
/// <see langword="null"/> degrades to a "no score available" 0.</summary>
|
||||
Func<uint, ChargenAttributeValues, ChargenSkillAdvancementClass, uint>? GetSkillScore = null,
|
||||
bool OpenOnStart = false);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -188,6 +195,13 @@ internal sealed class CharacterCreationUiController : IDisposable
|
|||
private uint _creditWarningDialogContext;
|
||||
private uint _randomizeWarningDialogContext;
|
||||
private uint _noNameWarningDialogContext;
|
||||
// CC5 review fix round F4 (2026-08-16): gmCharGenMainUI's own
|
||||
// m_uiErrorMessageContext (MakeErrorMessageDialog @ 0x004e8cb0's guard
|
||||
// at 0x004e8cc4, assigned at 0x004e8dd3, cleared by the dtor at
|
||||
// 0x004e83b3 alongside m_uiPleaseWaitContext/m_uiExitContext) — the
|
||||
// 0xF643 rejection dialog was the only one of the five dialogs this
|
||||
// controller owns without this same one-outstanding-dialog guard.
|
||||
private uint _errorMessageDialogContext;
|
||||
private RuntimeCharacterCreationRejection? _lastShownRejection;
|
||||
private bool _suppressDialogCallbacks;
|
||||
private bool _disposed;
|
||||
|
|
@ -270,7 +284,7 @@ internal sealed class CharacterCreationUiController : IDisposable
|
|||
_townPage = new CharacterCreationTownPage(townPageRoot, bindings);
|
||||
_appearancePage = new CharacterCreationAppearancePage(appearancePageRoot, bindings);
|
||||
_summaryPage = new CharacterCreationSummaryPage(
|
||||
summaryPageRoot, bindings, dialogs, strings.NameTooLong);
|
||||
summaryPageRoot, bindings, dialogs, strings.NameTooLong, templateResolver);
|
||||
|
||||
// gmCharGenMainUI::ListenToElementMessage @ 0x004e9450.
|
||||
_back.OnClick = OnBack;
|
||||
|
|
@ -929,16 +943,21 @@ internal sealed class CharacterCreationUiController : IDisposable
|
|||
// ── 0xF643 rejection dialogs (Handle_CharGenVerificationResponse @ ──
|
||||
// ── 0x0055E8B0) ──────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Ports the four rejection-dialog mappings from
|
||||
/// <c>Handle_CharGenVerificationResponse</c>'s per-case switch (restated
|
||||
/// on <see cref="RuntimeCharacterCreationRejection"/>'s own doc
|
||||
/// comment); Pending/Undef never reach this method (CC3's
|
||||
/// <c>ApplyCreationResponse</c> treats them as a silent state reset with
|
||||
/// no <see cref="RuntimeCharacterCreationRejection"/> produced at all).
|
||||
/// Dedups against the LAST rejection instance already shown so a
|
||||
/// same-value re-check on a later <see cref="Tick"/> (this method runs
|
||||
/// every tick, not just on revision change) doesn't reopen the dialog
|
||||
/// the player already dismissed.</summary>
|
||||
/// <summary>
|
||||
/// Ports the COMPLETE rejection-dialog mapping from
|
||||
/// <c>gmCharGenMainUI::RecvNotice_CharGenVerificationResponse @
|
||||
/// 0x004e9030</c>'s own switch + its <c>(arg2-1) > 6</c>
|
||||
/// unsigned-underflow default arm (restated on
|
||||
/// <see cref="RuntimeCharacterCreationRejection"/>'s own doc comment).
|
||||
/// CC5 review-fix round F2 (2026-08-16): every non-Ok code now reaches
|
||||
/// this method (<c>RuntimeCharacterCreationState.ApplyCreationResponse</c>
|
||||
/// no longer special-cases Pending/Undef as a silent reset) and every
|
||||
/// branch here resolves to a real dialog — retail's dispatch has NO
|
||||
/// silent case. Dedups against the LAST rejection instance already
|
||||
/// shown so a same-value re-check on a later <see cref="Tick"/> (this
|
||||
/// method runs every tick, not just on revision change) doesn't reopen
|
||||
/// the dialog the player already dismissed.
|
||||
/// </summary>
|
||||
private void ReconcileDialogs(RuntimeCharacterCreationSnapshot snapshot)
|
||||
{
|
||||
RuntimeCharacterCreationRejection? rejection = snapshot.LastRejection;
|
||||
|
|
@ -951,24 +970,42 @@ internal sealed class CharacterCreationUiController : IDisposable
|
|||
return;
|
||||
_lastShownRejection = rejection;
|
||||
|
||||
string? key = rejection.Value.Code switch
|
||||
// MakeErrorMessageDialog's own guard @0x004e8cc4: a context already
|
||||
// open is a no-op (the SECOND rejection's dialog is silently
|
||||
// dropped, not queued) — F4's fix, matching the four sibling
|
||||
// dialogs' shape. _lastShownRejection is already updated above even
|
||||
// when this guard blocks the dialog, which is retail-faithful: a
|
||||
// later Tick with the SAME rejection value must not retry it either
|
||||
// (this scenario is not reachable through the ordinary UI today —
|
||||
// TryBeginFinish's AlreadyPending refusal means a second Finish
|
||||
// cannot land while a rejection is still unacknowledged — but the
|
||||
// guard exists so the SHAPE matches retail's even if a future
|
||||
// caller reaches it).
|
||||
if (_errorMessageDialogContext != 0u)
|
||||
return;
|
||||
|
||||
// Pending/Corrupt/DatabaseDown are explicit switch cases in retail's
|
||||
// own dispatch landing on the SAME "ID_Character_Err_NameDBDown"
|
||||
// label; Undef and any code outside 1..7 fall through that
|
||||
// function's unsigned-underflow default arm to the identical label
|
||||
// — the `_` arm below is that default, not a "no dialog" case.
|
||||
string key = rejection.Value.Code switch
|
||||
{
|
||||
CharGenVerificationResponse.Code.NameInUse => "ID_Character_Err_NameReserved",
|
||||
CharGenVerificationResponse.Code.NameBanned => "ID_Character_Err_NameBanned",
|
||||
CharGenVerificationResponse.Code.Corrupt
|
||||
or CharGenVerificationResponse.Code.DatabaseDown => "ID_Character_Err_NameDBDown",
|
||||
CharGenVerificationResponse.Code.AdminPrivilegeDenied => "ID_Character_Err_NameAdminDenied",
|
||||
_ => null,
|
||||
_ => "ID_Character_Err_NameDBDown",
|
||||
};
|
||||
if (key is null)
|
||||
return;
|
||||
string? message = _bindings.ResolveText?.Invoke(key);
|
||||
if (message is null)
|
||||
return;
|
||||
|
||||
_dialogs.MakeMessage(message, data =>
|
||||
_errorMessageDialogContext = _dialogs.MakeMessage(message, data =>
|
||||
{
|
||||
_errorMessageDialogContext = 0u;
|
||||
_ = data;
|
||||
if (_disposed || _suppressDialogCallbacks)
|
||||
return;
|
||||
_bindings.AcknowledgeRejection?.Invoke();
|
||||
});
|
||||
}
|
||||
|
|
@ -1014,6 +1051,12 @@ internal sealed class CharacterCreationUiController : IDisposable
|
|||
_noNameWarningDialogContext = 0u;
|
||||
_dialogs.CloseDialog(closing);
|
||||
}
|
||||
if (_errorMessageDialogContext != 0u)
|
||||
{
|
||||
uint closing = _errorMessageDialogContext;
|
||||
_errorMessageDialogContext = 0u;
|
||||
_dialogs.CloseDialog(closing);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue