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
|
|
@ -56,17 +56,24 @@ internal sealed class CharacterCreationSummaryPage : IDisposable
|
|||
private const uint KeyTextId = 0x100002FCu;
|
||||
private const uint ValueTextId = 0x100002FDu;
|
||||
|
||||
/// <summary>Retail's <c>name[33]</c> buffer (32 usable chars + null
|
||||
/// terminator — <c>RuntimeCharacterCreationState.TrySetName</c>'s own
|
||||
/// already-established storage cap). The decompiled UI-side check at
|
||||
/// <c>ListenToElementMessage @ 0x0047bfd1</c> compares the raw input
|
||||
/// length against the literal <c>0x21</c> (33) — one more than this —
|
||||
/// but that comparison's exact base (visible character count vs. an
|
||||
/// internal length-prefix accounting the decompiler didn't resolve
|
||||
/// cleanly) is not fully certain from the pseudo-C. Using 32 here keeps
|
||||
/// the UI-level reject-and-revert threshold CONSISTENT with the
|
||||
/// already-reviewed storage cap rather than trusting an ambiguous
|
||||
/// 1-off decomp literal over that established contract.</summary>
|
||||
/// <summary>
|
||||
/// Retail's <c>name[33]</c> buffer (32 usable chars + null terminator —
|
||||
/// <c>RuntimeCharacterCreationState.TrySetName</c>'s own
|
||||
/// already-established storage cap). Review fix round F6 (2026-08-16):
|
||||
/// the decompiled UI-side check at <c>ListenToElementMessage @
|
||||
/// 0x0047bf40</c> (<c>~0x0047bfd1</c>) compares the field text's
|
||||
/// <c>m_charbuffer</c> LENGTH FIELD against the literal <c>0x21</c>
|
||||
/// (33) — that field is confirmed NUL-INCLUSIVE (the SAME method's own
|
||||
/// empty-field check earlier at <c>0x0047bf93</c> compares that field to
|
||||
/// <c>1</c>, i.e. an empty string's length reads as 1, not 0). So
|
||||
/// <c>length > 33</c> is EXACTLY <c>visibleChars > 32</c>: a
|
||||
/// 32-character name has length 33 (not <c>> 33</c>, accepted), a
|
||||
/// 33-character name has length 34 (<c>> 33</c>, rejected). This
|
||||
/// constant was ALWAYS byte-correct, not merely internally consistent
|
||||
/// with the storage cap it was originally justified against — the
|
||||
/// earlier "not fully certain" hedge and its AP-225 register row are
|
||||
/// both retired.
|
||||
/// </summary>
|
||||
private const int MaxNameLength = 32;
|
||||
|
||||
private readonly CharacterCreationRuntimeBindings _bindings;
|
||||
|
|
@ -75,7 +82,6 @@ internal sealed class CharacterCreationSummaryPage : IDisposable
|
|||
private readonly UiTemplateListBox? _list;
|
||||
private readonly UiField? _nameField;
|
||||
private string _lastCommittedName = string.Empty;
|
||||
private bool _suppressNextFieldEvent;
|
||||
private uint _nameTooLongDialogContext;
|
||||
private bool _disposed;
|
||||
|
||||
|
|
@ -92,13 +98,27 @@ internal sealed class CharacterCreationSummaryPage : IDisposable
|
|||
UiElement pageRoot,
|
||||
CharacterCreationRuntimeBindings bindings,
|
||||
RetailDialogFactory dialogs,
|
||||
string nameTooLongMessage)
|
||||
string nameTooLongMessage,
|
||||
Func<uint, uint, UiElement?> templateResolver)
|
||||
{
|
||||
_bindings = bindings;
|
||||
_dialogs = dialogs;
|
||||
_nameTooLongMessage = nameTooLongMessage;
|
||||
|
||||
_list = UiElement.FindDescendant(pageRoot, ListBoxId) as UiTemplateListBox;
|
||||
// Review fix round F3 residual (found while adding its own test,
|
||||
// 2026-08-16): this assignment was MISSING outright — every sibling
|
||||
// page that owns a UiTemplateListBox (CharacterCreationSkillsPage,
|
||||
// CharacterManagementUiController, every Options-panel controller)
|
||||
// wires TemplateResolver in its own constructor; this page never
|
||||
// did. Without it, ResolveTemplateRow's own `_list.TemplateResolver
|
||||
// is null` guard made EVERY RebuildListbox call a silent no-op —
|
||||
// the Summary listbox has never rendered a single row (Profession/
|
||||
// Gender/Heritage/Town, Attributes, Health/Stamina/Mana/Skill
|
||||
// Credits, or the skill buckets) since CC5 shipped, independent of
|
||||
// and masking the F3 template/score fix above.
|
||||
if (_list is not null)
|
||||
_list.TemplateResolver = templateResolver;
|
||||
|
||||
_nameField = UiElement.FindDescendant(pageRoot, NameTextId) as UiField;
|
||||
if (_nameField is not null)
|
||||
|
|
@ -137,10 +157,19 @@ internal sealed class CharacterCreationSummaryPage : IDisposable
|
|||
// Keep the field's displayed text in sync with the committed name
|
||||
// unless the player is actively typing (a mid-edit Refresh — driven
|
||||
// by an unrelated selection change elsewhere on the screen — must
|
||||
// not clobber their in-progress keystrokes).
|
||||
// not clobber their in-progress keystrokes). Review fix round F1
|
||||
// (2026-08-16): this used to arm a "_suppressNextFieldEvent" latch
|
||||
// before calling SetText, on the assumption that SetText raises the
|
||||
// same commit event a real keystroke/blur would. It does not —
|
||||
// UiField.SetText (UiField.cs:240-248) only mutates _text/_caret and
|
||||
// never invokes OnFocusLost/OnSubmit (those fire exclusively from
|
||||
// OnEvent's own idMessage dispatch, UiField.cs:~313-316/:729). The
|
||||
// latch therefore never had anything genuine to suppress; it just
|
||||
// sat armed until the PLAYER's own next real commit, which then hit
|
||||
// this early-return and silently dropped their typed name. Deleting
|
||||
// the latch outright (nothing to reproduce) fixes that bug.
|
||||
if (_nameField is { IsFocused: false } field && field.Text != snapshot.Name)
|
||||
{
|
||||
_suppressNextFieldEvent = true;
|
||||
field.SetText(snapshot.Name);
|
||||
_lastCommittedName = snapshot.Name;
|
||||
}
|
||||
|
|
@ -151,15 +180,35 @@ internal sealed class CharacterCreationSummaryPage : IDisposable
|
|||
|
||||
// ── Name field (ListenToElementMessage @ 0x0047bf40) ────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Review fix round F9 (2026-08-16), empty-name commit: retail's
|
||||
/// <c>ListenToElementMessage @ ~0x0047bf93</c> reads a length field that
|
||||
/// is NUL-INCLUSIVE (confirmed at F6/F2's own byte-decode — an empty
|
||||
/// field's length is 1, not 0) and gates the ENTIRE commit block —
|
||||
/// including <c>SetName</c> — behind <c>if (length != 1)</c>. Blurring
|
||||
/// an EMPTIED field in retail therefore leaves <c>CharGenState.name</c>
|
||||
/// UNCHANGED (whatever it held before), not cleared; <c>DoFinish</c>
|
||||
/// later reads that unchanged internal name, so retail's field and its
|
||||
/// internal state can legitimately show different things after an
|
||||
/// empty-field blur. This port deliberately does NOT reproduce that:
|
||||
/// it calls <see cref="CharacterCreationRuntimeBindings.SetName"/>
|
||||
/// (line below) for every commit including an empty one, so the state
|
||||
/// always agrees with what the field just showed. Verified this is a
|
||||
/// genuine, not cosmetic, choice — porting the exact skip would fight
|
||||
/// <see cref="Refresh"/>'s own field-sync block above (the F1 fix): the
|
||||
/// NEXT time anything else bumps the Runtime revision (e.g. the player
|
||||
/// returns to Attributes and changes a slider, then comes back), Refresh
|
||||
/// would see <c>field.Text ("") != snapshot.Name (the stale unchanged
|
||||
/// name)</c> and forcibly restore the OLD name into the field — a
|
||||
/// spontaneous, unexplained repopulation of a field the player
|
||||
/// deliberately emptied, which retail's own non-continuously-refreshed
|
||||
/// UI never produces. Register AP-227 records this as a deliberate
|
||||
/// divergence.
|
||||
/// </summary>
|
||||
private void CommitNameFromField(string text)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
if (_suppressNextFieldEvent)
|
||||
{
|
||||
_suppressNextFieldEvent = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (text.Length > MaxNameLength)
|
||||
{
|
||||
|
|
@ -236,8 +285,8 @@ internal sealed class CharacterCreationSummaryPage : IDisposable
|
|||
AddPair(pairTemplate, "Mana", a.Self);
|
||||
AddPair(pairTemplate, "Skill Credits", snapshot.RemainingSkillCredits);
|
||||
|
||||
AddSkillBucket(headerTemplate, lineTemplate, view, "Specialized Skills", ChargenSkillAdvancementClass.Specialized);
|
||||
AddSkillBucket(headerTemplate, lineTemplate, view, "Trained Skills", ChargenSkillAdvancementClass.Trained);
|
||||
AddSkillBucket(headerTemplate, pairTemplate, view, snapshot, "Specialized Skills", ChargenSkillAdvancementClass.Specialized);
|
||||
AddSkillBucket(headerTemplate, pairTemplate, view, snapshot, "Trained Skills", ChargenSkillAdvancementClass.Trained);
|
||||
}
|
||||
|
||||
private void AddLine(UiTemplateListEntry template, string text)
|
||||
|
|
@ -283,24 +332,33 @@ internal sealed class CharacterCreationSummaryPage : IDisposable
|
|||
return row is null ? null : UiElement.FindDescendant(row, childId) as UiText;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Review fix round F3 (2026-08-16), byte-decoded against
|
||||
/// <c>SetSummaryText @ ~0x0047b6be-0x0047b9e0</c>: retail adds each
|
||||
/// bucket's HEADER row UNCONDITIONALLY, before it ever walks
|
||||
/// <c>skillRecordList</c> for that bucket (an empty bucket still shows
|
||||
/// its header) — the previous lazy "only if any skill matched" gate had
|
||||
/// no decomp support. Each matching skill row uses template 2 (the
|
||||
/// key/value pair, <c>AddItemFromTemplateList(..., 2, ...)</c> @
|
||||
/// <c>0x0047b938</c>), not template 0's single line — KEY = the skill
|
||||
/// name, VALUE = <c>CharGenState::GetSkillScore(state, skill->id)</c> @
|
||||
/// <c>0x0047b923</c>, ported as <see cref="CharacterCreationRuntimeBindings.GetSkillScore"/>.
|
||||
/// </summary>
|
||||
private void AddSkillBucket(
|
||||
UiTemplateListEntry headerTemplate,
|
||||
UiTemplateListEntry lineTemplate,
|
||||
UiTemplateListEntry pairTemplate,
|
||||
IRuntimeCharacterCreationView view,
|
||||
RuntimeCharacterCreationSnapshot snapshot,
|
||||
string header,
|
||||
ChargenSkillAdvancementClass targetClass)
|
||||
{
|
||||
bool any = false;
|
||||
AddHeader(headerTemplate, header);
|
||||
for (uint skillId = 1; skillId < ChargenSkillAdvancementSet.SlotCount; skillId++)
|
||||
{
|
||||
if (view.GetSkillLevel(skillId) != targetClass)
|
||||
continue;
|
||||
if (!any)
|
||||
{
|
||||
AddHeader(headerTemplate, header);
|
||||
any = true;
|
||||
}
|
||||
AddLine(lineTemplate, ItemAppraisalTextFormatter.SkillName((int)skillId));
|
||||
uint score = _bindings.GetSkillScore?.Invoke(skillId, snapshot.Attributes, targetClass) ?? 0u;
|
||||
AddPair(pairTemplate, ItemAppraisalTextFormatter.SkillName((int)skillId), (int)score);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -364,6 +422,8 @@ internal sealed class CharacterCreationSummaryPage : IDisposable
|
|||
_nameTooLongDialogContext = 0u;
|
||||
_dialogs.CloseDialog(closing);
|
||||
}
|
||||
if (_list is not null)
|
||||
_list.TemplateResolver = null;
|
||||
_list?.Flush();
|
||||
// PreviewControl is owned by the composition root (disposed with
|
||||
// the leased ChargenPreviewRenderer) — just drop the reference.
|
||||
|
|
|
|||
|
|
@ -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