using System.Numerics;
using AcDream.Core.CharGen;
using AcDream.Runtime;
using AcDream.Runtime.Session;
namespace AcDream.App.UI.Layout;
///
/// Bindings the retail character-creation screen (gmCharGenMainUI)
/// needs beyond the borrowed view: generation-capturing command wrappers,
/// mirroring 's shape exactly.
/// Every Func here is a late-bound seam (Campaign CC — see
/// feedback_resolve_deferred_funcs_per_call.md): callers MUST resolve
/// it per-call, never capture the delegate once at mount time.
///
/// Campaign CC slice CC4: the retail
/// transition is Create Character (0x100003A0) →
/// QueueUIMode(0x1000000b), but that button stays ghosted until CC7's
/// closing move. This flag is the interim env/test-only open seam
/// (ACDREAM_OPEN_CHARGEN=1 → )
/// so the screen can be exercised before the real button is wired.
public sealed record CharacterCreationRuntimeBindings(
Func View,
Func SelectHeritage,
Func SelectGender,
Func SelectTemplate,
Func SetAttribute,
Func SetAttributeLock,
Func TrainSkill,
Func SpecializeSkill,
Func UntrainSkill,
Func SelectStartArea,
Func Finish,
Action RequestExit,
/// DAT string lookup (table 0x23000002, the SAME table
/// every other ID_CharGen_*/ID_Character* key resolves
/// through) — used by the Heritage page's composed description text.
/// degrades to the heritage's own DAT
/// Name field instead of the full composed copy.
Func? ResolveText = null,
bool OpenOnStart = false);
///
/// Projects Runtime's borrowed
/// through retail gmCharGenMainUI'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. The Appearance (0x100003d4) and Summary
/// (0x100003d6) page roots are mounted but content-inert — CC6/CC5
/// fill them (register TS-82).
///
///
/// Decomp anchors: root construction + child resolution
/// gmCharGenMainUI::gmCharGenMainUI @ 0x004e7eb0 (root element
/// 0x100003cc from enum 0x10000039); page switching
/// gmCharGenMainUI::SetProgressState @ 0x004e7a10 (the Olthoi
/// tab-hiding + redirect logic); nav dispatch
/// gmCharGenMainUI::ListenToElementMessage @ 0x004e9450; exit
/// confirmation gmCharGenMainUI::DoExit @ 0x004e8650; randomize
/// dispatch gmCharGenMainUI::DoRandom @ 0x004e7d70.
///
///
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;
/// Retail's gmCharGenMainUI::ECGProgress enum values —
/// used verbatim as the master page's per-page state ids
/// (0x10000025 + (page - 1)) and the tab-hide/redirect math in
/// .
internal enum Page
{
Heritage = 1,
Profession = 2,
Skills = 3,
Appearance = 4,
Town = 5,
Summary = 6,
}
internal sealed record DialogStrings(string ExitWarning);
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 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;
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 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 — see that controller's own comment.
// Both screens author the identical extent, so it is safe for both
// controllers to independently (idempotently) push the SAME value
// to the shared UiRoot.FixedCanvasSize; this controller therefore
// never NULLS it back out on close (see Deactivate/Close), leaving
// char-management's own per-tick set as the surviving owner once
// this screen is not the active one.
_authoredCanvas = new Vector2(
Root.Width > 0f ? Root.Width : 800f,
Root.Height > 0f ? Root.Height : 600f);
_heritagePage = new CharacterCreationHeritagePage(heritagePageRoot, bindings);
_professionPage = new CharacterCreationProfessionPage(professionPageRoot, bindings);
_skillsPage = new CharacterCreationSkillsPage(skillsPageRoot, bindings, templateResolver);
_townPage = new CharacterCreationTownPage(townPageRoot, bindings);
// gmCharGenMainUI::ListenToElementMessage @ 0x004e9450.
_back.OnClick = OnBack;
_next.OnClick = OnNext;
// Finish (0x100003c8) stays ghosted this round: Summary
// (0x100003d6) is CC5's placeholder, and DoFinish's real gate
// sequence lives in RuntimeCharacterCreationState.TryBeginFinish —
// wiring the button here without a Summary page to confirm/collect
// the name would let a click reach the wire with an empty name and
// silently refuse. No OnClick handler; _finish.Enabled stays false
// (see ApplyProgressState).
_finish.OnClick = null;
// 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;
internal static CharacterCreationUiController? CreateDetached(
UiRoot host,
ImportedLayout layout,
Func 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.FixedCanvasSize = _authoredCanvas;
_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);
_lastGeneration = snapshot.Generation;
_lastRevision = snapshot.Revision;
}
ReconcileDialogs(snapshot);
}
/// Opens the screen at retail's authored default page
/// (gmCharGenMainUI::gmCharGenMainUI's trailing
/// SetProgressState(this, ECG_HERTAGE)).
internal void Open()
{
if (_disposed)
return;
_isOpen = true;
ApplyProgressState(Page.Heritage);
}
private void Close()
{
_isOpen = false;
Root.Visible = false;
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
try
{
CloseAllDialogs(suppressCallbacks: true);
}
finally
{
_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();
_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
// are ported below; Skills' CharGenState::RandomizeSkills and the
// Summary randomize-warning dialog have no CC3 primitive/page yet
// this round — register AP-212 covers both gaps, and _random.Enabled
// already keeps the control ghosted on those pages (ApplyProgressState).
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.Town:
_townPage.Randomize(view);
break;
}
}
// ── 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): retail refuses on Skills (no
// RandomizeSkills primitive ported — AP-212) and on Summary
// (MakeRandomizeWarningDialog is CC5's); Appearance is this round's
// placeholder.
_random.Enabled = _currentPage
is not (Page.Skills or Page.Appearance or Page.Summary);
// Finish stays ghosted regardless of page — Summary is a
// placeholder this round (see the ctor comment on _finish.OnClick).
_finish.Enabled = false;
_lastRevision = long.MinValue;
Tick();
}
private void SetMasterPageState(uint stateId)
{
if (_masterPage is IUiDatStateful stateful)
stateful.TrySetRetailState(stateId);
}
private void ReconcileDialogs(RuntimeCharacterCreationSnapshot snapshot)
{
// Local-refusal / rejection surfacing is CC5's Summary-page job
// (the Finish gate only fires from that page). This round only
// needs the exit-confirmation dialog reconciled against disposal.
_ = snapshot;
}
private void Deactivate()
{
if (_active)
{
_active = false;
_isOpen = false;
_openOnStartConsumed = false;
Root.Visible = false;
}
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);
}
}
finally
{
_suppressDialogCallbacks = previous;
}
}
}