using System.Collections.Generic; using AcDream.App.Plugins; using AcDream.App.Rendering; using AcDream.App.UI.Layout; using AcDream.App.UI.Testing; using AcDream.Core.Combat; using AcDream.Core.Items; using AcDream.Core.Net.Messages; using AcDream.Core.Selection; using AcDream.UI.Abstractions; using AcDream.UI.Abstractions.Panels.Chat; using AcDream.UI.Abstractions.Panels.Settings; using AcDream.UI.Abstractions.Panels.Vitals; using DatReaderWriter; using Silk.NET.Input; namespace AcDream.App.UI; public sealed record RetailUiAssets( DatCollection Dats, object DatLock, Func ResolveSprite, Func ResolveFont, UiDatFont? DefaultFont, BitmapFont? DebugFont, ControlsIni Controls, IconComposer Icons); public sealed record VitalsRuntimeBindings(VitalsVM ViewModel); public sealed record ChatRuntimeBindings(ChatVM ViewModel, Func CommandBus); public sealed record RadarRuntimeBindings( Func Snapshot, SelectionState Selection, Action SetUiLocked); public sealed record ToolbarRuntimeBindings( ClientObjectTable Objects, Func> Shortcuts, Func ResolveIcon, Action UseItem, CombatState Combat, ItemManaState ItemMana, Action ToggleCombat, ItemInteractionController ItemInteraction, Action? SendAddShortcut, Action? SendRemoveShortcut, SelectionState Selection, Action> SubscribeHealthChanged, Action> UnsubscribeHealthChanged, Func IsHealthTarget, Func ResolveName, Func HealthPercent, Func HasHealth, Func StackSize, Action SendQueryHealth, Action SendQueryItemMana, Func PlayerGuid, Action? SendPutItemInContainer); public sealed record CharacterRuntimeBindings(CharacterSheetProvider Provider); public sealed record InventoryRuntimeBindings( ClientObjectTable Objects, Func PlayerGuid, Func ResolveIcon, Func Strength, Action? SendUse, Action? SendNoLongerViewing, Action? SendPutItemInContainer, Action? SendStackableSplitToContainer, Action? SendStackableMerge, Action? SendWield, ItemInteractionController ItemInteraction, SelectionState Selection); public sealed record RetailUiPersistenceBindings( SettingsStore Store, Func CharacterKey, Func<(int Width, int Height)> ScreenSize); public sealed record RetailUiProbeBindings( bool Enabled, string? ScriptPath, bool DumpOnStart, Action Log); public sealed record RetailUiCursorBindings( CursorFeedbackController Feedback, RetailCursorManager Manager); public sealed record RetailUiRuntimeBindings( UiHost Host, RetailUiAssets Assets, VitalsRuntimeBindings Vitals, ChatRuntimeBindings Chat, RadarRuntimeBindings Radar, ToolbarRuntimeBindings Toolbar, CharacterRuntimeBindings Character, InventoryRuntimeBindings Inventory, RetailUiCursorBindings Cursor, StackSplitQuantityState StackSplitQuantity, BufferedUiRegistry? Plugins, RetailUiPersistenceBindings? Persistence, RetailUiProbeBindings Probe); /// /// Composition owner for the production retained gameplay UI. GameWindow supplies /// state/action delegates and GL/DAT resolvers; this runtime imports layouts, binds /// controllers, mounts windows, owns persistence, and presents one tick/draw/dispose seam. /// public sealed class RetailUiRuntime : IDisposable { private readonly RetailUiRuntimeBindings _bindings; private StackSplitQuantityState StackSplitQuantity => _bindings.StackSplitQuantity; private readonly RetailWindowLayoutPersistence? _persistence; private readonly RetailUiAutomationScriptRunner? _automation; private bool _disposed; private RetailUiRuntime(RetailUiRuntimeBindings bindings) { _bindings = bindings; MountVitals(); MountRadar(); MountChat(); MountToolbar(); MountCharacter(); MountPlugins(); MountInventory(); Host.WindowManager.WindowVisibilityChanged += OnWindowVisibilityChanged; BindToolbarPanelButtons(); SyncToolbarWindowButtons(); if (bindings.Persistence is { } persistence) { _persistence = new RetailWindowLayoutPersistence( Host.WindowManager, persistence.Store, persistence.CharacterKey, persistence.ScreenSize); } if (bindings.Probe.Enabled) { var probe = new RetailUiAutomationProbe( Host.Root, bindings.Inventory.Objects, bindings.Probe.Log); _automation = new RetailUiAutomationScriptRunner( probe, bindings.Probe.ScriptPath, bindings.Probe.DumpOnStart, bindings.Probe.Log); } } public UiHost Host => _bindings.Host; public ItemInteractionController ItemInteraction => _bindings.Inventory.ItemInteraction; public CharacterSheetProvider CharacterSheetProvider => _bindings.Character.Provider; public ToolbarController? ToolbarController { get; private set; } public ToolbarInputController? ToolbarInputController { get; private set; } public SelectedObjectController? SelectedObjectController { get; private set; } public UiViewport? PaperdollViewportWidget { get; private set; } public UiNineSlicePanel? InventoryFrame { get; private set; } public static RetailUiRuntime Mount(RetailUiRuntimeBindings bindings) { ArgumentNullException.ThrowIfNull(bindings); try { return new RetailUiRuntime(bindings); } catch { bindings.Host.Dispose(); throw; } } public void Tick(double deltaSeconds) { SelectedObjectController?.Tick(deltaSeconds); Host.Tick(deltaSeconds); _automation?.Tick(deltaSeconds); } public void Draw(System.Numerics.Vector2 screenSize) => Host.Draw(screenSize); public bool HandleInputAction(AcDream.UI.Abstractions.Input.InputAction action) => ToolbarInputController?.Handle(action) == true; public void UpdateCursor(IEnumerable mice) { CursorFeedback feedback = _bindings.Cursor.Feedback.Update(Host.Root); _bindings.Cursor.Manager.Apply(mice, feedback); } public void RestoreLayout() => _persistence?.RestoreAll(); public bool ToggleWindow(string name) => Host.ToggleWindow(name); public void CloseWindow(string name) => Host.HideWindow(name); public void SyncToolbarWindowButtons() { if (ToolbarController is null) return; foreach (var (panelId, windowName) in RetailPanelCatalog.MountedPanels) ToolbarController.SetPanelOpen(panelId, Host.IsWindowVisible(windowName)); } private void BindToolbarPanelButtons() { ToolbarController?.BindPanelButtons( panelId => RetailPanelCatalog.TryGetWindowName(panelId, out string name) && Host.WindowManager.TryGet(name, out _), panelId => { if (RetailPanelCatalog.TryGetWindowName(panelId, out string name)) ToggleWindow(name); }); } private void OnWindowVisibilityChanged(string windowName, bool visible) { if (RetailPanelCatalog.TryGetPanelId(windowName, out uint panelId)) ToolbarController?.SetPanelOpen(panelId, visible); } private ImportedLayout? Import(uint layoutId) { lock (_bindings.Assets.DatLock) return LayoutImporter.Import( _bindings.Assets.Dats, layoutId, _bindings.Assets.ResolveSprite, _bindings.Assets.DefaultFont, _bindings.Assets.ResolveFont); } private void MountVitals() { ImportedLayout? layout = Import(0x2100006Cu); if (layout is null) { Console.WriteLine("[D.2b] vitals: LayoutDesc 0x2100006C not found — vitals unavailable."); return; } VitalsVM vm = _bindings.Vitals.ViewModel; VitalsController.Bind(layout, () => vm.HealthPercent, () => vm.StaminaPercent ?? 0f, () => vm.ManaPercent ?? 0f, () => (vm.HealthCurrent, vm.HealthMax) is (uint c, uint m) ? $"{c}/{m}" : "", () => (vm.StaminaCurrent, vm.StaminaMax) is (uint c, uint m) ? $"{c}/{m}" : "", () => (vm.ManaCurrent, vm.ManaMax) is (uint c, uint m) ? $"{c}/{m}" : ""); RetailWindowFrame.Mount(Host.Root, layout.Root, _bindings.Assets.ResolveSprite, new RetailWindowFrame.Options { WindowName = WindowNames.Vitals, Chrome = RetailWindowChrome.Imported, Left = 10f, Top = 30f, ResizeX = true, ResizeY = false, MinWidth = 40f, ContentClickThrough = false, }); Console.WriteLine("[D.2b] retail UI active — vitals window from LayoutDesc importer (0x2100006C)."); } private void MountRadar() { ImportedLayout? layout = Import(RadarController.LayoutId); if (layout is null || layout.Root is not UiRadar radarRoot) { Console.WriteLine("[D.6] radar: LayoutDesc 0x21000074 not found or root class mismatch."); return; } RadarController controller = RadarController.Bind( layout, _bindings.Radar.Snapshot, guid => { if (_bindings.Toolbar.ItemInteraction.OfferPrimaryClick(guid) != ItemPrimaryClickResult.NotActive) return; _bindings.Radar.Selection.Select(guid, SelectionChangeSource.Radar); }, setUiLocked: _bindings.Radar.SetUiLocked, datFont: _bindings.Assets.DefaultFont); RetailWindowFrame.Mount(Host.Root, radarRoot, _bindings.Assets.ResolveSprite, new RetailWindowFrame.Options { WindowName = WindowNames.Radar, Chrome = RetailWindowChrome.Imported, Left = Math.Max(0f, Host.Root.Width - radarRoot.Width - 10f), Top = 10f, Resizable = false, ResizeX = false, ResizeY = false, ConstrainDragToParent = true, ContentClickThrough = false, Controller = controller, }); Console.WriteLine("[D.6] retail radar/compass from LayoutDesc 0x21000074."); } private void MountChat() { ElementInfo? info; ImportedLayout? layout; lock (_bindings.Assets.DatLock) { info = LayoutImporter.ImportInfos(_bindings.Assets.Dats, ChatWindowController.LayoutId); layout = info is null ? null : LayoutImporter.Build( info, _bindings.Assets.ResolveSprite, _bindings.Assets.DefaultFont, _bindings.Assets.ResolveFont); } if (info is null || layout is null) { Console.WriteLine("[D.2b] chat: LayoutDesc 0x21000006 not found."); return; } ChatWindowController? controller = ChatWindowController.Bind( info, layout, _bindings.Chat.ViewModel, _bindings.Chat.CommandBus, _bindings.Assets.DefaultFont, _bindings.Assets.DebugFont, _bindings.Assets.ResolveSprite); if (controller is null) { Console.WriteLine("[D.2b] chat: required role elements missing in 0x21000006."); return; } controller.Transcript.Keyboard = Host.Keyboard; controller.Input.Keyboard = Host.Keyboard; UiElement root = controller.Root; RetailWindowHandle handle = RetailWindowFrame.Mount( Host.Root, root, _bindings.Assets.ResolveSprite, new RetailWindowFrame.Options { WindowName = WindowNames.Chat, Chrome = RetailWindowChrome.NineSlice, Left = 10f, Top = 440f, ContentWidth = 490f, ContentHeight = root.Height, RebaseContentLayout = true, DatConstraintSource = controller.DatWindowInfo, MinWidth = 200f, ResizeX = true, ResizeY = true, Opacity = 0.75f, Controller = controller, StateController = controller, }); controller.AttachWindow(handle); Host.Root.DefaultTextInput = controller.Input; Console.WriteLine("[D.2b] retail chat window from LayoutDesc importer (0x21000006)."); } private void MountToolbar() { ImportedLayout? layout = Import(0x21000016u); if (layout is null) { Console.WriteLine("[D.5.1] toolbar: LayoutDesc 0x21000016 not found."); return; } var (peace, war, empty) = LoadToolbarDigits(); ToolbarRuntimeBindings b = _bindings.Toolbar; ToolbarController = Layout.ToolbarController.Bind( layout, b.Objects, b.Shortcuts, b.ResolveIcon, b.UseItem, b.Combat, peace, war, empty, b.ItemInteraction, b.SendAddShortcut, b.SendRemoveShortcut, toggleCombat: b.ToggleCombat, selectItem: guid => b.Selection.Select(guid, SelectionChangeSource.Toolbar), selectedObjectId: () => b.Selection.SelectedObjectId ?? 0u, playerGuid: b.PlayerGuid, sendPutItemInContainer: b.SendPutItemInContainer); ToolbarInputController = new ToolbarInputController(ToolbarController, b.Selection); SelectedObjectController = Layout.SelectedObjectController.Bind( layout, b.Selection, b.SubscribeHealthChanged, b.UnsubscribeHealthChanged, handler => b.ItemMana.ItemManaChanged += handler, handler => b.ItemMana.ItemManaChanged -= handler, b.IsHealthTarget, b.ItemInteraction.IsOwnedByPlayer, b.ResolveName, b.HealthPercent, b.HasHealth, b.StackSize, b.SendQueryHealth, b.ItemMana.GetManaPercent, b.SendQueryItemMana, _bindings.Assets.DefaultFont, StackSplitQuantity, handler => b.Objects.ObjectUpdated += handler, handler => b.Objects.ObjectUpdated -= handler); UiElement root = layout.Root; RetailWindowHandle handle = RetailWindowFrame.Mount( Host.Root, root, _bindings.Assets.ResolveSprite, new RetailWindowFrame.Options { WindowName = WindowNames.Toolbar, Chrome = RetailWindowChrome.CollapsibleNineSlice, Left = 10f, Top = 300f, ContentWidth = 300f, ContentHeight = root.Height, Resizable = false, ResizeX = false, ResizeY = true, ResizableEdges = ResizeEdges.Bottom, ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right, ContentClickThrough = true, Controller = new RetainedPanelControllerGroup( ToolbarController, SelectedObjectController), }); ConfigureToolbarCollapse(layout, (UiCollapsibleFrame)handle.OuterFrame, root.Height); Console.WriteLine("[D.5.1] retail toolbar window from LayoutDesc importer (0x21000016)."); } private (uint[] Peace, uint[] War, uint[]? Empty) LoadToolbarDigits() { uint[]? peace = null, war = null, empty = null; lock (_bindings.Assets.DatLock) { var layout = _bindings.Assets.Dats.Get(0x21000037u); if (layout is not null && layout.Elements.TryGetValue(0x10000346u, out var composite) && composite.Children.TryGetValue(0x1000034Au, out var number) && number.StateDesc?.Properties is { } props) { peace = ReadDataIds(props, 0x10000042u); war = ReadDataIds(props, 0x10000043u); } if (layout is not null && layout.Elements.TryGetValue(0x10000341u, out var emptyComposite) && emptyComposite.Children.TryGetValue(0x1000034Au, out var emptyNumber) && emptyNumber.StateDesc?.Properties is { } emptyProps) empty = ReadDataIds(emptyProps, 0x1000005Eu); } peace ??= [ 0x0600109Eu, 0x0600109Fu, 0x060010A0u, 0x060010A1u, 0x060010A2u, 0x060010A3u, 0x060010A4u, 0x060010A5u, 0x060010A6u, ]; war ??= [ 0x06001ACCu, 0x06001ACDu, 0x06001ACEu, 0x06001ACFu, 0x06001AD0u, 0x06001AD1u, 0x06001AD2u, 0x06001AD3u, 0x06001AD4u, ]; Console.WriteLine($"[D.5.1] toolbar digit arrays ready: peace={peace.Length}, war={war.Length}, empty={empty?.Length ?? 0} entries."); return (peace, war, empty); } private static uint[]? ReadDataIds( IReadOnlyDictionary properties, uint key) { if (!properties.TryGetValue(key, out var raw) || raw is not DatReaderWriter.Types.ArrayBaseProperty array) return null; var result = new uint[array.Value.Count]; for (int i = 0; i < array.Value.Count; i++) if (array.Value[i] is DatReaderWriter.Types.DataIdBaseProperty dataId) result[i] = dataId.Value; return result; } private static void ConfigureToolbarCollapse( ImportedLayout layout, UiCollapsibleFrame frame, float contentHeight) { uint[] ids = [ 0x100006B6u, 0x100006B7u, 0x100006B8u, 0x100006B9u, 0x100006BAu, 0x100006BBu, 0x100006BCu, 0x100006BDu, 0x100006BEu, 0x100006BFu, 0x100006C0u, ]; var row = new List(); float top = float.MaxValue; foreach (uint id in ids) if (layout.FindElement(id) is { } element) { row.Add(element); top = Math.Min(top, element.Top); } if (row.Count == 0) return; const int border = RetailChromeSprites.Border; frame.CollapsedHeight = top + 2 * border; frame.ExpandedHeight = contentHeight + 2 * border; frame.SecondRow = row; frame.Resizable = true; frame.ResizableEdges = ResizeEdges.Bottom; frame.MinHeight = frame.CollapsedHeight; frame.MaxHeight = frame.ExpandedHeight; } private void MountCharacter() { ImportedLayout? layout = Import(0x2100002Eu); if (layout is null) { Console.WriteLine("[D.2b-C] character: LayoutDesc 0x2100002E not found."); return; } CharacterSheetProvider provider = _bindings.Character.Provider; CharacterStatController.Bind( layout, provider.BuildSheet, _bindings.Assets.DefaultFont, _bindings.Assets.ResolveFont(0x40000001u) ?? _bindings.Assets.DefaultFont, _bindings.Assets.ResolveSprite, provider.HandleRaiseRequest, () => CloseWindow(WindowNames.Character)); RetailWindowFrame.Mount(Host.Root, layout.Root, _bindings.Assets.ResolveSprite, new RetailWindowFrame.Options { WindowName = WindowNames.Character, Chrome = RetailWindowChrome.NineSlice, Left = 540f, Top = 18f, MaxHeight = 760f, ResizeX = false, ResizeY = true, ResizableEdges = ResizeEdges.Bottom, Visible = false, ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom, ContentClickThrough = false, }); Console.WriteLine("[D.2b-C] retail character window from LayoutDesc importer (0x2100002E)."); } private void MountPlugins() { if (_bindings.Plugins is null) return; foreach (var panel in _bindings.Plugins.Drain()) { try { string xml = File.ReadAllText(panel.MarkupPath); UiElement element = MarkupDocument.Build( xml, panel.Binding, _bindings.Assets.ResolveSprite, _bindings.Assets.Controls); Host.Root.AddChild(element); Console.WriteLine($"[D.2b] plugin UI panel loaded: {panel.MarkupPath}"); } catch (Exception ex) { Console.WriteLine($"[D.2b] plugin UI panel '{panel.MarkupPath}' failed to load: {ex.Message}"); } } } private void MountInventory() { ImportedLayout? layout = Import(0x21000023u); if (layout is null) { Console.WriteLine("[D.2b-B] inventory: LayoutDesc 0x21000023 not found."); return; } UiElement root = layout.Root; RetailWindowHandle handle = RetailWindowFrame.Mount( Host.Root, root, _bindings.Assets.ResolveSprite, new RetailWindowFrame.Options { WindowName = WindowNames.Inventory, Chrome = RetailWindowChrome.NineSlice, Left = root.Left, Top = root.Top, ContentWidth = root.Width, ContentHeight = root.Height, Visible = false, ResizeX = false, ResizeY = true, ResizableEdges = ResizeEdges.Bottom, // The old 560px cap left a previously expanded window permanently // parked at its maximum. Let it grow to the available screen height. MaxHeight = Math.Max( root.Height + 2f * RetailChromeSprites.Border, Host.Root.Height - root.Top), ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom, }); uint contents, sideBag, mainPack; PaperdollClickMap? paperdollClickMap; lock (_bindings.Assets.DatLock) { contents = ItemListCellTemplate.ResolveEmptySprite(_bindings.Assets.Dats, 0x21000021u, 0x100001C6u); sideBag = ItemListCellTemplate.ResolveEmptySprite(_bindings.Assets.Dats, 0x21000022u, 0x100001CAu); mainPack = ItemListCellTemplate.ResolveEmptySprite(_bindings.Assets.Dats, 0x21000022u, 0x100001C9u); paperdollClickMap = PaperdollClickMap.Load(_bindings.Assets.Dats); } InventoryRuntimeBindings b = _bindings.Inventory; Action? notifyMergeAttempt = ToolbarController is null ? null : ToolbarController.ReplaceFullyMergedShortcut; InventoryController inventory = InventoryController.Bind( layout, b.Objects, b.PlayerGuid, b.ResolveIcon, b.Strength, b.Selection, _bindings.Assets.DefaultFont, _bindings.Character.Provider.CharacterName, contents, sideBag, mainPack, b.SendUse, b.SendNoLongerViewing, b.SendPutItemInContainer, b.SendStackableSplitToContainer, b.SendStackableMerge, notifyMergeAttempt, b.ItemInteraction, () => CloseWindow(WindowNames.Inventory), StackSplitQuantity); PaperdollController paperdoll = PaperdollController.Bind( layout, b.Objects, b.PlayerGuid, b.ResolveIcon, b.Selection, b.SendWield, contents, _bindings.Assets.DefaultFont, b.ItemInteraction, paperdollClickMap); Host.WindowManager.AttachController( WindowNames.Inventory, new RetainedPanelControllerGroup(inventory, paperdoll)); PaperdollViewportWidget = layout.FindElement(0x100001D5u) as UiViewport; InventoryFrame = (UiNineSlicePanel)handle.OuterFrame; Console.WriteLine("[D.2b-B] retail inventory window from LayoutDesc importer (0x21000023)."); } public void Dispose() { if (_disposed) return; _disposed = true; _persistence?.Dispose(); Host.WindowManager.WindowVisibilityChanged -= OnWindowVisibilityChanged; Host.Dispose(); } }