using System.Numerics; using AcDream.Runtime; using AcDream.Runtime.Session; namespace AcDream.App.UI.Layout; /// /// Projects Runtime's one borrowed pre-world character-selection owner through /// retail gmCharacterManagementUI's authored retained layout. The list is /// intentionally flat: the retail class owns no viewport or model preview. /// internal sealed class CharacterManagementUiController : IDisposable { internal const uint RootEnum = 0x10000005u; internal const uint RootElementId = 0x1000039Au; internal const uint ListElementId = 0x1000039Du; internal const uint CreateElementId = 0x100003A0u; internal const uint EnterElementId = 0x100003A2u; internal const uint DeleteElementId = 0x1000039Fu; internal const uint RestoreElementId = 0x1000039Eu; internal sealed record DialogStrings( Func DeleteConfirmation, string DeleteResponse, string PleaseWait, string EnteringWorld); private readonly UiRoot _host; private readonly ImportedLayout _layout; private readonly UiTemplateListBox _list; private readonly UiButton _create; private readonly UiButton _enter; private readonly UiButton _delete; private readonly UiButton _restore; private readonly RetailDialogFactory _dialogs; private readonly CharacterSelectionRuntimeBindings _bindings; private readonly DialogStrings _strings; private readonly List _rows = []; private readonly Dictionary _rowIds = []; private RuntimeGenerationToken _lastGeneration; private long _lastRevision = long.MinValue; private uint _deleteDialogContext; private uint _operationWaitContext; private uint _enterWaitContext; private uint _errorDialogContext; private bool _active; private bool _restoreCommandInFlight; private bool _suppressDialogCallbacks; private bool _disposed; private CharacterManagementUiController( UiRoot host, ImportedLayout layout, UiTemplateListBox list, UiButton create, UiButton enter, UiButton delete, UiButton restore, RetailDialogFactory dialogs, CharacterSelectionRuntimeBindings bindings, DialogStrings strings) { _host = host; _layout = layout; _list = list; _create = create; _enter = enter; _delete = delete; _restore = restore; _dialogs = dialogs; _bindings = bindings; _strings = strings; Root.Left = 0f; Root.Top = 0f; Root.Anchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right | AnchorEdges.Bottom; if (host.Width > 0f) Root.Width = host.Width; if (host.Height > 0f) Root.Height = host.Height; Root.ClickThrough = false; Root.Visible = false; // Create Character belongs to a future campaign. Keep retail's // authored control in place and visibly ghosted; do not hide it or // invent an action. _create.Visible = true; _create.Enabled = false; _create.OnClick = null; _enter.OnClick = EnterSelected; _delete.OnClick = RequestDelete; _restore.OnClick = RestoreSelected; } internal UiElement Root => _layout.Root; internal IReadOnlyList Rows => _rows; internal uint DeleteDialogContext => _deleteDialogContext; internal uint OperationWaitContext => _operationWaitContext; internal uint EnterWaitContext => _enterWaitContext; internal uint ErrorDialogContext => _errorDialogContext; internal void ResetSession() { if (_disposed) return; Deactivate(); _lastRevision = long.MinValue; } internal static CharacterManagementUiController? Bind( UiRoot host, ImportedLayout layout, Func templateResolver, RetailDialogFactory dialogs, CharacterSelectionRuntimeBindings bindings, DialogStrings strings) { ArgumentNullException.ThrowIfNull(host); ArgumentNullException.ThrowIfNull(layout); ArgumentNullException.ThrowIfNull(templateResolver); ArgumentNullException.ThrowIfNull(dialogs); ArgumentNullException.ThrowIfNull(bindings); ArgumentNullException.ThrowIfNull(strings); if (ContainsViewport(layout.Root)) { Console.WriteLine( "[UI] character management: refusing an unapproved model-preview viewport."); return null; } if (layout.Root.DatElementId != RootElementId || layout.FindElement(ListElementId) is not UiTemplateListBox list || layout.FindElement(CreateElementId) is not UiButton create || layout.FindElement(EnterElementId) is not UiButton enter || layout.FindElement(DeleteElementId) is not UiButton delete || layout.FindElement(RestoreElementId) is not UiButton restore) { Console.WriteLine( "[UI] character management: the authored root/list/button contract is incomplete."); return null; } list.TemplateResolver = templateResolver; var controller = new CharacterManagementUiController( host, layout, list, create, enter, delete, restore, dialogs, bindings, strings); host.AddChild(controller.Root); controller.Tick(); return controller; } private static bool ContainsViewport(UiElement element) { if (element is UiViewport) return true; foreach (UiElement child in element.Children) if (ContainsViewport(child)) return true; return false; } internal void Tick() { if (_disposed) return; IRuntimeCharacterSelectionView? view = _bindings.View(); RuntimeCharacterSelectionSnapshot snapshot = view?.Snapshot ?? default; if (view is null || !snapshot.IsActive) { Deactivate(); _lastGeneration = snapshot.Generation; _lastRevision = snapshot.Revision; return; } if (!_active) { _active = true; Root.Visible = true; _host.BringToFront(Root); } if (_lastGeneration != snapshot.Generation || _lastRevision != snapshot.Revision) { if (TryCaptureRoster(view, snapshot, out RuntimeCharacterSelectionEntry[] roster)) { bool rowsReady; if (RowsMatchRoster(roster, snapshot.SlotCount)) { ApplyHighlight(snapshot.HighlightedCharacterId); rowsReady = true; } else { rowsReady = RebuildRows( roster, snapshot.SlotCount, snapshot.HighlightedCharacterId); } if (rowsReady) { _lastGeneration = snapshot.Generation; _lastRevision = snapshot.Revision; } } else { // A receive-thread roster/reset raced the borrowed snapshot. // Leave the revision unconsumed so the next frame retries from // one coherent view; never present a partially mixed roster. _lastRevision = long.MinValue; snapshot = view.Snapshot; if (!snapshot.IsActive) { Deactivate(); _lastGeneration = snapshot.Generation; _lastRevision = snapshot.Revision; return; } } } else { ApplyHighlight(snapshot.HighlightedCharacterId); } ApplyButtons(snapshot.Buttons); ReconcileDialogs(view, snapshot); } public void Dispose() { if (_disposed) return; _disposed = true; CloseAllDialogs(suppressCallbacks: true); _enter.OnClick = null; _delete.OnClick = null; _restore.OnClick = null; foreach (UiButton row in _rows) { row.OnClick = null; row.OnDoubleClick = null; } _rows.Clear(); _rowIds.Clear(); _list.Flush(); _list.TemplateResolver = null; _host.RemoveChild(Root); } private static bool TryCaptureRoster( IRuntimeCharacterSelectionView view, RuntimeCharacterSelectionSnapshot expected, out RuntimeCharacterSelectionEntry[] roster) { roster = new RuntimeCharacterSelectionEntry[expected.RosterCount]; for (int i = 0; i < roster.Length; i++) { if (!view.TryGetAt(i, out roster[i])) return false; } RuntimeCharacterSelectionSnapshot after = view.Snapshot; return after.Generation == expected.Generation && after.Revision == expected.Revision && after.RosterCount == expected.RosterCount; } private bool RowsMatchRoster( IReadOnlyList roster, int allowedSlotCount) { if (_rows.Count != roster.Count) return false; int rowHeight = ComputeRowHeight( _list.Height, roster.Count, allowedSlotCount); for (int i = 0; i < roster.Count; i++) { UiButton row = _rows[i]; RuntimeCharacterSelectionEntry character = roster[i]; if (!_rowIds.TryGetValue(row, out uint characterId) || characterId != character.CharacterId || !string.Equals(row.Label, character.Name, StringComparison.Ordinal) || (int)row.Height != rowHeight || row.LabelColor != (character.IsPendingDelete ? new Vector4(1f, 0f, 0f, 1f) : Vector4.One)) { return false; } } return true; } private bool RebuildRows( IReadOnlyList roster, int allowedSlotCount, uint highlightedCharacterId) { foreach (UiButton row in _rows) { row.OnClick = null; row.OnDoubleClick = null; } _rows.Clear(); _rowIds.Clear(); _list.Flush(); int rowHeight = ComputeRowHeight( _list.Height, roster.Count, allowedSlotCount); _list.LineHeight = rowHeight; bool complete = _list.Templates.Count > 0 && _list.TemplateResolver is not null; foreach (RuntimeCharacterSelectionEntry character in roster) { if (!complete) break; UiTemplateListEntry template = _list.Templates[0]; if (_list.TemplateResolver!( template.TemplateLayoutId, template.TemplateElementId) is not UiButton row) { complete = false; break; } // AddItemFromTemplateList creates the same template, but its // retained viewport stacks at the template's authored 16px // height. Retail establishes the computed size on every row; our // list fixes Top during insertion, so build and resize first to // make every subsequent Top exact. row.Height = rowHeight; _list.AddPrebuiltRow(row); uint characterId = character.CharacterId; row.Label = character.Name; row.LabelColor = character.IsPendingDelete ? new Vector4(1f, 0f, 0f, 1f) : Vector4.One; row.Enabled = true; row.SuppressSelfToggle = true; row.Selected = characterId == highlightedCharacterId; row.OnClick = () => Highlight(characterId); row.OnDoubleClick = EnterSelected; _rows.Add(row); _rowIds.Add(row, characterId); } if (complete) return true; foreach (UiButton row in _rows) { row.OnClick = null; row.OnDoubleClick = null; } _rows.Clear(); _rowIds.Clear(); _list.Flush(); _lastRevision = long.MinValue; return false; } internal static int ComputeRowHeight( float listHeight, int rosterCount, int allowedSlotCount) { // RebuildCharacterList @ 0x004EC3A0 uses integer UIRegion height and // signed integer division for both terms. The 0x66666667 multiply/ // shift sequence is compiler output for height / 10. int height = (int)MathF.Truncate(listHeight); int denominator = Math.Max(rosterCount, allowedSlotCount); if (denominator <= 0) return height / 10; return Math.Max(height / denominator, height / 10); } private void ApplyHighlight(uint highlightedCharacterId) { foreach (UiButton row in _rows) row.Selected = _rowIds.TryGetValue(row, out uint characterId) && characterId == highlightedCharacterId; } private void ApplyButtons(RuntimeCharacterSelectionButtons buttons) { _create.Visible = true; _create.Enabled = false; _enter.Enabled = buttons.CanEnter; _delete.Visible = buttons.DeleteVisible; _delete.Enabled = buttons.CanDelete; _restore.Visible = buttons.RestoreVisible; _restore.Enabled = buttons.CanRestore; } private void Highlight(uint characterId) { if (_disposed) return; _bindings.Highlight(characterId); InvalidateAndTick(); } private void EnterSelected() { if (_disposed) return; // Open retail's wait context before the synchronous Runtime command // starts its existing ServerReady transaction. The state projection // remains authoritative and closes it on InWorld/error/reset. EnsureEnterWait(); RuntimeCommandResult result = _bindings.Enter(); if (!result.Accepted) CloseContext(ref _enterWaitContext, suppressCallback: true); InvalidateAndTick(); } private void RequestDelete() { if (_disposed) return; _bindings.RequestDelete(); InvalidateAndTick(); } private void RestoreSelected() { if (_disposed) return; // ListenToElementMessage @ 0x004ED5A0 opens Please Wait before it // calls CPlayerSystem::RestoreCharacter. Keep it modal even if a // synchronous command callback re-enters Tick before Runtime has // returned its accepted projection. EnsureOperationWait(); RuntimeCommandResult result = default; Exception? failure = null; _restoreCommandInFlight = true; try { result = _bindings.Restore(); } catch (Exception error) { failure = error; } finally { _restoreCommandInFlight = false; } if (failure is not null) { Console.WriteLine( $"[UI] character restore command failed: {failure.Message}"); CloseContext(ref _operationWaitContext, suppressCallback: true); InvalidateAndTick(); return; } if (!result.Accepted) CloseContext(ref _operationWaitContext, suppressCallback: true); InvalidateAndTick(); } private void ReconcileDialogs( IRuntimeCharacterSelectionView view, RuntimeCharacterSelectionSnapshot snapshot) { if (snapshot.Error is { } error) { CloseContext(ref _deleteDialogContext, suppressCallback: true); CloseContext(ref _operationWaitContext, suppressCallback: true); CloseContext(ref _enterWaitContext, suppressCallback: true); EnsureError(error.Message); return; } CloseContext(ref _errorDialogContext, suppressCallback: true); if (snapshot.Lifecycle == RuntimeCharacterSelectionLifecycle.EnteringWorld) { CloseContext(ref _deleteDialogContext, suppressCallback: true); CloseContext(ref _operationWaitContext, suppressCallback: true); EnsureEnterWait(); return; } CloseContext(ref _enterWaitContext, suppressCallback: true); if (snapshot.PendingDeleteCharacterId != 0u && view.TryGet(snapshot.PendingDeleteCharacterId, out RuntimeCharacterSelectionEntry pending)) { EnsureDeleteConfirmation(pending.Name); } else { CloseContext(ref _deleteDialogContext, suppressCallback: true); } if (_restoreCommandInFlight || snapshot.Operation is RuntimeCharacterSelectionOperation.DeleteRequested or RuntimeCharacterSelectionOperation.DeleteAcknowledged or RuntimeCharacterSelectionOperation.RestoreRequested) { EnsureOperationWait(); } else { CloseContext(ref _operationWaitContext, suppressCallback: true); } } private void EnsureDeleteConfirmation(string characterName) { if (_deleteDialogContext != 0u) return; _deleteDialogContext = _dialogs.MakeConfirmationTextInput( _strings.DeleteConfirmation(characterName), data => { _deleteDialogContext = 0u; if (_disposed || _suppressDialogCallbacks) return; string response = data.GetString( RetailDialogProperty.TextInputResult) ?? string.Empty; if (string.Equals( response, _strings.DeleteResponse, StringComparison.OrdinalIgnoreCase)) { _bindings.ConfirmDelete(); } else { _bindings.Cancel(); } InvalidateAndTick(); }); } private void EnsureOperationWait() { if (_operationWaitContext == 0u) _operationWaitContext = _dialogs.MakeWait(_strings.PleaseWait); } private void EnsureEnterWait() { if (_enterWaitContext == 0u) _enterWaitContext = _dialogs.MakeWait(_strings.EnteringWorld); } private void EnsureError(string message) { if (_errorDialogContext != 0u) return; _errorDialogContext = _dialogs.MakeMessage( message, _ => { _errorDialogContext = 0u; if (_disposed || _suppressDialogCallbacks) return; _bindings.Cancel(); InvalidateAndTick(); }); } private void InvalidateAndTick() { _lastRevision = long.MinValue; Tick(); } private void Deactivate() { if (_active) { _active = false; Root.Visible = false; } foreach (UiButton row in _rows) { row.OnClick = null; row.OnDoubleClick = null; } _rows.Clear(); _rowIds.Clear(); _list.Flush(); CloseAllDialogs(suppressCallbacks: true); } private void CloseAllDialogs(bool suppressCallbacks) { bool previous = _suppressDialogCallbacks; _suppressDialogCallbacks |= suppressCallbacks; try { CloseContext(ref _deleteDialogContext, suppressCallback: false); CloseContext(ref _operationWaitContext, suppressCallback: false); CloseContext(ref _enterWaitContext, suppressCallback: false); CloseContext(ref _errorDialogContext, suppressCallback: false); } finally { _suppressDialogCallbacks = previous; } } private void CloseContext(ref uint context, bool suppressCallback) { uint closing = context; if (closing == 0u) return; context = 0u; bool previous = _suppressDialogCallbacks; _suppressDialogCallbacks |= suppressCallback; try { _dialogs.CloseDialog(closing); } finally { _suppressDialogCallbacks = previous; } } }