using System.Collections.Generic; using AcDream.App.Plugins; using AcDream.App.Combat; using AcDream.App.Input; using AcDream.App.Rendering; using AcDream.App.Spells; using AcDream.App.UI.Layout; using AcDream.App.UI.Testing; using AcDream.Core.Combat; using AcDream.Core.Items; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Selection; using AcDream.Core.Spells; 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 CombatRuntimeBindings( CombatState State, CombatAttackController Attacks, Func Gameplay, Action SetGameplay); public sealed record MagicRuntimeBindings( Spellbook Spellbook, SpellCastingController Casting, ClientObjectTable Objects, Func PlayerGuid, IReadOnlyDictionary Components, Func ResolveIcon, Func ResolveDragIcon, Func ResolveSpellIcon, Func ResolveComponentIcon, SelectionState Selection, Func SpellLevel, Action SelectObject, Action UseItem, Action AddFavorite, Action RemoveFavorite, Action SendSpellbookFilter, Action RemoveSpell, Action SetDesiredComponent, Func ServerTime); public sealed record JumpPowerbarRuntimeBindings(Func Snapshot); public sealed record FpsRuntimeBindings( Func FramesPerSecond, Func DegradeMultiplier, Func IsVisible); public sealed record IndicatorRuntimeBindings( Spellbook Spellbook, ClientObjectTable Objects, Func PlayerGuid, Func Strength, Func LinkStatus, Func CurrentTime, Action RequestLinkStatusPing, Action EndCharacterSession); public sealed record ToolbarRuntimeBindings( ClientObjectTable Objects, Func> Shortcuts, Func ResolveIcon, Func ResolveDragIcon, 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 ResolveDragIcon, Func Strength, Action? SendUse, Action? SendPutItemInContainer, Action? SendStackableSplitToContainer, Action? SendStackableMerge, ItemInteractionController ItemInteraction, SelectionState Selection); public sealed record ExternalContainerRuntimeBindings( ExternalContainerState State, ClientObjectTable Objects, Func ResolveIcon, Func ResolveDragIcon, ItemInteractionController ItemInteraction, SelectionState Selection, Action SendUse, Action SendPutItemInContainer, Action SendStackableSplitToContainer, Func IsWithinUseRange); 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 ConfirmationRuntimeBindings( Action SendResponse); public sealed record RetailUiRuntimeBindings( UiHost Host, RetailUiAssets Assets, VitalsRuntimeBindings Vitals, ChatRuntimeBindings Chat, RadarRuntimeBindings Radar, CombatRuntimeBindings Combat, MagicRuntimeBindings Magic, JumpPowerbarRuntimeBindings JumpPowerbar, FpsRuntimeBindings Fps, VividTargetRuntimeBindings VividTarget, IndicatorRuntimeBindings Indicators, ToolbarRuntimeBindings Toolbar, CharacterRuntimeBindings Character, InventoryRuntimeBindings Inventory, ExternalContainerRuntimeBindings ExternalContainer, RetailUiCursorBindings Cursor, ConfirmationRuntimeBindings Confirmations, 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 readonly RetailPanelUiController _panelUi; private GameplayConfirmationController? _gameplayConfirmationController; private RetailItemConfirmationController? _itemConfirmationController; private RetailSkillTrainingConfirmationController? _skillTrainingConfirmationController; private UiShortcutDigitGraphics? _shortcutDigitGraphics; private VividTargetIndicatorController? _vividTargetIndicator; private bool _disposed; private RetailUiRuntime(RetailUiRuntimeBindings bindings) { _bindings = bindings; _panelUi = new RetailPanelUiController( bindings.Host.IsWindowVisible, bindings.Host.ShowWindow, bindings.Host.HideWindow); MountFpsDisplay(); MountVividTargetIndicator(); MountVitals(); MountRadar(); MountChat(); MountToolbar(); MountCombat(); MountSpellbook(); MountEffects(); MountIndicatorDetailPanels(); MountIndicators(); MountJumpPowerbar(); MountDialogFactory(); MountCharacter(); MountPlugins(); MountInventory(); MountExternalContainer(); Host.WindowManager.WindowVisibilityChanged += OnWindowVisibilityChanged; BindToolbarPanelButtons(); SyncToolbarWindowButtons(); if (bindings.Persistence is { } persistence) { _persistence = new RetailWindowLayoutPersistence( Host.WindowManager, persistence.Store, persistence.CharacterKey, persistence.ScreenSize, stateManagedVisibilityWindows: [ WindowNames.Combat, WindowNames.JumpPowerbar, WindowNames.ExternalContainer, ]); } 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, text => ChatCommandRouter.Submit( text, bindings.Chat.ViewModel, bindings.Chat.CommandBus(), ChatChannelKind.Say)); } } 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 CombatUiController? CombatUiController { get; private set; } public SpellcastingUiController? SpellcastingUiController { get; private set; } public SpellbookWindowController? SpellbookWindowController { get; private set; } public EffectsUiController? PositiveEffectsController { get; private set; } public EffectsUiController? NegativeEffectsController { get; private set; } public LinkStatusUiController? LinkStatusUiController { get; private set; } public VitaeUiController? VitaeUiController { get; private set; } public MiniGameUiController? MiniGameUiController { get; private set; } public IndicatorBarController? IndicatorBarController { get; private set; } public JumpPowerbarController? JumpPowerbarController { get; private set; } public RetailFpsController? FpsController { get; private set; } public SelectedObjectController? SelectedObjectController { get; private set; } public UiViewport? PaperdollViewportWidget { get; private set; } public UiNineSlicePanel? InventoryFrame { get; private set; } public InventoryController? InventoryPanelController { get; private set; } public RetailDialogFactory? DialogFactory { get; private set; } public ExternalContainerController? ExternalContainerController { 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) { FpsController?.Tick(); _vividTargetIndicator?.Tick(); SpellbookWindowController?.Tick(); SpellcastingUiController?.Tick(); PositiveEffectsController?.Tick(); NegativeEffectsController?.Tick(); LinkStatusUiController?.Tick(); IndicatorBarController?.Tick(); JumpPowerbarController?.Tick(); SelectedObjectController?.Tick(deltaSeconds); ExternalContainerController?.Tick(); DialogFactory?.Tick(); 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) { if (SpellcastingUiController?.Handle(action) == true) return true; if (action == AcDream.UI.Abstractions.Input.InputAction.ToggleSpellbookPanel) { OpenSpellbook(SpellbookWindowPage.Spells); return true; } if (action == AcDream.UI.Abstractions.Input.InputAction.ToggleSpellComponentsPanel) { OpenSpellbook(SpellbookWindowPage.Components); return true; } return ToolbarInputController?.Handle(action) == true; } private void OpenSpellbook(SpellbookWindowPage page) { bool visible = Host.IsWindowVisible(WindowNames.Spellbook); if (visible && SpellbookWindowController?.CurrentPage == page) CloseWindow(WindowNames.Spellbook); else { SpellbookWindowController?.ShowPage(page); _panelUi.SetPanelVisibility(RetailPanelCatalog.Magic, visible: true); } } public bool HandleConfirmationRequest(GameEvents.CharacterConfirmationRequest request) => _gameplayConfirmationController?.HandleRequest(request) == true; public bool HandleConfirmationDone(GameEvents.CharacterConfirmationDone done) => _gameplayConfirmationController?.HandleDone(done) == true; public uint ShowConfirmation(string message, Action completed) { ArgumentNullException.ThrowIfNull(message); ArgumentNullException.ThrowIfNull(completed); return DialogFactory?.MakeConfirmation( message, data => completed(data.GetBoolean(RetailDialogProperty.ConfirmationResult))) ?? 0u; } 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 void SaveLayout() => _persistence?.SaveAll(); public void SaveNamedLayout(string profileName) => _persistence?.SaveNamed(profileName); public void RestoreNamedLayout(string profileName) => _persistence?.RestoreNamed(profileName); public bool ToggleWindow(string name) => RetailPanelCatalog.TryGetPanelId(name, out uint panelId) ? _panelUi.TogglePanel(panelId) : Host.ToggleWindow(name); public void CloseWindow(string name) { if (RetailPanelCatalog.TryGetPanelId(name, out uint panelId)) _panelUi.SetPanelVisibility(panelId, visible: false); else Host.HideWindow(name); } public void SyncToolbarWindowButtons() { if (ToolbarController is null) return; foreach (var (panelId, windowName) in RetailPanelCatalog.ToolbarPanels) 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) { _panelUi.ObserveWindowVisibility(windowName, 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 ImportedLayout? Import(uint layoutId, uint rootElementId) { lock (_bindings.Assets.DatLock) return LayoutImporter.Import( _bindings.Assets.Dats, layoutId, rootElementId, _bindings.Assets.ResolveSprite, _bindings.Assets.DefaultFont, _bindings.Assets.ResolveFont); } private void MountFpsDisplay() { ImportedLayout? layout = Import( RetailFpsController.LayoutId, RetailFpsController.DisplayElementId); if (layout is null) { Console.WriteLine("[D.2b] FPS display: SmartBox element 0x10000047 not found."); return; } FpsRuntimeBindings b = _bindings.Fps; FpsController = RetailFpsController.Bind( layout, b.FramesPerSecond, b.DegradeMultiplier, b.IsVisible); if (FpsController is null) { Console.WriteLine("[D.2b] FPS display: SmartBox element is not UIElement_Text."); return; } // SmartBox HUD content is mounted before movable windows, matching retail's // screen-layer ordering and ensuring ordinary panels can cover the readout. Host.Root.AddChild(layout.Root); Console.WriteLine("[D.2b] retail FPS display from SmartBox LayoutDesc 0x2100000F."); } private void MountVividTargetIndicator() { _vividTargetIndicator = VividTargetIndicatorController.Mount( Host.Root, _bindings.Assets, _bindings.VividTarget); Console.WriteLine(_vividTargetIndicator is null ? "[D.2b] vivid target indicator DAT surfaces unavailable." : "[D.2b] vivid target indicator mounted from client-enum category 0x10000009."); } 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); var strings = new DatStringResolver(_bindings.Assets.Dats); layout = info is null ? null : LayoutImporter.Build( info, _bindings.Assets.ResolveSprite, _bindings.Assets.DefaultFont, _bindings.Assets.ResolveFont, strings.Resolve); } 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, // The imported 9px top Resizebar is deliberately removed by // ChatWindowController. Its replacement top chrome is the move // affordance; only the other three wrapper edges resize. ResizableEdges = ResizeEdges.Left | ResizeEdges.Right | ResizeEdges.Bottom, 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; } UiShortcutDigitGraphics shortcutDigits = LoadShortcutDigitGraphics(); ToolbarRuntimeBindings b = _bindings.Toolbar; ToolbarController = Layout.ToolbarController.Bind( layout, b.Objects, b.Shortcuts, b.ResolveIcon, b.UseItem, b.Combat, shortcutDigits.RegularDigits, shortcutDigits.GhostedDigits, shortcutDigits.EmptyDigits, 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, ammoFont: _bindings.Assets.DefaultFont, dragIconIds: b.ResolveDragIcon); 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 void MountCombat() { ElementInfo? info; ImportedLayout? layout; CombatUiLabels? labels; uint favoriteEmptySprite; lock (_bindings.Assets.DatLock) { info = LayoutImporter.ImportInfos(_bindings.Assets.Dats, CombatUiController.LayoutId); favoriteEmptySprite = info is null ? 0u : ItemListCellTemplate.ResolveEmptySprite( _bindings.Assets.Dats, info, SpellcastingUiController.FavoriteListId); var strings = new DatStringResolver(_bindings.Assets.Dats); layout = info is null ? null : LayoutImporter.Build( info, _bindings.Assets.ResolveSprite, _bindings.Assets.DefaultFont, _bindings.Assets.ResolveFont, strings.Resolve); labels = info is null ? null : CombatUiLabels.Resolve(info, strings); } if (layout is null || labels is null) { Console.WriteLine("[M2] combat: LayoutDesc 0x21000073 not found."); return; } CombatUiController? controller = Layout.CombatUiController.Bind( layout, _bindings.Combat.State, _bindings.Combat.Attacks, _bindings.Combat.Gameplay, _bindings.Combat.SetGameplay, labels, visible => { if (visible) Host.ShowWindow(WindowNames.Combat); else Host.HideWindow(WindowNames.Combat); }); if (controller is null) { Console.WriteLine("[M2] combat: required controls missing in LayoutDesc 0x21000073."); return; } SpellcastingUiController? spellcasting = Layout.SpellcastingUiController.Bind( layout, _bindings.Magic.Spellbook, _bindings.Magic.Casting, _bindings.Magic.Objects, _bindings.Magic.PlayerGuid, _bindings.Magic.ResolveSpellIcon, item => _bindings.Magic.ResolveDragIcon( item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects), _bindings.Magic.UseItem, _bindings.Magic.Selection, _bindings.Magic.AddFavorite, _bindings.Magic.RemoveFavorite, LoadShortcutDigitGraphics(), favoriteEmptySprite); if (spellcasting is null) Console.WriteLine("[M3] spellcasting: required controls missing in LayoutDesc 0x21000073."); CombatUiController = controller; SpellcastingUiController = spellcasting; IRetainedPanelController controllerOwner = spellcasting is null ? controller : new RetainedPanelControllerGroup(controller, spellcasting); UiElement root = layout.Root; RetailWindowFrame.Mount( Host.Root, root, _bindings.Assets.ResolveSprite, new RetailWindowFrame.Options { WindowName = WindowNames.Combat, Chrome = RetailWindowChrome.Imported, Left = Math.Max(0f, (Host.Root.Width - root.Width) * 0.5f), Top = Math.Max(0f, Host.Root.Height - root.Height - 10f), Visible = false, Resizable = false, ResizeX = false, ResizeY = false, ConstrainDragToParent = true, ContentClickThrough = false, Controller = controllerOwner, }); controller.SyncVisibility(); Console.WriteLine(spellcasting is null ? "[M2] retail combat from LayoutDesc 0x21000073; magic binding unavailable." : $"[M3] retail combat + spell bar from LayoutDesc 0x21000073; " + $"favorite empty=0x{favoriteEmptySprite:X8}."); } private void MountSpellbook() { ImportedLayout? layout; SpellbookRowStyle? rowStyle; ComponentBookTemplateFactory? componentTemplates; lock (_bindings.Assets.DatLock) { layout = LayoutImporter.Import( _bindings.Assets.Dats, SpellbookWindowController.LayoutId, SpellbookWindowController.RootId, _bindings.Assets.ResolveSprite, _bindings.Assets.DefaultFont, _bindings.Assets.ResolveFont); rowStyle = SpellbookRowStyle.TryLoad(_bindings.Assets.Dats); componentTemplates = ComponentBookTemplateFactory.TryLoad( _bindings.Assets.Dats, _bindings.Assets.ResolveSprite, _bindings.Assets.DefaultFont, _bindings.Assets.ResolveFont); } if (layout is null || rowStyle is null || componentTemplates is null) { Console.WriteLine( "[M3] spellbook: layout, spell row, or component list templates not found."); return; } UiDatFont? rowFont = rowStyle.Value.FontDid == 0u ? _bindings.Assets.DefaultFont : _bindings.Assets.ResolveFont(rowStyle.Value.FontDid) ?? _bindings.Assets.DefaultFont; SpellbookWindowController? controller = Layout.SpellbookWindowController.Bind( layout, _bindings.Magic.Spellbook, _bindings.Magic.Objects, _bindings.Magic.PlayerGuid, _bindings.Magic.Components, _bindings.Magic.Selection, _bindings.Magic.ResolveSpellIcon, _bindings.Magic.ResolveComponentIcon, _bindings.Magic.SpellLevel, _bindings.Magic.SelectObject, spellId => SpellcastingUiController?.AddFavorite(spellId), _bindings.Magic.SendSpellbookFilter, _bindings.Magic.RemoveSpell, (message, completed) => ShowConfirmation(message, completed), _bindings.Magic.SetDesiredComponent, () => CloseWindow(WindowNames.Spellbook), componentTemplates, rowStyle.Value, rowFont); if (controller is null) { Console.WriteLine("[M3] spellbook: required controls missing in LayoutDesc 0x21000034."); return; } SpellbookWindowController = controller; UiElement root = layout.Root; RetailWindowHandle handle = RetailWindowFrame.Mount( Host.Root, root, _bindings.Assets.ResolveSprite, new RetailWindowFrame.Options { WindowName = WindowNames.Spellbook, // LayoutDesc 0x21000034 is the 300x600 content surface. Like the // Attributes/Skills window, retail supplies the outer gold frame // through the shared window chrome rather than this root element. Chrome = RetailWindowChrome.NineSlice, Left = 18f, Top = 18f, Visible = false, ResizeX = false, ResizeY = true, ResizableEdges = ResizeEdges.Bottom, ConstrainDragToParent = true, ConstrainResizeToParent = true, ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom, ContentClickThrough = false, Controller = controller, }); _panelUi.RegisterMainPanel( RetailPanelCatalog.Magic, WindowNames.Spellbook, handle); Console.WriteLine("[M3] retail spellbook/component book from LayoutDesc 0x21000034."); } private void MountEffects() { MountEffectsInstance(positive: true); MountEffectsInstance(positive: false); } private void MountEffectsInstance(bool positive) { uint rootId = positive ? EffectsUiController.PositiveRootId : EffectsUiController.NegativeRootId; ElementInfo? rootInfo; ImportedLayout? layout; EffectRowTemplateFactory? rowTemplates; string selectPrompt; lock (_bindings.Assets.DatLock) { rootInfo = LayoutImporter.ImportInfos( _bindings.Assets.Dats, EffectsUiController.LayoutId, rootId); layout = rootInfo is null ? null : LayoutImporter.Build( rootInfo, _bindings.Assets.ResolveSprite, _bindings.Assets.DefaultFont, _bindings.Assets.ResolveFont, new DatStringResolver(_bindings.Assets.Dats).Resolve); rowTemplates = EffectRowTemplateFactory.TryLoad( _bindings.Assets.Dats, _bindings.Assets.ResolveSprite, _bindings.Assets.DefaultFont, _bindings.Assets.ResolveFont); var strings = new DatStringResolver(_bindings.Assets.Dats); selectPrompt = strings.Resolve( 0x23000001u, DatStringResolver.ComputeHash("ID_Effects_Info_SelectASpell")) ?? "SELECT A SPELL"; } if (rootInfo is null || layout is null || rowTemplates is null) { Console.WriteLine($"[M3] effects: root or row template for 0x{rootId:X8} not found."); return; } EffectsUiController? controller = Layout.EffectsUiController.Bind( layout, _bindings.Magic.Spellbook, positive, _bindings.Magic.ServerTime, _bindings.Assets.ResolveSprite, _bindings.Magic.ResolveSpellIcon, rowTemplates, selectPrompt, close: () => CloseWindow( positive ? WindowNames.PositiveEffects : WindowNames.NegativeEffects)); if (controller is null) { Console.WriteLine($"[M3] effects: list missing under root 0x{rootId:X8}."); return; } if (positive) PositiveEffectsController = controller; else NegativeEffectsController = controller; UiElement root = layout.Root; RetailWindowHandle handle = RetailWindowFrame.Mount( Host.Root, root, _bindings.Assets.ResolveSprite, new RetailWindowFrame.Options { WindowName = positive ? WindowNames.PositiveEffects : WindowNames.NegativeEffects, Chrome = RetailWindowChrome.NineSlice, Left = Math.Max(0f, Host.Root.Width - root.Width - 12f), Top = 18f, Visible = false, ResizeX = false, ResizeY = true, ResizableEdges = ResizeEdges.Bottom, ConstrainDragToParent = true, ConstrainResizeToParent = true, ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right | AnchorEdges.Bottom, ContentClickThrough = false, DrawChromeCenter = !AuthorsFullPanelCenter(rootInfo), Controller = controller, }); _panelUi.RegisterMainPanel( positive ? RetailPanelCatalog.PositiveEffects : RetailPanelCatalog.NegativeEffects, positive ? WindowNames.PositiveEffects : WindowNames.NegativeEffects, handle, rootInfo.TryGetEffectiveBool( RetailPanelUiController.RestorePreviousPropertyId, out bool restorePrevious) && restorePrevious); } private void MountIndicatorDetailPanels() { MountLinkStatusPanel(); MountVitaePanel(); MountMiniGamePanel(); MountCharacterInformationPanel(); } private void MountCharacterInformationPanel() { ElementInfo? rootInfo; ImportedLayout? layout; CharacterInfoStrings strings; lock (_bindings.Assets.DatLock) { rootInfo = LayoutImporter.ImportInfos( _bindings.Assets.Dats, CharacterController.LayoutId, CharacterController.RootId); var resolver = new DatStringResolver(_bindings.Assets.Dats); layout = rootInfo is null ? null : LayoutImporter.Build( rootInfo, _bindings.Assets.ResolveSprite, _bindings.Assets.DefaultFont, _bindings.Assets.ResolveFont, resolver.Resolve); strings = CharacterInfoStrings.FromDat(key => resolver.ResolveAll( 0x23000001u, DatStringResolver.ComputeHash(key))); } if (rootInfo is null || layout is null) return; CharacterController.Bind( layout, _bindings.Character.Provider.BuildSheet, _bindings.Assets.DefaultFont, () => CloseWindow(WindowNames.CharacterInformation), strings); RegisterIndicatorDetailPanel( RetailPanelCatalog.CharacterInformation, WindowNames.CharacterInformation, rootInfo, layout.Root, controller: null); } private void MountLinkStatusPanel() { ElementInfo? rootInfo; ImportedLayout? layout; LinkStatusStrings strings; lock (_bindings.Assets.DatLock) { rootInfo = LayoutImporter.ImportInfos( _bindings.Assets.Dats, LinkStatusUiController.LayoutId, LinkStatusUiController.RootId); var resolver = new DatStringResolver(_bindings.Assets.Dats); layout = rootInfo is null ? null : LayoutImporter.Build( rootInfo, _bindings.Assets.ResolveSprite, _bindings.Assets.DefaultFont, _bindings.Assets.ResolveFont, resolver.Resolve); LinkStatusStrings fallback = LinkStatusStrings.English; strings = new LinkStatusStrings( resolver.Resolve(0x23000001u, 0x0D632F7Fu) ?? fallback.Description, resolver.Resolve(0x23000001u, 0x038FABF3u) ?? fallback.Legend, resolver.Resolve(0x23000001u, 0x05481084u) ?? fallback.DisconnectWarning, resolver.Resolve(0x23000001u, 0x0CADBA93u) ?? fallback.PacketLossPrefix, resolver.Resolve(0x23000001u, 0x0D6485F7u) ?? fallback.PingPrefix); } if (rootInfo is null || layout is null) return; LinkStatusUiController? controller = Layout.LinkStatusUiController.Bind( layout, _bindings.Indicators.LinkStatus, _bindings.Indicators.CurrentTime, _bindings.Indicators.RequestLinkStatusPing, strings, () => CloseWindow(WindowNames.LinkStatus)); if (controller is null) return; LinkStatusUiController = controller; RegisterIndicatorDetailPanel( RetailPanelCatalog.LinkStatus, WindowNames.LinkStatus, rootInfo, layout.Root, controller); } private void MountVitaePanel() { ElementInfo? rootInfo; ImportedLayout? layout; VitaeStrings strings; lock (_bindings.Assets.DatLock) { rootInfo = LayoutImporter.ImportInfos( _bindings.Assets.Dats, VitaeUiController.LayoutId, VitaeUiController.RootId); var resolver = new DatStringResolver(_bindings.Assets.Dats); layout = rootInfo is null ? null : LayoutImporter.Build( rootInfo, _bindings.Assets.ResolveSprite, _bindings.Assets.DefaultFont, _bindings.Assets.ResolveFont, resolver.Resolve); VitaeStrings fallback = VitaeStrings.English; strings = new VitaeStrings( Resolve("ID_Vitae_Text_Full", 0, fallback.FullStrength), Resolve("ID_Vitae_Text_Vitae", 0, fallback.LostPrefix), Resolve("ID_Vitae_Text_Vitae", 1, fallback.LostSuffix), Resolve("ID_Vitae_Text_Skills", 0, fallback.SkillsPrefix), Resolve("ID_Vitae_Text_Skills", 1, fallback.SkillsSuffix), Resolve("ID_Vitae_Text_Experience", 0, fallback.RecoveryPrefix), Resolve("ID_Vitae_Text_Experience", 1, fallback.RecoverySuffix)); string Resolve(string key, int token, string fallbackValue) => resolver.Resolve( 0x23000001u, DatStringResolver.ComputeHash(key), token) ?? fallbackValue; } if (rootInfo is null || layout is null) return; VitaeUiController? controller = Layout.VitaeUiController.Bind( layout, _bindings.Indicators.Spellbook, _bindings.Indicators.Objects, _bindings.Indicators.PlayerGuid, strings, () => CloseWindow(WindowNames.Vitae)); if (controller is null) return; VitaeUiController = controller; RegisterIndicatorDetailPanel( RetailPanelCatalog.Vitae, WindowNames.Vitae, rootInfo, layout.Root, controller); } private void MountMiniGamePanel() { ElementInfo? rootInfo; ImportedLayout? layout; lock (_bindings.Assets.DatLock) { rootInfo = LayoutImporter.ImportInfos( _bindings.Assets.Dats, MiniGameUiController.LayoutId, MiniGameUiController.RootId); layout = rootInfo is null ? null : LayoutImporter.Build( rootInfo, _bindings.Assets.ResolveSprite, _bindings.Assets.DefaultFont, _bindings.Assets.ResolveFont, new DatStringResolver(_bindings.Assets.Dats).Resolve); } if (rootInfo is null || layout is null) return; MiniGameUiController? controller = Layout.MiniGameUiController.Bind( layout, () => CloseWindow(WindowNames.MiniGame)); if (controller is null) return; MiniGameUiController = controller; RegisterIndicatorDetailPanel( RetailPanelCatalog.MiniGame, WindowNames.MiniGame, rootInfo, layout.Root, controller); } private void RegisterIndicatorDetailPanel( uint panelId, string windowName, ElementInfo rootInfo, UiElement root, IRetainedPanelController? controller) { RetailWindowHandle handle = RetailWindowFrame.Mount( Host.Root, root, _bindings.Assets.ResolveSprite, new RetailWindowFrame.Options { WindowName = windowName, Chrome = RetailWindowChrome.NineSlice, Left = 18f, Top = 18f, Visible = false, ResizeX = false, ResizeY = true, ResizableEdges = ResizeEdges.Bottom, ConstrainDragToParent = true, ConstrainResizeToParent = true, ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right | AnchorEdges.Bottom, ContentClickThrough = false, DrawChromeCenter = !AuthorsFullPanelCenter(rootInfo), Controller = controller, }); _panelUi.RegisterMainPanel( panelId, windowName, handle, rootInfo.TryGetEffectiveBool( RetailPanelUiController.RestorePreviousPropertyId, out bool restorePrevious) && restorePrevious); } /// /// Retail's Vitae and Character Information children carry their own /// 0x06004CC2 body surface below the 25px title row. The standalone shared /// window contributes the border only in that case; painting the same /// translucent center twice makes the panel look opaque. /// internal static bool AuthorsFullPanelCenter(ElementInfo rootInfo) { const float RetailTitleHeight = 25f; return Visit(rootInfo); bool Visit(ElementInfo info) { bool coversBody = info.Width >= rootInfo.Width && info.Height >= rootInfo.Height - RetailTitleHeight; if (coversBody && info.StateMedia.Values.Any( media => media.File == RetailChromeSprites.CenterFill)) return true; return info.Children.Any(Visit); } } private void MountIndicators() { ImportedLayout? layout = Import(IndicatorBarController.LayoutId); if (layout is null) { Console.WriteLine("[UI] indicator bar: LayoutDesc 0x21000071 not found."); return; } IndicatorRuntimeBindings b = _bindings.Indicators; IndicatorBarController? controller = Layout.IndicatorBarController.Bind( layout, new IndicatorBarBindings( b.Spellbook, b.Objects, b.PlayerGuid, b.Strength, b.LinkStatus, b.CurrentTime, panelId => _panelUi.TogglePanel(panelId), RequestEndCharacterSession)); if (controller is null) { Console.WriteLine("[UI] indicator bar: one or more authored controls are missing."); return; } IndicatorBarController = controller; RetailWindowFrame.Mount( Host.Root, layout.Root, _bindings.Assets.ResolveSprite, new RetailWindowFrame.Options { WindowName = WindowNames.Indicators, Chrome = RetailWindowChrome.Imported, Left = 10f, Top = 96f, Visible = true, Resizable = false, ResizeX = false, ResizeY = false, ConstrainDragToParent = true, ContentClickThrough = false, Controller = controller, }); Console.WriteLine("[UI] retail seven-control indicator bar from LayoutDesc 0x21000071."); } private void RequestEndCharacterSession() { const string fallback = "Are you sure you want to end this character session?"; string message; lock (_bindings.Assets.DatLock) { message = new DatStringResolver(_bindings.Assets.Dats).Resolve( 0x23000001u, DatStringResolver.ComputeHash("ID_Client_EndCharacterSessionConfirm")) ?? fallback; } ShowConfirmation(message, accepted => { if (accepted) _bindings.Indicators.EndCharacterSession(); }); } private void MountJumpPowerbar() { ElementInfo? info; ImportedLayout? layout; lock (_bindings.Assets.DatLock) { info = LayoutImporter.ImportInfos( _bindings.Assets.Dats, JumpPowerbarController.LayoutId); layout = info is null ? null : LayoutImporter.Build( info, _bindings.Assets.ResolveSprite, _bindings.Assets.DefaultFont, _bindings.Assets.ResolveFont); } if (layout is null) { Console.WriteLine("[D.6] jump powerbar: LayoutDesc 0x21000072 not found."); return; } JumpPowerbarController? controller = Layout.JumpPowerbarController.Bind( layout, _bindings.JumpPowerbar.Snapshot, visible => { if (visible) Host.ShowWindow(WindowNames.JumpPowerbar); else Host.HideWindow(WindowNames.JumpPowerbar); }); if (controller is null) { Console.WriteLine("[D.6] jump powerbar: required JumpMode meter missing in LayoutDesc 0x21000072."); return; } JumpPowerbarController = controller; UiElement root = layout.Root; RetailWindowFrame.Mount( Host.Root, root, _bindings.Assets.ResolveSprite, new RetailWindowFrame.Options { WindowName = WindowNames.JumpPowerbar, Chrome = RetailWindowChrome.Imported, Left = Math.Max(0f, (Host.Root.Width - root.Width) * 0.5f), Top = Math.Max(0f, Host.Root.Height - root.Height - 105f), Visible = false, DatConstraintSource = info, Resizable = true, ResizeX = true, ResizeY = true, ConstrainDragToParent = true, ContentClickThrough = false, Controller = controller, }); controller.SyncVisibility(); Console.WriteLine("[D.6] retail jump bar from gmFloatyPowerBarUI LayoutDesc 0x21000072."); } private void MountDialogFactory() { uint layoutId; lock (_bindings.Assets.DatLock) { // DialogFactory::CreateDialog_ @ 0x00477AD0 resolves the shared // catalog through GetDIDByEnum(2, 5). Each shown DialogInfo then // creates a fresh type-specific root from that catalog. layoutId = RetailDataIdResolver.Resolve(_bindings.Assets.Dats, 2u, 5u); } if (layoutId == 0u) { Console.WriteLine("[UI] retail dialog catalog could not be resolved."); return; } ImportedLayout? CreateLayout(RetailDialogType type) { uint rootElementId = RetailDialogFactory.RootElementId(type); if (rootElementId == 0u) return null; lock (_bindings.Assets.DatLock) { return LayoutImporter.Import( _bindings.Assets.Dats, layoutId, rootElementId, _bindings.Assets.ResolveSprite, _bindings.Assets.DefaultFont, _bindings.Assets.ResolveFont); } } DialogFactory = new RetailDialogFactory(Host.Root, CreateLayout); _gameplayConfirmationController = new GameplayConfirmationController( DialogFactory, _bindings.Confirmations.SendResponse); _itemConfirmationController = new RetailItemConfirmationController( DialogFactory, ItemInteraction); _skillTrainingConfirmationController = new RetailSkillTrainingConfirmationController(DialogFactory); Console.WriteLine( $"[UI] retail DialogFactory from LayoutDesc 0x{layoutId:X8}; confirmation root 0x15."); } private UiShortcutDigitGraphics LoadShortcutDigitGraphics() { if (_shortcutDigitGraphics is not null) return _shortcutDigitGraphics; uint[]? regular = null, ghosted = 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) { regular = ReadDataIds(props, 0x10000042u); ghosted = 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); } regular ??= [ 0x0600109Eu, 0x0600109Fu, 0x060010A0u, 0x060010A1u, 0x060010A2u, 0x060010A3u, 0x060010A4u, 0x060010A5u, 0x060010A6u, ]; ghosted ??= [ 0x06001ACCu, 0x06001ACDu, 0x06001ACEu, 0x06001ACFu, 0x06001AD0u, 0x06001AD1u, 0x06001AD2u, 0x06001AD3u, 0x06001AD4u, ]; _shortcutDigitGraphics = new UiShortcutDigitGraphics( regular, ghosted, empty); Console.WriteLine( $"[D.5.1] shared shortcut digits ready: regular={regular.Length}, " + $"ghosted={ghosted.Length}, empty={empty?.Length ?? 0}."); return _shortcutDigitGraphics; } 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, (request, completed) => HandleCharacterRaise(provider, request, completed), () => CloseWindow(WindowNames.Character)); RetailWindowHandle handle = RetailWindowFrame.Mount( Host.Root, layout.Root, _bindings.Assets.ResolveSprite, new RetailWindowFrame.Options { WindowName = WindowNames.Character, Chrome = RetailWindowChrome.NineSlice, Left = 540f, Top = 18f, ResizeX = false, ResizeY = true, ResizableEdges = ResizeEdges.Bottom, ConstrainResizeToParent = true, Visible = false, ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom, ContentClickThrough = false, }); _panelUi.RegisterMainPanel( RetailPanelCatalog.Character, WindowNames.Character, handle); Console.WriteLine("[D.2b-C] retail character window from LayoutDesc importer (0x2100002E)."); } private void HandleCharacterRaise( CharacterSheetProvider provider, CharacterStatController.RaiseRequest request, Action completed) { if (request.Kind != CharacterStatController.RaiseTargetKind.TrainSkill) { provider.HandleRaiseRequest(request); completed(); return; } RetailSkillTrainingConfirmationController confirmations = _skillTrainingConfirmationController ?? throw new InvalidOperationException( "The retail dialog factory must be mounted before the character panel."); confirmations.Request( request, provider.BuildSheet(), provider.HandleRaiseRequest, completed); } 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, ConstrainResizeToParent = true, ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom, }); _panelUi.RegisterMainPanel( RetailPanelCatalog.Inventory, WindowNames.Inventory, handle); uint contents, sideBag, mainPack; IReadOnlyDictionary paperdollEmptySprites; 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); paperdollEmptySprites = PaperdollSlotBackgrounds.ResolveEmptySprites(_bindings.Assets.Dats); 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.SendPutItemInContainer, b.SendStackableSplitToContainer, b.SendStackableMerge, notifyMergeAttempt, b.ItemInteraction, () => CloseWindow(WindowNames.Inventory), StackSplitQuantity, b.ResolveDragIcon); InventoryPanelController = inventory; PaperdollController paperdoll = PaperdollController.Bind( layout, b.Objects, b.PlayerGuid, b.ResolveIcon, b.Selection, b.ItemInteraction, contents, _bindings.Assets.DefaultFont, paperdollClickMap, b.ResolveDragIcon, paperdollEmptySprites); 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)."); } private void MountExternalContainer() { ImportedLayout? layout; lock (_bindings.Assets.DatLock) { layout = LayoutImporter.Import( _bindings.Assets.Dats, ExternalContainerController.LayoutId, ExternalContainerController.RootId, _bindings.Assets.ResolveSprite, _bindings.Assets.DefaultFont, _bindings.Assets.ResolveFont); } if (layout is null) { Console.WriteLine("[D.2b] external container: LayoutDesc 0x21000008 not found."); return; } UiElement root = layout.Root; RetailWindowHandle handle = RetailWindowFrame.Mount( Host.Root, root, _bindings.Assets.ResolveSprite, ExternalContainerController.CreateWindowOptions(root)); uint contentsEmpty; uint containerEmpty; lock (_bindings.Assets.DatLock) { contentsEmpty = ItemListCellTemplate.ResolveEmptySprite( _bindings.Assets.Dats, ExternalContainerController.LayoutId, ExternalContainerController.ContentsListId); containerEmpty = ItemListCellTemplate.ResolveEmptySprite( _bindings.Assets.Dats, ExternalContainerController.LayoutId, ExternalContainerController.ContainerListId); } ExternalContainerRuntimeBindings b = _bindings.ExternalContainer; ExternalContainerController = ExternalContainerController.Bind( layout, b.State, b.Objects, b.Selection, b.ItemInteraction, StackSplitQuantity, b.ResolveIcon, b.ResolveDragIcon, b.SendUse, b.SendPutItemInContainer, b.SendStackableSplitToContainer, b.IsWithinUseRange, handle, contentsEmpty, containerEmpty); Host.WindowManager.AttachController( WindowNames.ExternalContainer, ExternalContainerController); Console.WriteLine( "[D.2b] retail external-container strip mounted from LayoutDesc 0x21000008."); } public void Dispose() { if (_disposed) return; _disposed = true; _persistence?.Dispose(); Host.WindowManager.WindowVisibilityChanged -= OnWindowVisibilityChanged; _itemConfirmationController?.Dispose(); _gameplayConfirmationController?.Dispose(); DialogFactory?.Dispose(); _panelUi.Dispose(); Host.Dispose(); } }