Replace the single mutable confirmation service with retail's property-backed DialogFactory model: fresh DAT roots, context ids, queue groups, priority preemption, callback/close-notice ordering, and context cancellation. Route /die, server confirmation aborts, and guarded item use through focused semantic owners. Co-Authored-By: Codex <codex@openai.com>
933 lines
35 KiB
C#
933 lines
35 KiB
C#
using System.Collections.Generic;
|
|
using AcDream.App.Plugins;
|
|
using AcDream.App.Combat;
|
|
using AcDream.App.Input;
|
|
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<uint, (uint Texture, int Width, int Height)> ResolveSprite,
|
|
Func<uint, UiDatFont?> ResolveFont,
|
|
UiDatFont? DefaultFont,
|
|
BitmapFont? DebugFont,
|
|
ControlsIni Controls,
|
|
IconComposer Icons);
|
|
|
|
public sealed record VitalsRuntimeBindings(VitalsVM ViewModel);
|
|
|
|
public sealed record ChatRuntimeBindings(ChatVM ViewModel, Func<ICommandBus> CommandBus);
|
|
|
|
public sealed record RadarRuntimeBindings(
|
|
Func<UiRadarSnapshot> Snapshot,
|
|
SelectionState Selection,
|
|
Action<bool> SetUiLocked);
|
|
|
|
public sealed record CombatRuntimeBindings(
|
|
CombatState State,
|
|
CombatAttackController Attacks,
|
|
Func<GameplaySettings> Gameplay,
|
|
Action<GameplaySettings> SetGameplay);
|
|
|
|
public sealed record JumpPowerbarRuntimeBindings(Func<JumpChargeSnapshot> Snapshot);
|
|
|
|
public sealed record FpsRuntimeBindings(
|
|
Func<double> FramesPerSecond,
|
|
Func<double> DegradeMultiplier,
|
|
Func<bool> IsVisible);
|
|
|
|
public sealed record ToolbarRuntimeBindings(
|
|
ClientObjectTable Objects,
|
|
Func<IReadOnlyList<ShortcutEntry>> Shortcuts,
|
|
Func<ItemType, uint, uint, uint, uint, uint> ResolveIcon,
|
|
Func<ItemType, uint, uint, uint, uint, uint> ResolveDragIcon,
|
|
Action<uint> UseItem,
|
|
CombatState Combat,
|
|
ItemManaState ItemMana,
|
|
Action ToggleCombat,
|
|
ItemInteractionController ItemInteraction,
|
|
Action<ShortcutEntry>? SendAddShortcut,
|
|
Action<uint>? SendRemoveShortcut,
|
|
SelectionState Selection,
|
|
Action<Action<uint, float>> SubscribeHealthChanged,
|
|
Action<Action<uint, float>> UnsubscribeHealthChanged,
|
|
Func<uint, bool> IsHealthTarget,
|
|
Func<uint, string?> ResolveName,
|
|
Func<uint, float> HealthPercent,
|
|
Func<uint, bool> HasHealth,
|
|
Func<uint, uint> StackSize,
|
|
Action<uint> SendQueryHealth,
|
|
Action<uint> SendQueryItemMana,
|
|
Func<uint> PlayerGuid,
|
|
Action<uint, uint, int>? SendPutItemInContainer);
|
|
|
|
public sealed record CharacterRuntimeBindings(CharacterSheetProvider Provider);
|
|
|
|
public sealed record InventoryRuntimeBindings(
|
|
ClientObjectTable Objects,
|
|
Func<uint> PlayerGuid,
|
|
Func<ItemType, uint, uint, uint, uint, uint> ResolveIcon,
|
|
Func<ItemType, uint, uint, uint, uint, uint> ResolveDragIcon,
|
|
Func<int?> Strength,
|
|
Action<uint>? SendUse,
|
|
Action<uint>? SendNoLongerViewing,
|
|
Action<uint, uint, int>? SendPutItemInContainer,
|
|
Action<uint, uint, uint, uint>? SendStackableSplitToContainer,
|
|
Action<uint, uint, uint>? SendStackableMerge,
|
|
Action<uint, uint>? SendWield,
|
|
ItemInteractionController ItemInteraction,
|
|
SelectionState Selection);
|
|
|
|
public sealed record RetailUiPersistenceBindings(
|
|
SettingsStore Store,
|
|
Func<string> CharacterKey,
|
|
Func<(int Width, int Height)> ScreenSize);
|
|
|
|
public sealed record RetailUiProbeBindings(
|
|
bool Enabled,
|
|
string? ScriptPath,
|
|
bool DumpOnStart,
|
|
Action<string> Log);
|
|
|
|
public sealed record RetailUiCursorBindings(
|
|
CursorFeedbackController Feedback,
|
|
RetailCursorManager Manager);
|
|
|
|
public sealed record ConfirmationRuntimeBindings(
|
|
Action<uint, uint, bool> SendResponse);
|
|
|
|
public sealed record RetailUiRuntimeBindings(
|
|
UiHost Host,
|
|
RetailUiAssets Assets,
|
|
VitalsRuntimeBindings Vitals,
|
|
ChatRuntimeBindings Chat,
|
|
RadarRuntimeBindings Radar,
|
|
CombatRuntimeBindings Combat,
|
|
JumpPowerbarRuntimeBindings JumpPowerbar,
|
|
FpsRuntimeBindings Fps,
|
|
ToolbarRuntimeBindings Toolbar,
|
|
CharacterRuntimeBindings Character,
|
|
InventoryRuntimeBindings Inventory,
|
|
RetailUiCursorBindings Cursor,
|
|
ConfirmationRuntimeBindings Confirmations,
|
|
StackSplitQuantityState StackSplitQuantity,
|
|
BufferedUiRegistry? Plugins,
|
|
RetailUiPersistenceBindings? Persistence,
|
|
RetailUiProbeBindings Probe);
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public sealed class RetailUiRuntime : IDisposable
|
|
{
|
|
private readonly RetailUiRuntimeBindings _bindings;
|
|
private StackSplitQuantityState StackSplitQuantity => _bindings.StackSplitQuantity;
|
|
private readonly RetailWindowLayoutPersistence? _persistence;
|
|
private readonly RetailUiAutomationScriptRunner? _automation;
|
|
private GameplayConfirmationController? _gameplayConfirmationController;
|
|
private RetailItemConfirmationController? _itemConfirmationController;
|
|
private bool _disposed;
|
|
|
|
private RetailUiRuntime(RetailUiRuntimeBindings bindings)
|
|
{
|
|
_bindings = bindings;
|
|
MountFpsDisplay();
|
|
MountVitals();
|
|
MountRadar();
|
|
MountChat();
|
|
MountToolbar();
|
|
MountCombat();
|
|
MountJumpPowerbar();
|
|
MountDialogFactory();
|
|
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,
|
|
stateManagedVisibilityWindows: [WindowNames.Combat, WindowNames.JumpPowerbar]);
|
|
}
|
|
|
|
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 CombatUiController? CombatUiController { 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 RetailDialogFactory? DialogFactory { 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();
|
|
JumpPowerbarController?.Tick();
|
|
SelectedObjectController?.Tick(deltaSeconds);
|
|
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)
|
|
=> ToolbarInputController?.Handle(action) == 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<bool> completed)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(message);
|
|
ArgumentNullException.ThrowIfNull(completed);
|
|
return DialogFactory?.MakeConfirmation(
|
|
message,
|
|
data => completed(data.GetBoolean(RetailDialogProperty.ConfirmationResult))) ?? 0u;
|
|
}
|
|
|
|
public void UpdateCursor(IEnumerable<IMouse> 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)
|
|
=> 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 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 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,
|
|
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 (regular, ghosted, empty) = LoadToolbarDigits();
|
|
ToolbarRuntimeBindings b = _bindings.Toolbar;
|
|
ToolbarController = Layout.ToolbarController.Bind(
|
|
layout, b.Objects, b.Shortcuts, b.ResolveIcon, b.UseItem, b.Combat,
|
|
regular, ghosted, 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,
|
|
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;
|
|
lock (_bindings.Assets.DatLock)
|
|
{
|
|
info = LayoutImporter.ImportInfos(_bindings.Assets.Dats, CombatUiController.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);
|
|
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;
|
|
}
|
|
|
|
CombatUiController = controller;
|
|
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 = controller,
|
|
});
|
|
controller.SyncVisibility();
|
|
Console.WriteLine("[M2] retail combat bar from gmCombatUI LayoutDesc 0x21000073.");
|
|
}
|
|
|
|
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);
|
|
Console.WriteLine(
|
|
$"[UI] retail DialogFactory from LayoutDesc 0x{layoutId:X8}; confirmation root 0x15.");
|
|
}
|
|
|
|
private (uint[] Regular, uint[] Ghosted, uint[]? Empty) LoadToolbarDigits()
|
|
{
|
|
uint[]? regular = null, ghosted = null, empty = null;
|
|
lock (_bindings.Assets.DatLock)
|
|
{
|
|
var layout = _bindings.Assets.Dats.Get<DatReaderWriter.DBObjs.LayoutDesc>(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,
|
|
];
|
|
Console.WriteLine($"[D.5.1] toolbar digit arrays ready: regular={regular.Length}, ghosted={ghosted.Length}, empty={empty?.Length ?? 0} entries.");
|
|
return (regular, ghosted, empty);
|
|
}
|
|
|
|
private static uint[]? ReadDataIds(
|
|
IReadOnlyDictionary<uint, DatReaderWriter.Types.BaseProperty> 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<UiElement>();
|
|
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;
|
|
IReadOnlyDictionary<uint, uint> 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<uint, uint>? 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,
|
|
b.ResolveDragIcon);
|
|
PaperdollController paperdoll = PaperdollController.Bind(
|
|
layout, b.Objects, b.PlayerGuid, b.ResolveIcon, b.Selection, b.SendWield,
|
|
contents, _bindings.Assets.DefaultFont, b.ItemInteraction, 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).");
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed) return;
|
|
_disposed = true;
|
|
_persistence?.Dispose();
|
|
Host.WindowManager.WindowVisibilityChanged -= OnWindowVisibilityChanged;
|
|
_itemConfirmationController?.Dispose();
|
|
_gameplayConfirmationController?.Dispose();
|
|
DialogFactory?.Dispose();
|
|
Host.Dispose();
|
|
}
|
|
}
|