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>
1023 lines
42 KiB
C#
1023 lines
42 KiB
C#
using System.Numerics;
|
|
using AcDream.Core.CharGen;
|
|
using AcDream.Core.Net.Messages;
|
|
using AcDream.Runtime;
|
|
using AcDream.Runtime.Session;
|
|
|
|
namespace AcDream.App.UI.Layout;
|
|
|
|
/// <summary>
|
|
/// Bindings the retail character-creation screen (<c>gmCharGenMainUI</c>)
|
|
/// needs beyond the borrowed view: generation-capturing command wrappers,
|
|
/// mirroring <see cref="CharacterSelectionRuntimeBindings"/>'s shape exactly.
|
|
/// Every <c>Func</c> here is a late-bound seam (Campaign CC — see
|
|
/// <c>feedback_resolve_deferred_funcs_per_call.md</c>): callers MUST resolve
|
|
/// it per-call, never capture the delegate once at mount time.
|
|
/// </summary>
|
|
/// <param name="OpenOnStart">Campaign CC slice CC4: the retail
|
|
/// transition is Create Character (<c>0x100003A0</c>) →
|
|
/// <c>QueueUIMode(0x1000000b)</c>, but that button stays ghosted until CC7's
|
|
/// closing move. This flag is the interim env/test-only open seam
|
|
/// (<c>ACDREAM_OPEN_CHARGEN=1</c> → <see cref="AcDream.App.RuntimeOptions.OpenCharacterCreationOnStart"/>)
|
|
/// so the screen can be exercised before the real button is wired.</param>
|
|
public sealed record CharacterCreationRuntimeBindings(
|
|
Func<IRuntimeCharacterCreationView?> View,
|
|
Func<uint, RuntimeCommandResult> SelectHeritage,
|
|
Func<uint, RuntimeCommandResult> SelectGender,
|
|
Func<uint, RuntimeCommandResult> SelectTemplate,
|
|
Func<ChargenAttributeId, int, RuntimeCommandResult> SetAttribute,
|
|
Func<ChargenAttributeId, bool, RuntimeCommandResult> SetAttributeLock,
|
|
Func<uint, RuntimeCommandResult> TrainSkill,
|
|
Func<uint, RuntimeCommandResult> SpecializeSkill,
|
|
Func<uint, RuntimeCommandResult> UntrainSkill,
|
|
Func<int, RuntimeCommandResult> SelectStartArea,
|
|
Func<bool, RuntimeCommandResult> Finish,
|
|
Action RequestExit,
|
|
/// <summary>Campaign CC slice CC6b-MOUNT: the Appearance page's nine
|
|
/// spin controls and nine color swatches.</summary>
|
|
Func<ChargenAppearanceSlot, uint, RuntimeCommandResult>? SetAppearanceIndex = null,
|
|
/// <summary>CC6b-MOUNT: the Appearance page's shade scrollbar.</summary>
|
|
Func<ChargenShadeSlot, double, RuntimeCommandResult>? SetShade = null,
|
|
/// <summary>DAT string lookup (table <c>0x23000002</c>, the SAME table
|
|
/// every other <c>ID_CharGen_*</c>/<c>ID_Character*</c> key resolves
|
|
/// through) — used by the Heritage page's composed description text.
|
|
/// <see langword="null"/> degrades to the heritage's own DAT
|
|
/// <c>Name</c> field instead of the full composed copy.</summary>
|
|
Func<string, string?>? ResolveText = null,
|
|
/// <summary>Campaign CC slice CC5: the Summary page's name field
|
|
/// commit (<c>gmCGSummaryPage::ListenToElementMessage</c>'s
|
|
/// <c>CharGenState::SetName</c> call).</summary>
|
|
Func<string, RuntimeCommandResult>? SetName = null,
|
|
/// <summary>CC5: dismisses a surfaced <c>0xF643</c> rejection after its
|
|
/// dialog closes (<c>RuntimeCharacterCreationState.TryAcknowledgeRejection</c>).</summary>
|
|
Func<RuntimeCommandResult>? AcknowledgeRejection = null,
|
|
/// <summary>CC5: the screen-open roll
|
|
/// (<c>gmCharGenMainUI</c>'s ctor-time <c>RandomizeCharacter</c> call)
|
|
/// and the Summary page's Random button.</summary>
|
|
Func<RuntimeCommandResult>? RandomizeCharacter = null,
|
|
/// <summary>CC5: the Appearance page's Random button on its Face
|
|
/// sub-tab.</summary>
|
|
Func<RuntimeCommandResult>? RandomizeAppearance = null,
|
|
/// <summary>CC5: the Appearance page's Random button on its Clothes
|
|
/// sub-tab.</summary>
|
|
Func<RuntimeCommandResult>? RandomizeClothing = null,
|
|
bool OpenOnStart = false);
|
|
|
|
/// <summary>
|
|
/// Projects Runtime's borrowed <see cref="IRuntimeCharacterCreationView"/>
|
|
/// through retail <c>gmCharGenMainUI</c>'s authored retained layout — the
|
|
/// mount + master shell (progress bar, tab strip, Back/Next/Finish/Help/
|
|
/// Exit/Random nav) plus the Heritage/Profession/Skills/Town pages this
|
|
/// slice builds. Fix round F6: the Appearance (<c>0x100003d4</c>) page root
|
|
/// is fully LIVE as of CC6b-MOUNT (<see cref="CharacterCreationAppearancePage"/>);
|
|
/// only the Summary (<c>0x100003d6</c>) page root remains mounted but
|
|
/// content-inert — CC5 fills it (register TS-82, narrowed to Summary-only
|
|
/// at CC6b-MOUNT).
|
|
///
|
|
/// <para>
|
|
/// Decomp anchors: root construction + child resolution
|
|
/// <c>gmCharGenMainUI::gmCharGenMainUI @ 0x004e7eb0</c> (root element
|
|
/// <c>0x100003cc</c> from enum <c>0x10000039</c>); page switching
|
|
/// <c>gmCharGenMainUI::SetProgressState @ 0x004e7a10</c> (the Olthoi
|
|
/// tab-hiding + redirect logic); nav dispatch
|
|
/// <c>gmCharGenMainUI::ListenToElementMessage @ 0x004e9450</c>; exit
|
|
/// confirmation <c>gmCharGenMainUI::DoExit @ 0x004e8650</c>; randomize
|
|
/// dispatch <c>gmCharGenMainUI::DoRandom @ 0x004e7d70</c>.
|
|
/// </para>
|
|
/// </summary>
|
|
internal sealed class CharacterCreationUiController : IDisposable
|
|
{
|
|
internal const uint RootEnum = 0x10000039u;
|
|
internal const uint RootElementId = 0x100003CCu;
|
|
internal const uint ProgressBarElementId = 0x100003CEu;
|
|
internal const uint BackElementId = 0x100003C6u;
|
|
internal const uint NextElementId = 0x100003C7u;
|
|
internal const uint FinishElementId = 0x100003C8u;
|
|
internal const uint HelpElementId = 0x100003C9u;
|
|
internal const uint ExitElementId = 0x100003CAu;
|
|
internal const uint RandomElementId = 0x100003CBu;
|
|
internal const uint MasterPageElementId = 0x100003D0u;
|
|
internal const uint HeritagePageElementId = 0x100003D1u;
|
|
internal const uint ProfessionPageElementId = 0x100003D2u;
|
|
internal const uint SkillsPageElementId = 0x100003D3u;
|
|
internal const uint AppearancePageElementId = 0x100003D4u;
|
|
internal const uint TownPageElementId = 0x100003D5u;
|
|
internal const uint SummaryPageElementId = 0x100003D6u;
|
|
internal const uint HeritageTabElementId = 0x100003EFu;
|
|
internal const uint ProfessionTabElementId = 0x100003F0u;
|
|
internal const uint SkillsTabElementId = 0x100003F1u;
|
|
internal const uint AppearanceTabElementId = 0x100003F2u;
|
|
internal const uint TownTabElementId = 0x100003F3u;
|
|
internal const uint SummaryTabElementId = 0x100003F4u;
|
|
|
|
/// <summary>Retail's <c>gmCharGenMainUI::ECGProgress</c> enum values —
|
|
/// used verbatim as the master page's per-page state ids
|
|
/// (<c>0x10000025 + (page - 1)</c>) and the tab-hide/redirect math in
|
|
/// <see cref="ApplyProgressState"/>.</summary>
|
|
internal enum Page
|
|
{
|
|
Heritage = 1,
|
|
Profession = 2,
|
|
Skills = 3,
|
|
Appearance = 4,
|
|
Town = 5,
|
|
Summary = 6,
|
|
}
|
|
|
|
internal sealed record DialogStrings(
|
|
string ExitWarning,
|
|
/// <summary>Campaign CC slice CC5: <c>ID_CharGen_NoNameWarning</c> —
|
|
/// <c>DoFinish</c>'s empty-name refusal dialog.</summary>
|
|
string NoNameWarning,
|
|
/// <summary>CC5: <c>ID_CharGen_CreditWarning</c> —
|
|
/// <c>MakeCreditWarningDialog</c>'s unspent-attribute-credits
|
|
/// confirmation.</summary>
|
|
string CreditWarning,
|
|
/// <summary>CC5: <c>ID_CharGen_RandomizeWarning</c> —
|
|
/// <c>MakeRandomizeWarningDialog</c>'s Summary-page Random
|
|
/// confirmation.</summary>
|
|
string RandomizeWarning,
|
|
/// <summary>CC5: <c>ID_CharGen_NameTooLong</c> —
|
|
/// <c>gmCGSummaryPage::DoNameLimitDialog</c>'s name-field-too-long
|
|
/// notice.</summary>
|
|
string NameTooLong);
|
|
|
|
private readonly UiRoot _host;
|
|
private readonly ImportedLayout _layout;
|
|
private readonly UiElement _progressBar;
|
|
private readonly UiButton _back;
|
|
private readonly UiButton _next;
|
|
private readonly UiButton _finish;
|
|
private readonly UiButton _help;
|
|
private readonly UiButton _exit;
|
|
private readonly UiButton _random;
|
|
private readonly UiElement _masterPage;
|
|
private readonly UiElement _heritagePageRoot;
|
|
private readonly UiElement _professionPageRoot;
|
|
private readonly UiElement _skillsPageRoot;
|
|
private readonly UiElement _appearancePageRoot;
|
|
private readonly UiElement _townPageRoot;
|
|
private readonly UiElement _summaryPageRoot;
|
|
private readonly UiButton _heritageTab;
|
|
private readonly UiButton _professionTab;
|
|
private readonly UiButton _skillsTab;
|
|
private readonly UiButton _appearanceTab;
|
|
private readonly UiButton _townTab;
|
|
private readonly UiButton _summaryTab;
|
|
private readonly RetailDialogFactory _dialogs;
|
|
private readonly CharacterCreationRuntimeBindings _bindings;
|
|
private readonly DialogStrings _strings;
|
|
private readonly CharacterCreationHeritagePage _heritagePage;
|
|
private readonly CharacterCreationProfessionPage _professionPage;
|
|
private readonly CharacterCreationSkillsPage _skillsPage;
|
|
private readonly CharacterCreationTownPage _townPage;
|
|
private readonly CharacterCreationAppearancePage _appearancePage;
|
|
private readonly CharacterCreationSummaryPage _summaryPage;
|
|
|
|
private Vector2 _authoredCanvas;
|
|
private RuntimeGenerationToken _lastGeneration;
|
|
private long _lastRevision = long.MinValue;
|
|
private Page _currentPage = Page.Heritage;
|
|
private bool _active;
|
|
private bool _isOpen;
|
|
private bool _openOnStartConsumed;
|
|
private uint _exitDialogContext;
|
|
// Campaign CC slice CC5: gmCharGenMainUI's own m_uiCreditWarningContext/
|
|
// m_uiRandomizeWarningContext (0x004e8870/0x004e8a90) — same
|
|
// one-outstanding-dialog-at-a-time guard shape as _exitDialogContext.
|
|
private uint _creditWarningDialogContext;
|
|
private uint _randomizeWarningDialogContext;
|
|
private uint _noNameWarningDialogContext;
|
|
private RuntimeCharacterCreationRejection? _lastShownRejection;
|
|
private bool _suppressDialogCallbacks;
|
|
private bool _disposed;
|
|
|
|
private CharacterCreationUiController(
|
|
UiRoot host,
|
|
ImportedLayout layout,
|
|
UiElement progressBar,
|
|
UiButton back,
|
|
UiButton next,
|
|
UiButton finish,
|
|
UiButton help,
|
|
UiButton exit,
|
|
UiButton random,
|
|
UiElement masterPage,
|
|
UiElement heritagePageRoot,
|
|
UiElement professionPageRoot,
|
|
UiElement skillsPageRoot,
|
|
UiElement appearancePageRoot,
|
|
UiElement townPageRoot,
|
|
UiElement summaryPageRoot,
|
|
UiButton heritageTab,
|
|
UiButton professionTab,
|
|
UiButton skillsTab,
|
|
UiButton appearanceTab,
|
|
UiButton townTab,
|
|
UiButton summaryTab,
|
|
Func<uint, uint, UiElement?> templateResolver,
|
|
RetailDialogFactory dialogs,
|
|
CharacterCreationRuntimeBindings bindings,
|
|
DialogStrings strings)
|
|
{
|
|
_host = host;
|
|
_layout = layout;
|
|
_progressBar = progressBar;
|
|
_back = back;
|
|
_next = next;
|
|
_finish = finish;
|
|
_help = help;
|
|
_exit = exit;
|
|
_random = random;
|
|
_masterPage = masterPage;
|
|
_heritagePageRoot = heritagePageRoot;
|
|
_professionPageRoot = professionPageRoot;
|
|
_skillsPageRoot = skillsPageRoot;
|
|
_appearancePageRoot = appearancePageRoot;
|
|
_townPageRoot = townPageRoot;
|
|
_summaryPageRoot = summaryPageRoot;
|
|
_heritageTab = heritageTab;
|
|
_professionTab = professionTab;
|
|
_skillsTab = skillsTab;
|
|
_appearanceTab = appearanceTab;
|
|
_townTab = townTab;
|
|
_summaryTab = summaryTab;
|
|
_dialogs = dialogs;
|
|
_bindings = bindings;
|
|
_strings = strings;
|
|
|
|
Root.Left = 0f;
|
|
Root.Top = 0f;
|
|
Root.ClickThrough = false;
|
|
Root.Visible = false;
|
|
// AD-98: the same authored 800x600 fixed-canvas treatment as the
|
|
// character-management screen. Both controllers DECLARE/REVOKE
|
|
// through UiRoot's owner-scoped arbiter (review fix round R1,
|
|
// 2026-08-15) rather than writing UiRoot.FixedCanvasSize directly —
|
|
// char-management can be simultaneously active underneath this
|
|
// screen, and a raw write from either controller is a last-writer-
|
|
// wins race with no owner (the F1 fix's own Close() null wiped
|
|
// char-management's still-active canvas out from under it). See
|
|
// Open/Close/Deactivate/Dispose below for the matching declare/
|
|
// revoke pair.
|
|
_authoredCanvas = new Vector2(
|
|
Root.Width > 0f ? Root.Width : 800f,
|
|
Root.Height > 0f ? Root.Height : 600f);
|
|
|
|
_heritagePage = new CharacterCreationHeritagePage(heritagePageRoot, bindings, ApplyHeritageTabRestore);
|
|
_professionPage = new CharacterCreationProfessionPage(professionPageRoot, bindings);
|
|
_skillsPage = new CharacterCreationSkillsPage(skillsPageRoot, bindings, templateResolver);
|
|
_townPage = new CharacterCreationTownPage(townPageRoot, bindings);
|
|
_appearancePage = new CharacterCreationAppearancePage(appearancePageRoot, bindings);
|
|
_summaryPage = new CharacterCreationSummaryPage(
|
|
summaryPageRoot, bindings, dialogs, strings.NameTooLong);
|
|
|
|
// gmCharGenMainUI::ListenToElementMessage @ 0x004e9450.
|
|
_back.OnClick = OnBack;
|
|
_next.OnClick = OnNext;
|
|
// Finish (0x100003c8): retail enables it on Summary only
|
|
// (ListenToElementMessage's case 0x100003c8 no-ops unless
|
|
// m_eProgressState == ECG_SUMMARY @ 0x004e956f) — ApplyProgressState
|
|
// gates _finish.Enabled the same way. OnFinish itself re-checks the
|
|
// current page defensively (mirroring that same retail guard).
|
|
_finish.OnClick = OnFinish;
|
|
// Help (0x100003c9) is not handled in gmCharGenMainUI's own
|
|
// ListenToElementMessage switch (case 0x100003c9 falls straight
|
|
// through to the base UIFramework handler) — retail has no custom
|
|
// help action here either; leave it a no-op.
|
|
_help.OnClick = null;
|
|
_exit.OnClick = OnExit;
|
|
_random.OnClick = OnRandom;
|
|
_heritageTab.OnClick = () => ApplyProgressState(Page.Heritage);
|
|
_professionTab.OnClick = () => ApplyProgressState(Page.Profession);
|
|
_skillsTab.OnClick = () => ApplyProgressState(Page.Skills);
|
|
_appearanceTab.OnClick = () => ApplyProgressState(Page.Appearance);
|
|
_townTab.OnClick = () => ApplyProgressState(Page.Town);
|
|
_summaryTab.OnClick = () => ApplyProgressState(Page.Summary);
|
|
}
|
|
|
|
internal UiElement Root => _layout.Root;
|
|
|
|
/// <summary>The authored Appearance-page viewport (<c>0x100003bb</c>) —
|
|
/// CC6b-MOUNT's composition root assigns its <c>Renderer</c> once the
|
|
/// graphics backend exists (mirrors the paperdoll's own late
|
|
/// <c>viewport.Renderer = ...</c> assignment).</summary>
|
|
internal UiViewport? AppearanceViewport => _appearancePage.Viewport;
|
|
|
|
/// <summary>CC6b-MOUNT: the late-bound zoom/rotate control surface —
|
|
/// see <see cref="AcDream.App.Rendering.IChargenPreviewControl"/>'s own
|
|
/// doc comment for why this is assigned after construction rather than
|
|
/// threaded through the ctor.</summary>
|
|
internal AcDream.App.Rendering.IChargenPreviewControl? AppearancePreviewControl
|
|
{
|
|
get => _appearancePage.PreviewControl;
|
|
set => _appearancePage.PreviewControl = value;
|
|
}
|
|
|
|
/// <summary>Gates the Appearance preview's per-frame work on whether
|
|
/// that specific page — AND the whole chargen screen — is the one
|
|
/// currently showing. <c>Close()</c> only ever hides <see cref="Root"/>,
|
|
/// not the individual page roots, so a page-root-only check would stay
|
|
/// true after the screen closes on the Appearance page. Mirrors the
|
|
/// paperdoll's own outer-inventory-frame gate.</summary>
|
|
internal bool IsAppearancePageVisible => Root.Visible && _appearancePageRoot.Visible;
|
|
|
|
/// <summary>Campaign CC slice CC5: the authored Summary-page viewport
|
|
/// (<c>0x10000406</c>) — its OWN <c>gmCG3DView</c> instance, distinct
|
|
/// from the Appearance page's (see this class's own class doc on the
|
|
/// decomp citation).</summary>
|
|
internal UiViewport? SummaryViewport => _summaryPage.Viewport;
|
|
|
|
/// <summary>CC5: the Summary preview's late-bound control surface. No
|
|
/// zoom/rotate buttons bind against it — see
|
|
/// <see cref="AcDream.App.Rendering.RetailSummaryPreviewPageVisibility"/>'s
|
|
/// doc comment.</summary>
|
|
internal AcDream.App.Rendering.IChargenPreviewControl? SummaryPreviewControl
|
|
{
|
|
get => _summaryPage.PreviewControl;
|
|
set => _summaryPage.PreviewControl = value;
|
|
}
|
|
|
|
/// <summary>CC5: same shape as <see cref="IsAppearancePageVisible"/>,
|
|
/// for the Summary page.</summary>
|
|
internal bool IsSummaryPageVisible => Root.Visible && _summaryPageRoot.Visible;
|
|
|
|
internal static CharacterCreationUiController? CreateDetached(
|
|
UiRoot host,
|
|
ImportedLayout layout,
|
|
Func<uint, uint, UiElement?> templateResolver,
|
|
RetailDialogFactory dialogs,
|
|
CharacterCreationRuntimeBindings bindings,
|
|
DialogStrings strings)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(host);
|
|
ArgumentNullException.ThrowIfNull(layout);
|
|
ArgumentNullException.ThrowIfNull(templateResolver);
|
|
ArgumentNullException.ThrowIfNull(dialogs);
|
|
ArgumentNullException.ThrowIfNull(bindings);
|
|
ArgumentNullException.ThrowIfNull(strings);
|
|
|
|
if (layout.Root.DatElementId != RootElementId
|
|
|| layout.FindElement(ProgressBarElementId) is not { } progressBar
|
|
|| layout.FindElement(BackElementId) is not UiButton back
|
|
|| layout.FindElement(NextElementId) is not UiButton next
|
|
|| layout.FindElement(FinishElementId) is not UiButton finish
|
|
|| layout.FindElement(HelpElementId) is not UiButton help
|
|
|| layout.FindElement(ExitElementId) is not UiButton exit
|
|
|| layout.FindElement(RandomElementId) is not UiButton random
|
|
|| layout.FindElement(MasterPageElementId) is not { } masterPage
|
|
|| layout.FindElement(HeritagePageElementId) is not { } heritagePageRoot
|
|
|| layout.FindElement(ProfessionPageElementId) is not { } professionPageRoot
|
|
|| layout.FindElement(SkillsPageElementId) is not { } skillsPageRoot
|
|
|| layout.FindElement(AppearancePageElementId) is not { } appearancePageRoot
|
|
|| layout.FindElement(TownPageElementId) is not { } townPageRoot
|
|
|| layout.FindElement(SummaryPageElementId) is not { } summaryPageRoot
|
|
|| layout.FindElement(HeritageTabElementId) is not UiButton heritageTab
|
|
|| layout.FindElement(ProfessionTabElementId) is not UiButton professionTab
|
|
|| layout.FindElement(SkillsTabElementId) is not UiButton skillsTab
|
|
|| layout.FindElement(AppearanceTabElementId) is not UiButton appearanceTab
|
|
|| layout.FindElement(TownTabElementId) is not UiButton townTab
|
|
|| layout.FindElement(SummaryTabElementId) is not UiButton summaryTab)
|
|
{
|
|
Console.WriteLine(
|
|
"[UI] character creation: the authored root/master-shell contract is incomplete.");
|
|
return null;
|
|
}
|
|
|
|
return new CharacterCreationUiController(
|
|
host,
|
|
layout,
|
|
progressBar,
|
|
back,
|
|
next,
|
|
finish,
|
|
help,
|
|
exit,
|
|
random,
|
|
masterPage,
|
|
heritagePageRoot,
|
|
professionPageRoot,
|
|
skillsPageRoot,
|
|
appearancePageRoot,
|
|
townPageRoot,
|
|
summaryPageRoot,
|
|
heritageTab,
|
|
professionTab,
|
|
skillsTab,
|
|
appearanceTab,
|
|
townTab,
|
|
summaryTab,
|
|
templateResolver,
|
|
dialogs,
|
|
bindings,
|
|
strings);
|
|
}
|
|
|
|
internal void AttachAndTick()
|
|
{
|
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
|
if (Root.Parent is null)
|
|
_host.AddChild(Root);
|
|
Tick();
|
|
}
|
|
|
|
internal void Tick()
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
|
|
IRuntimeCharacterCreationView? view = _bindings.View();
|
|
RuntimeCharacterCreationSnapshot snapshot = view?.Snapshot ?? default;
|
|
if (view is null || !snapshot.IsActive)
|
|
{
|
|
Deactivate();
|
|
_lastGeneration = snapshot.Generation;
|
|
_lastRevision = snapshot.Revision;
|
|
return;
|
|
}
|
|
|
|
if (!_active)
|
|
{
|
|
_active = true;
|
|
// CC4 interim open seam (ACDREAM_OPEN_CHARGEN=1) — the real
|
|
// Create-button transition is CC7's. Fires once per mount.
|
|
if (_bindings.OpenOnStart && !_openOnStartConsumed)
|
|
{
|
|
_openOnStartConsumed = true;
|
|
Open();
|
|
}
|
|
}
|
|
|
|
if (_isOpen)
|
|
{
|
|
Root.Visible = true;
|
|
_host.BringToFront(Root);
|
|
}
|
|
else
|
|
{
|
|
Root.Visible = false;
|
|
}
|
|
|
|
if (_lastGeneration != snapshot.Generation
|
|
|| _lastRevision != snapshot.Revision)
|
|
{
|
|
_heritagePage.Refresh(view, snapshot);
|
|
_professionPage.Refresh(view, snapshot);
|
|
_skillsPage.Refresh(view, snapshot);
|
|
_townPage.Refresh(view, snapshot);
|
|
_appearancePage.Refresh(view, snapshot);
|
|
_summaryPage.Refresh(view, snapshot);
|
|
_lastGeneration = snapshot.Generation;
|
|
_lastRevision = snapshot.Revision;
|
|
}
|
|
|
|
ReconcileDialogs(snapshot);
|
|
}
|
|
|
|
/// <summary>Opens the screen at retail's authored default page
|
|
/// (<c>gmCharGenMainUI::gmCharGenMainUI</c>'s trailing
|
|
/// <c>SetProgressState(this, ECG_HERTAGE)</c>). Declares the fixed
|
|
/// canvas on this exact activation edge through <see cref="UiRoot"/>'s
|
|
/// arbiter — matching <see cref="CharacterManagementUiController"/>'s
|
|
/// own one-shot declare — not per-tick; <see cref="Close"/>/
|
|
/// <see cref="Deactivate"/>/<see cref="Dispose"/> revoke it back out
|
|
/// symmetrically, and the canvas stays set for as long as ANY other
|
|
/// declarer (e.g. character-management underneath) remains active.</summary>
|
|
internal void Open()
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
_isOpen = true;
|
|
_host.DeclareFixedCanvas(this, _authoredCanvas);
|
|
RollOpeningCharacter();
|
|
ApplyProgressState(Page.Heritage);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Campaign CC slice CC5: ports <c>gmCharGenMainUI</c>'s ctor-time roll
|
|
/// (<c>~0x004e81f5-0x004e8218</c>) — <c>CharGenState::RandomizeCharacter
|
|
/// (state, hasToD) @ 0x005c6d80</c> runs BEFORE any page constructs,
|
|
/// retiring AP-214's honest-blank deviation (retail's chargen screen is
|
|
/// never actually blank on open). Then reproduces
|
|
/// <c>gmCGAppearancePage::InitializePage</c>'s own gender-read-then-FLIP
|
|
/// (<c>~0x004802da-0x00480303</c>, decomp-confirmed:
|
|
/// <c>mGender==1 -> SetGender(2)</c>, <c>mGender==2 -> SetGender(1)</c>) —
|
|
/// a genuine, always-firing retail quirk that runs immediately AFTER
|
|
/// <c>RandomizeCharacter</c> already assigned a real (non-zero) gender.
|
|
/// Retail's whole UI tree (every page, including Appearance) is
|
|
/// reconstructed fresh each time the chargen screen opens, so the flip
|
|
/// fires once per visit there; acdream's pages are built once at mount
|
|
/// time and only toggle visibility, so <see cref="Open"/> — the closest
|
|
/// analogue to "runs once per screen-open" this architecture has — is
|
|
/// where both the roll and the flip belong.
|
|
/// </summary>
|
|
private void RollOpeningCharacter()
|
|
{
|
|
if (_bindings.RandomizeCharacter?.Invoke().Status != RuntimeCommandStatus.Accepted)
|
|
return;
|
|
|
|
uint gender = _bindings.View()?.Snapshot.GenderKey ?? 0u;
|
|
if (gender == 1u)
|
|
_bindings.SelectGender(2u);
|
|
else if (gender == 2u)
|
|
_bindings.SelectGender(1u);
|
|
}
|
|
|
|
private void Close()
|
|
{
|
|
if (!_isOpen)
|
|
return;
|
|
_isOpen = false;
|
|
Root.Visible = false;
|
|
_host.RevokeFixedCanvas(this);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
_disposed = true;
|
|
try
|
|
{
|
|
CloseAllDialogs(suppressCallbacks: true);
|
|
}
|
|
finally
|
|
{
|
|
// Matches CharacterManagementUiController.Dispose's own
|
|
// unconditional revoke — defends against disposing while
|
|
// _isOpen (Close() is not otherwise called on this path). Idle
|
|
// if Close() already revoked (RevokeFixedCanvas is a no-op for
|
|
// an owner that already revoked).
|
|
_host.RevokeFixedCanvas(this);
|
|
_back.OnClick = null;
|
|
_next.OnClick = null;
|
|
_finish.OnClick = null;
|
|
_help.OnClick = null;
|
|
_exit.OnClick = null;
|
|
_random.OnClick = null;
|
|
_heritageTab.OnClick = null;
|
|
_professionTab.OnClick = null;
|
|
_skillsTab.OnClick = null;
|
|
_appearanceTab.OnClick = null;
|
|
_townTab.OnClick = null;
|
|
_summaryTab.OnClick = null;
|
|
_heritagePage.Dispose();
|
|
_professionPage.Dispose();
|
|
_skillsPage.Dispose();
|
|
_townPage.Dispose();
|
|
_appearancePage.Dispose();
|
|
_summaryPage.Dispose();
|
|
_host.RemoveChild(Root);
|
|
}
|
|
}
|
|
|
|
// ── Nav dispatch (gmCharGenMainUI::ListenToElementMessage @ 0x004e9450) ──
|
|
|
|
private void OnBack()
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
if (_currentPage <= Page.Heritage)
|
|
{
|
|
OnExit();
|
|
return;
|
|
}
|
|
ApplyProgressState(_currentPage - 1);
|
|
}
|
|
|
|
private void OnNext()
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
if (_currentPage < Page.Summary)
|
|
ApplyProgressState(_currentPage + 1);
|
|
}
|
|
|
|
private void OnExit()
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
// gmCharGenMainUI::DoExit @ 0x004e8650's own guard: a second Exit
|
|
// click while the confirmation is already open is a no-op.
|
|
if (_exitDialogContext != 0u)
|
|
return;
|
|
|
|
_exitDialogContext = _dialogs.MakeConfirmation(
|
|
_strings.ExitWarning,
|
|
data =>
|
|
{
|
|
_exitDialogContext = 0u;
|
|
if (_disposed || _suppressDialogCallbacks)
|
|
return;
|
|
|
|
// RecvNotice_CloseDialog @ 0x004e9780's exit-context branch:
|
|
// confirm -> QueueUIMode(0x1000000a) (leave chargen). Our
|
|
// equivalent is closing this screen; whatever mounted the
|
|
// character-management screen already keeps re-drawing it
|
|
// underneath (this screen only BringToFront's itself while
|
|
// open — see Tick).
|
|
if (data.GetBoolean(RetailDialogProperty.ConfirmationResult))
|
|
{
|
|
Close();
|
|
_bindings.RequestExit();
|
|
}
|
|
});
|
|
}
|
|
|
|
private void OnRandom()
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
|
|
// gmCharGenMainUI::DoRandom @ 0x004e7d70. Heritage/Profession/Town
|
|
// still use the AP-212 uniform-pick approximation (unchanged this
|
|
// slice); Appearance now delegates to the page's own real
|
|
// RandomizeAppearance/RandomizeClothing primitives (CC5); Skills'
|
|
// CharGenState::RandomizeSkills remains unported (AP-212, narrowed) —
|
|
// _random.Enabled already keeps the control ghosted there
|
|
// (ApplyProgressState). Summary goes through
|
|
// gmCharGenMainUI::MakeRandomizeWarningDialog @ 0x004e8a90 first —
|
|
// that dialog + its confirm-triggered RandomizeCharacter call are
|
|
// gmCharGenMainUI's OWN methods in retail (not gmCGSummaryPage's),
|
|
// so they live here on the master controller.
|
|
IRuntimeCharacterCreationView? view = _bindings.View();
|
|
if (view is null)
|
|
return;
|
|
RuntimeCharacterCreationSnapshot snapshot = view.Snapshot;
|
|
|
|
switch (_currentPage)
|
|
{
|
|
case Page.Heritage:
|
|
_heritagePage.Randomize(snapshot);
|
|
break;
|
|
case Page.Profession:
|
|
_professionPage.Randomize(snapshot);
|
|
break;
|
|
case Page.Appearance:
|
|
_appearancePage.Randomize();
|
|
break;
|
|
case Page.Town:
|
|
_townPage.Randomize(view);
|
|
break;
|
|
case Page.Summary:
|
|
ShowRandomizeWarningDialog();
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>Ports <c>gmCharGenMainUI::MakeRandomizeWarningDialog @
|
|
/// 0x004e8a90</c> (<c>ID_CharGen_RandomizeWarning</c>) +
|
|
/// <c>CloseRandomizeWarningDialog @ 0x004e8400</c>'s own confirm arm
|
|
/// (<c>arg2 != 0 -> DoRandom(this)</c>, which on THIS second call
|
|
/// takes <c>DoRandom</c>'s Summary case directly — no re-entrant
|
|
/// warning, since the gate lives in the button-click dispatcher above,
|
|
/// not inside <c>DoRandom</c> itself).</summary>
|
|
private void ShowRandomizeWarningDialog()
|
|
{
|
|
// MakeRandomizeWarningDialog's own guard: a second click while the
|
|
// dialog is already open is a no-op.
|
|
if (_randomizeWarningDialogContext != 0u)
|
|
return;
|
|
|
|
_randomizeWarningDialogContext = _dialogs.MakeConfirmation(
|
|
_strings.RandomizeWarning,
|
|
data =>
|
|
{
|
|
_randomizeWarningDialogContext = 0u;
|
|
if (_disposed || _suppressDialogCallbacks)
|
|
return;
|
|
if (data.GetBoolean(RetailDialogProperty.ConfirmationResult))
|
|
_bindings.RandomizeCharacter?.Invoke();
|
|
});
|
|
}
|
|
|
|
// ── Page switching (gmCharGenMainUI::SetProgressState @ 0x004e7a10) ────
|
|
|
|
private void ApplyProgressState(Page target)
|
|
{
|
|
_heritagePageRoot.Visible = false;
|
|
_professionPageRoot.Visible = false;
|
|
_skillsPageRoot.Visible = false;
|
|
_appearancePageRoot.Visible = false;
|
|
_townPageRoot.Visible = false;
|
|
_summaryPageRoot.Visible = false;
|
|
_next.Visible = true;
|
|
_finish.Visible = false;
|
|
|
|
Page previous = _currentPage;
|
|
_currentPage = target;
|
|
_heritageTab.Selected = false;
|
|
_professionTab.Selected = false;
|
|
_skillsTab.Selected = false;
|
|
_appearanceTab.Selected = false;
|
|
_townTab.Selected = false;
|
|
_summaryTab.Selected = false;
|
|
|
|
uint heritageId = _bindings.View()?.Snapshot.HeritageId ?? 0u;
|
|
bool isOlthoi = heritageId == (uint)ChargenHeritageGroup.Olthoi
|
|
|| heritageId == (uint)ChargenHeritageGroup.OlthoiAcid;
|
|
if (isOlthoi)
|
|
{
|
|
_professionTab.Visible = false;
|
|
_skillsTab.Visible = false;
|
|
_townTab.Visible = false;
|
|
if (_currentPage < previous)
|
|
{
|
|
if (_currentPage is Page.Profession or Page.Skills)
|
|
_currentPage = Page.Heritage;
|
|
else if (_currentPage == Page.Town)
|
|
_currentPage = Page.Appearance;
|
|
}
|
|
else
|
|
{
|
|
if (_currentPage is Page.Profession or Page.Skills)
|
|
_currentPage = Page.Appearance;
|
|
else if (_currentPage == Page.Town)
|
|
_currentPage = Page.Summary;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_professionTab.Visible = true;
|
|
_skillsTab.Visible = true;
|
|
_townTab.Visible = true;
|
|
}
|
|
|
|
SetMasterPageState(0x10000025u + (uint)_currentPage - 1u);
|
|
switch (_currentPage)
|
|
{
|
|
case Page.Heritage:
|
|
_heritagePageRoot.Visible = true;
|
|
_heritageTab.Selected = true;
|
|
break;
|
|
case Page.Profession:
|
|
_professionPageRoot.Visible = true;
|
|
_professionTab.Selected = true;
|
|
break;
|
|
case Page.Skills:
|
|
_skillsPageRoot.Visible = true;
|
|
_skillsTab.Selected = true;
|
|
break;
|
|
case Page.Appearance:
|
|
_appearancePageRoot.Visible = true;
|
|
_appearanceTab.Selected = true;
|
|
break;
|
|
case Page.Town:
|
|
_townPageRoot.Visible = true;
|
|
_townTab.Selected = true;
|
|
break;
|
|
case Page.Summary:
|
|
_summaryPageRoot.Visible = true;
|
|
_summaryTab.Selected = true;
|
|
_next.Visible = false;
|
|
_finish.Visible = true;
|
|
break;
|
|
}
|
|
|
|
// Random (0x100003cb): CC5 ports RandomizeAppearance/RandomizeClothing
|
|
// (Appearance) and RandomizeCharacter (Summary), retiring both gaps
|
|
// AP-212 used to track for those two pages — only Skills'
|
|
// RandomizeSkills remains unported (AP-212, narrowed).
|
|
_random.Enabled = _currentPage is not Page.Skills;
|
|
// Finish (0x100003c8): retail enables it on Summary only
|
|
// (ListenToElementMessage's case 0x100003c8 no-ops off Summary).
|
|
_finish.Enabled = _currentPage == Page.Summary;
|
|
|
|
_lastRevision = long.MinValue;
|
|
Tick();
|
|
}
|
|
|
|
private void SetMasterPageState(uint stateId)
|
|
{
|
|
if (_masterPage is IUiDatStateful stateful)
|
|
stateful.TrySetRetailState(stateId);
|
|
}
|
|
|
|
// ── Heritage tab-restore (gmCharGenMainUI::ListenToElementMessage @ ────
|
|
// ── 0x004e9450, the heritage-button bubble arm) ─────────────────────
|
|
|
|
/// <summary>SHOW ids (label_4e9673, three <c>SetVisible(1)</c> calls) —
|
|
/// verbatim off the decompiled switch's case list at
|
|
/// <c>0x004e9450</c>.</summary>
|
|
private static readonly IReadOnlySet<uint> HeritageTabShowButtonIds = new HashSet<uint>
|
|
{
|
|
0x100003BFu, 0x100003C1u, 0x100003C2u, 0x100003C3u,
|
|
0x10000590u, 0x10000591u, 0x100005A9u, 0x100005BFu,
|
|
0x100005C4u, 0x100005E8u,
|
|
};
|
|
|
|
/// <summary>HIDE ids (@0x004e96b9, three <c>SetVisible(0)</c> calls) —
|
|
/// the Olthoi/OlthoiAcid heritage buttons.</summary>
|
|
private static readonly IReadOnlySet<uint> HeritageTabHideButtonIds = new HashSet<uint>
|
|
{
|
|
0x100005C7u, 0x100005C8u,
|
|
};
|
|
|
|
/// <summary>
|
|
/// Ports <c>gmCharGenMainUI::ListenToElementMessage @ 0x004e9450</c>'s
|
|
/// heritage-button tab-restore arm: heritage-button clicks bubble to
|
|
/// the master shell and SYNCHRONOUSLY show/hide the Profession/Skills/
|
|
/// Town tabs, independent of <see cref="ApplyProgressState"/>'s own
|
|
/// tab-visibility recompute at page-switch time (that recompute only
|
|
/// runs when Back/Next/a tab is clicked — not on every heritage pick).
|
|
/// Retail quirk reproduced faithfully: Lugian's button id
|
|
/// (<c>0x100005f1</c>) sits OUTSIDE both the SHOW and HIDE case lists
|
|
/// in the decompiled switch, so clicking Lugian neither restores nor
|
|
/// hides the tabs — a genuine retail bug (the tabs stay in whatever
|
|
/// state the PREVIOUS heritage selection left them), not an acdream
|
|
/// omission. Review fix round F3 (2026-08-15): this arm was entirely
|
|
/// unported — before this fix, selecting a human heritage right after
|
|
/// Olthoi/OlthoiAcid left the tabs hidden until the next Back/Next/tab
|
|
/// click recomputed them.
|
|
/// </summary>
|
|
private void ApplyHeritageTabRestore(uint buttonElementId)
|
|
{
|
|
if (HeritageTabShowButtonIds.Contains(buttonElementId))
|
|
{
|
|
_professionTab.Visible = true;
|
|
_skillsTab.Visible = true;
|
|
_townTab.Visible = true;
|
|
}
|
|
else if (HeritageTabHideButtonIds.Contains(buttonElementId))
|
|
{
|
|
_professionTab.Visible = false;
|
|
_skillsTab.Visible = false;
|
|
_townTab.Visible = false;
|
|
}
|
|
// Else (including Lugian, 0x100005f1): no-op, matching retail.
|
|
}
|
|
|
|
// ── Finish (gmCharGenMainUI::DoFinish @ 0x004E9170) ─────────────────
|
|
|
|
/// <summary>The Finish button's ordinary click — retail's <c>arg2 = 1</c>
|
|
/// call site (<c>0x004E9579</c>). Re-checks the current page defensively,
|
|
/// mirroring <c>ListenToElementMessage</c>'s own
|
|
/// <c>m_eProgressState != ECG_SUMMARY</c> no-op guard.</summary>
|
|
private void OnFinish()
|
|
{
|
|
if (_disposed || _currentPage != Page.Summary)
|
|
return;
|
|
TryFinish(confirmedUnspentCredits: false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends via <see cref="CharacterCreationRuntimeBindings.Finish"/>
|
|
/// (which itself calls <c>RuntimeCharacterCreationState.TryBeginFinish</c>);
|
|
/// on a LOCAL refusal, surfaces retail's own dialog for the two refusal
|
|
/// reasons retail dialogs at all (<c>NoName</c> ->
|
|
/// <c>ID_CharGen_NoNameWarning</c>; <c>AttributeCreditsUnspent</c> ->
|
|
/// the credit-warning confirm, whose OWN confirm re-invokes this method
|
|
/// with <paramref name="confirmedUnspentCredits"/> — retail's
|
|
/// <c>arg2 == 0</c> call site, <c>0x004E98BB</c>). The remaining local
|
|
/// refusals (<c>HeritageOrGenderUnset</c>, <c>AlreadyPending</c>,
|
|
/// <c>RosterFull</c>) have no retail dialog citation — retail's own
|
|
/// <c>DoFinish</c> silently falls through to its final <c>return 0</c>
|
|
/// for an already-Pending double-click, and the other two are
|
|
/// acdream-only additions (register AP-223, AP-211) with the same
|
|
/// silent-refusal shape.
|
|
/// </summary>
|
|
private void TryFinish(bool confirmedUnspentCredits)
|
|
{
|
|
if (_bindings.Finish(confirmedUnspentCredits).Status != RuntimeCommandStatus.Rejected)
|
|
return;
|
|
|
|
RuntimeCharacterCreationLocalRefusal refusal =
|
|
_bindings.View()?.Snapshot.LastLocalRefusal ?? default;
|
|
if (refusal.NoName)
|
|
ShowNoNameWarningDialog();
|
|
else if (refusal.AttributeCreditsUnspent)
|
|
ShowCreditWarningDialog();
|
|
}
|
|
|
|
/// <summary>Ports the empty-name half of <c>DoFinish</c>
|
|
/// (<c>ID_CharGen_NoNameWarning</c>, <c>@0x004e91dd</c>) — a plain
|
|
/// informational dialog, no confirm/cancel semantics.</summary>
|
|
private void ShowNoNameWarningDialog()
|
|
{
|
|
if (_noNameWarningDialogContext != 0u)
|
|
return;
|
|
_noNameWarningDialogContext = _dialogs.MakeMessage(
|
|
_strings.NoNameWarning,
|
|
data =>
|
|
{
|
|
_ = data;
|
|
_noNameWarningDialogContext = 0u;
|
|
});
|
|
}
|
|
|
|
/// <summary>Ports <c>gmCharGenMainUI::MakeCreditWarningDialog @
|
|
/// 0x004e8870</c> (<c>ID_CharGen_CreditWarning</c>) — on confirm,
|
|
/// re-invokes <see cref="TryFinish"/> with
|
|
/// <c>confirmedUnspentCredits: true</c>, retail's <c>DoFinish(this, 0)</c>
|
|
/// call at <c>RecvNotice_CloseDialog @0x004e98bb</c>.</summary>
|
|
private void ShowCreditWarningDialog()
|
|
{
|
|
if (_creditWarningDialogContext != 0u)
|
|
return;
|
|
_creditWarningDialogContext = _dialogs.MakeConfirmation(
|
|
_strings.CreditWarning,
|
|
data =>
|
|
{
|
|
_creditWarningDialogContext = 0u;
|
|
if (_disposed || _suppressDialogCallbacks)
|
|
return;
|
|
if (data.GetBoolean(RetailDialogProperty.ConfirmationResult))
|
|
TryFinish(confirmedUnspentCredits: true);
|
|
});
|
|
}
|
|
|
|
// ── 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>
|
|
private void ReconcileDialogs(RuntimeCharacterCreationSnapshot snapshot)
|
|
{
|
|
RuntimeCharacterCreationRejection? rejection = snapshot.LastRejection;
|
|
if (rejection is null)
|
|
{
|
|
_lastShownRejection = null;
|
|
return;
|
|
}
|
|
if (_lastShownRejection == rejection)
|
|
return;
|
|
_lastShownRejection = rejection;
|
|
|
|
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,
|
|
};
|
|
if (key is null)
|
|
return;
|
|
string? message = _bindings.ResolveText?.Invoke(key);
|
|
if (message is null)
|
|
return;
|
|
|
|
_dialogs.MakeMessage(message, data =>
|
|
{
|
|
_ = data;
|
|
_bindings.AcknowledgeRejection?.Invoke();
|
|
});
|
|
}
|
|
|
|
private void Deactivate()
|
|
{
|
|
if (_active)
|
|
{
|
|
_active = false;
|
|
_openOnStartConsumed = false;
|
|
Close();
|
|
}
|
|
CloseAllDialogs(suppressCallbacks: true);
|
|
}
|
|
|
|
private void CloseAllDialogs(bool suppressCallbacks)
|
|
{
|
|
bool previous = _suppressDialogCallbacks;
|
|
_suppressDialogCallbacks |= suppressCallbacks;
|
|
try
|
|
{
|
|
if (_exitDialogContext != 0u)
|
|
{
|
|
uint closing = _exitDialogContext;
|
|
_exitDialogContext = 0u;
|
|
_dialogs.CloseDialog(closing);
|
|
}
|
|
if (_creditWarningDialogContext != 0u)
|
|
{
|
|
uint closing = _creditWarningDialogContext;
|
|
_creditWarningDialogContext = 0u;
|
|
_dialogs.CloseDialog(closing);
|
|
}
|
|
if (_randomizeWarningDialogContext != 0u)
|
|
{
|
|
uint closing = _randomizeWarningDialogContext;
|
|
_randomizeWarningDialogContext = 0u;
|
|
_dialogs.CloseDialog(closing);
|
|
}
|
|
if (_noNameWarningDialogContext != 0u)
|
|
{
|
|
uint closing = _noNameWarningDialogContext;
|
|
_noNameWarningDialogContext = 0u;
|
|
_dialogs.CloseDialog(closing);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_suppressDialogCallbacks = previous;
|
|
}
|
|
}
|
|
}
|