Applies docs/research/2026-08-10-ch6ab-review-findings.md in full:
- BLOCKER 1: UiResizeGrip now carries its ElementInfo/resolve pair and
draws its own authored DirectState media (a synthetic parameterless
grip still draws nothing, preserving existing resize-drag tests).
DatWidgetFactory.BuildResizeGrip threads resolve through. All seven
live grips on the main chat window now resolve a non-zero sprite,
restoring the visible borders/corners CH6a silently dropped.
- SHOULD-FIX 2: ChatWindowState gains BroadcastTargetWindow, a sentinel
distinct from every real window id (0-4), fixing the bug where the
main window's explicit-addressing branch coincided with the broadcast
check (both were literal 0). SetFilter's main-window no-op is dropped
— the main window's filter is now genuinely settable. ChatWindowController
.Bind takes a ChatWindowState (the same canonical instance the floating
windows already share) and GetTranscriptLines builds a real accept
predicate instead of accept:null. Verified safe: ClientLocal (0x1A)
never reaches ChatLog (AddText routes it to the SpewBox and returns),
so nothing observable regresses.
- SHOULD-FIX 3: UiButton.SuppressSelfToggle stops the four chat-window
indicator buttons (DAT property 0x0B=true, no retail click handler)
from flipping their own Selected mirror on a stray click.
- SHOULD-FIX 4: generated and committed chat_floaty_2100005b.json from
the real installed dats; added the permanent RetailLayoutFixtureGenerator
entry. All three flagged FloatingChatWindowController assumptions
(input field, title bar, close button) are confirmed correct against
real data — no controller code changes needed. New finding: unlike the
main window, ALL EIGHT floaty border/corner elements are live Type-9
grips (the floaty's own title bar is its move handle), so a floaty
window resizes from every edge and corner.
- SHOULD-FIX 5: register row AP-189 documents the shared-500-entry/
200-line-tail vs retail's per-window 10,000-line scrollback depth gap.
- NITs 1-5: documented the filter-persistence-only-on-/saveautoui
asymmetry and the reconnect-preserves-filters intent; corrected the
research doc's modifier-mask mislabel and the "ONLY function" false
superlative; moved WrapText off ChatWindowController onto
ChatTranscriptRenderer, closing the circular dependency.
Full Release suite: 12,420 passed / 4 skipped / 0 failed (baseline
12,392/4/0 at 22020ef2; net +28 tests, zero regressions).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2399 lines
94 KiB
C#
2399 lines
94 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.Spells;
|
|
using AcDream.App.UI.Layout;
|
|
using AcDream.App.UI.Testing;
|
|
using AcDream.Core.Chat;
|
|
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.Runtime.Gameplay;
|
|
using AcDream.Content;
|
|
using AcDream.UI.Abstractions;
|
|
using AcDream.UI.Abstractions.Panels.Chat;
|
|
using AcDream.UI.Abstractions.Panels.Settings;
|
|
using AcDream.UI.Abstractions.Panels.Vitals;
|
|
using AcDream.UI.Abstractions.Input;
|
|
using DatReaderWriter;
|
|
using Silk.NET.Input;
|
|
|
|
namespace AcDream.App.UI;
|
|
|
|
public sealed record RetailUiAssets(
|
|
IDatReaderWriter 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);
|
|
|
|
/// <param name="Windows">
|
|
/// Runtime's canonical per-window filter/open state
|
|
/// (<see cref="AcDream.Runtime.Gameplay.RuntimeCommunicationState.ChatWindows"/>).
|
|
/// Campaign CH slice CH6b: the four floating chat windows read/write this
|
|
/// exact instance — never a presentation-owned copy.
|
|
/// </param>
|
|
/// <param name="Store">
|
|
/// Optional local settings store used to persist the four floating
|
|
/// windows' text-type filters (open/visible + geometry already persist for
|
|
/// free through the generic <see cref="RetailWindowLayoutPersistence"/>
|
|
/// path once each window is registered under a distinct
|
|
/// <see cref="WindowNames"/> entry). Null in hosts that don't have a
|
|
/// settings store wired (e.g. some tests) — filters then simply keep their
|
|
/// retail <c>PostInit</c> defaults for the session. The wire format
|
|
/// (retail's <c>0x1000008B</c>/<c>0x1000008C</c> gameplay-options blob) is
|
|
/// explicitly deferred — see
|
|
/// <c>docs/plans/2026-08-09-chat-parity-campaign.md</c>'s CH6f row.
|
|
/// </param>
|
|
public sealed record ChatRuntimeBindings(
|
|
ChatVM ViewModel,
|
|
Func<ICommandBus> CommandBus,
|
|
ChatWindowState Windows,
|
|
SettingsStore? Store = null);
|
|
|
|
public sealed record RadarRuntimeBindings(
|
|
Func<UiRadarSnapshot> Snapshot,
|
|
SelectionState Selection,
|
|
Action<bool> SetUiLocked);
|
|
|
|
public sealed record CombatRuntimeBindings(
|
|
CombatState State,
|
|
RuntimeCombatAttackState Attacks,
|
|
Func<GameplaySettings> Gameplay,
|
|
Action<GameplaySettings> SetGameplay);
|
|
|
|
public sealed record MagicRuntimeBindings(
|
|
Spellbook Spellbook,
|
|
RuntimeSpellCastState Casting,
|
|
ClientObjectTable Objects,
|
|
Func<uint> PlayerGuid,
|
|
IReadOnlyDictionary<uint, SpellComponentDescriptor> Components,
|
|
Func<ItemType, uint, uint, uint, uint, uint> ResolveIcon,
|
|
Func<ItemType, uint, uint, uint, uint, uint> ResolveDragIcon,
|
|
Func<uint, uint> ResolveSpellIcon,
|
|
Func<uint, uint> ResolveComponentIcon,
|
|
SelectionState Selection,
|
|
Func<uint, int> SpellLevel,
|
|
Func<uint, IReadOnlyList<SpellExamineComponent>> SpellComponents,
|
|
Func<MagicSchool, uint> MagicSkill,
|
|
Action<uint> SelectObject,
|
|
Action<uint> UseItem,
|
|
Action<int, int, uint> AddFavorite,
|
|
Action<int, uint> RemoveFavorite,
|
|
Action<uint> SendSpellbookFilter,
|
|
Action<uint> RemoveSpell,
|
|
Action<uint, uint> SetDesiredComponent,
|
|
Func<double> ServerTime);
|
|
|
|
public sealed record JumpPowerbarRuntimeBindings(Func<JumpChargeSnapshot> Snapshot);
|
|
|
|
public sealed record FpsRuntimeBindings(
|
|
Func<double> FramesPerSecond,
|
|
Func<double> DegradeMultiplier,
|
|
Func<bool> IsVisible);
|
|
|
|
public sealed record IndicatorRuntimeBindings(
|
|
Spellbook Spellbook,
|
|
ClientObjectTable Objects,
|
|
Func<uint> PlayerGuid,
|
|
Func<int?> Strength,
|
|
Func<LinkStatusSnapshot> LinkStatus,
|
|
Func<double> CurrentTime,
|
|
Action RequestLinkStatusPing,
|
|
Action EndCharacterSession);
|
|
|
|
public sealed record ToolbarRuntimeBindings(
|
|
ClientObjectTable Objects,
|
|
ShortcutStore 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,
|
|
// Slice 6.2: composed at InteractionRetainedUiComposition from
|
|
// d.Inventory.Vendor + d.Inventory.Objects — SelectedObjectController's
|
|
// vendor-owned split-exempt-seed predicate (research doc §C.1).
|
|
Func<uint, bool> IsVendorSplitExempt);
|
|
|
|
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,
|
|
Spellbook Spellbook,
|
|
Action<uint>? SendUse,
|
|
Action<uint, uint, int>? SendPutItemInContainer,
|
|
Action<uint, uint, uint, uint>? SendStackableSplitToContainer,
|
|
Action<uint, uint, uint>? SendStackableMerge,
|
|
ItemInteractionController ItemInteraction,
|
|
SelectionState Selection);
|
|
|
|
public sealed record ExternalContainerRuntimeBindings(
|
|
ExternalContainerState State,
|
|
ClientObjectTable Objects,
|
|
Func<ItemType, uint, uint, uint, uint, uint> ResolveIcon,
|
|
Func<ItemType, uint, uint, uint, uint, uint> ResolveDragIcon,
|
|
ItemInteractionController ItemInteraction,
|
|
SelectionState Selection,
|
|
Action<uint> SendUse,
|
|
Action<uint, uint, int> SendPutItemInContainer,
|
|
Action<uint, uint, uint, uint> SendStackableSplitToContainer,
|
|
Func<uint, bool> IsWithinUseRange);
|
|
|
|
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,
|
|
Func<InputAction, bool> PressInput,
|
|
Func<InputAction, bool, bool> SetInputHeld,
|
|
Testing.IRetailUiAutomationRuntime? Runtime = null);
|
|
|
|
public sealed record RetailUiCursorBindings(
|
|
CursorFeedbackController Feedback,
|
|
RetailCursorManager Manager);
|
|
|
|
public sealed record ConfirmationRuntimeBindings(
|
|
Action<uint, uint, bool> SendResponse);
|
|
|
|
public sealed record AppraisalRuntimeBindings(
|
|
Func<string> PlayerName,
|
|
Action<uint, string> SendSetInscription,
|
|
Action<string> DisplaySystemMessage);
|
|
|
|
public sealed record VendorRuntimeBindings(
|
|
VendorState State,
|
|
Func<ItemType, uint, uint, uint, uint, uint> ResolveIcon,
|
|
// Slice 6.1: shared examine routing — VendorUiController.ExamineItem
|
|
// calls ItemInteraction.ExamineSelectedOrEnterMode the same way
|
|
// ExternalContainerRuntimeBindings.ItemInteraction's consumer does.
|
|
ItemInteractionController ItemInteraction,
|
|
// Slice 6.2: the canonical selection authority — every sibling binding
|
|
// record (Radar/Magic/Toolbar/Inventory/ExternalContainer) already
|
|
// carries this; Vendor was the one outlier (research doc §C.1).
|
|
SelectionState Selection,
|
|
// Slice 6b/6c: InqAcceptability's rejection strings, delivered on the
|
|
// SAME notice-0x1a system-message channel AppraisalRuntimeBindings'
|
|
// own DisplaySystemMessage already uses.
|
|
Action<string>? DisplaySystemMessage = null);
|
|
|
|
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,
|
|
VendorRuntimeBindings Vendor,
|
|
RetailUiCursorBindings Cursor,
|
|
ConfirmationRuntimeBindings Confirmations,
|
|
AppraisalRuntimeBindings Appraisal,
|
|
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 RetailWindowLayoutPersistence? _persistence;
|
|
private RetailUiAutomationScriptRunner? _automation;
|
|
private readonly RetailPanelUiController _panelUi;
|
|
private ChatWindowController? _chatWindowController;
|
|
private readonly FloatingChatWindowController?[] _floatingChatControllers = new FloatingChatWindowController?[4];
|
|
private GameplayConfirmationController? _gameplayConfirmationController;
|
|
private RetailItemConfirmationController? _itemConfirmationController;
|
|
private RetailSkillTrainingConfirmationController? _skillTrainingConfirmationController;
|
|
private UiShortcutDigitGraphics? _shortcutDigitGraphics;
|
|
private ItemCooldownUiController? _itemCooldownController;
|
|
private VividTargetIndicatorController? _vividTargetIndicator;
|
|
private IDisposable? _characterSheetSubscription;
|
|
private ResourceShutdownTransaction? _shutdown;
|
|
private bool _disposed;
|
|
|
|
internal bool IsDisposalComplete => _disposed;
|
|
|
|
private RetailUiRuntime(RetailUiRuntimeBindings bindings)
|
|
{
|
|
_bindings = bindings;
|
|
_panelUi = new RetailPanelUiController(
|
|
bindings.Host.IsWindowVisible,
|
|
bindings.Host.ShowWindow,
|
|
bindings.Host.HideWindow);
|
|
}
|
|
|
|
internal static RetailUiRuntime CreateUninitialized(
|
|
RetailUiRuntimeBindings bindings)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(bindings);
|
|
return new RetailUiRuntime(bindings);
|
|
}
|
|
|
|
internal void InitializeForLease() => Initialize();
|
|
|
|
private void Initialize()
|
|
{
|
|
RetailUiRuntimeBindings bindings = _bindings;
|
|
MountFpsDisplay();
|
|
MountVividTargetIndicator();
|
|
MountVitals();
|
|
MountRadar();
|
|
MountChat();
|
|
MountFloatingChatWindows();
|
|
MountToolbar();
|
|
MountCombat();
|
|
MountSpellbook();
|
|
MountAppraisal();
|
|
MountEffects();
|
|
MountIndicatorDetailPanels();
|
|
MountIndicators();
|
|
MountJumpPowerbar();
|
|
MountDialogFactory();
|
|
MountCharacter();
|
|
MountPlugins();
|
|
MountInventory();
|
|
MountExternalContainer();
|
|
MountVendor();
|
|
MountItemCooldowns();
|
|
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,
|
|
WindowNames.Vendor,
|
|
]);
|
|
}
|
|
|
|
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),
|
|
bindings.Probe.PressInput,
|
|
bindings.Probe.SetInputHeld,
|
|
bindings.Probe.Runtime);
|
|
}
|
|
}
|
|
|
|
public UiHost Host => _bindings.Host;
|
|
|
|
/// <summary>
|
|
/// Shared dat/sprite/font resolvers this runtime was built with.
|
|
/// Campaign CH user-gate round 3: lets a controller built OUTSIDE this
|
|
/// runtime's own <c>Mount*</c> graph (e.g. <c>SpewBoxController</c>,
|
|
/// composed directly in <c>LivePresentationComposition</c>) resolve the
|
|
/// SAME memoized <see cref="UiDatFont"/> instances instead of loading
|
|
/// its own copy.
|
|
/// </summary>
|
|
public RetailUiAssets Assets => _bindings.Assets;
|
|
|
|
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 AppraisalUiController? AppraisalController { get; private set; }
|
|
public UiViewport? CreatureAppraisalViewportWidget { get; private set; }
|
|
public UiElement? ExaminationFrame { 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 VendorUiController? VendorController { get; private set; }
|
|
|
|
public static RetailUiRuntime Mount(RetailUiRuntimeBindings bindings)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(bindings);
|
|
var runtime = CreateUninitialized(bindings);
|
|
try
|
|
{
|
|
runtime.Initialize();
|
|
return runtime;
|
|
}
|
|
catch (Exception initializationFailure)
|
|
{
|
|
try
|
|
{
|
|
runtime.Dispose();
|
|
}
|
|
catch (Exception cleanupFailure)
|
|
{
|
|
throw new AggregateException(
|
|
"Retail UI initialization failed and its partially constructed ownership could not be fully released.",
|
|
initializationFailure,
|
|
cleanupFailure);
|
|
}
|
|
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public void Tick(double deltaSeconds)
|
|
{
|
|
FpsController?.Tick();
|
|
_vividTargetIndicator?.Tick();
|
|
SpellbookWindowController?.Tick();
|
|
AppraisalController?.Tick(deltaSeconds);
|
|
SpellcastingUiController?.Tick();
|
|
PositiveEffectsController?.Tick();
|
|
NegativeEffectsController?.Tick();
|
|
LinkStatusUiController?.Tick();
|
|
IndicatorBarController?.Tick();
|
|
JumpPowerbarController?.Tick();
|
|
SelectedObjectController?.Tick(deltaSeconds);
|
|
ExternalContainerController?.Tick();
|
|
_itemCooldownController?.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 bool HandleAppraisal(AppraiseInfoParser.Parsed appraisal)
|
|
=> AppraisalController is { } controller
|
|
? controller.Apply(appraisal)
|
|
: ItemInteraction.AcceptAppraisalResponse(appraisal.Guid).Accepted;
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Close character-session dialogs through retail's normal callback and
|
|
/// close-notice path, then forget the gameplay confirmation tuple.
|
|
/// <c>gmGamePlayUI::~gmGamePlayUI @ 0x004EA2A0</c> closes its retained
|
|
/// contexts before <c>DialogFactory::Reset @ 0x00477950</c> drains the
|
|
/// remaining factory entries.
|
|
/// </summary>
|
|
public void ResetSessionDialogs()
|
|
{
|
|
try
|
|
{
|
|
DialogFactory?.Reset();
|
|
}
|
|
finally
|
|
{
|
|
_gameplayConfirmationController?.ResetSession();
|
|
}
|
|
}
|
|
|
|
public void ResetSessionTransientUi()
|
|
{
|
|
ResetSessionDialogs();
|
|
AppraisalController?.ResetSession();
|
|
Host.HideWindow(WindowNames.Examination);
|
|
}
|
|
|
|
public void UpdateCursor(IEnumerable<IMouse> mice)
|
|
{
|
|
CursorFeedback feedback = _bindings.Cursor.Feedback.Update(Host.Root);
|
|
_bindings.Cursor.Manager.Apply(mice, feedback);
|
|
}
|
|
|
|
/// <summary>
|
|
/// #348: opts cursor application into the process-lifetime native
|
|
/// GLFW cursor cache. Called once by the graphical host after the
|
|
/// native window exists; zero handle keeps the Silk fallback.
|
|
/// </summary>
|
|
public void AttachNativeCursorWindow(nint glfwWindowHandle)
|
|
{
|
|
_bindings.Cursor.Manager.AttachNativeWindow(glfwWindowHandle);
|
|
}
|
|
|
|
public void RestoreLayout() => _persistence?.RestoreAll();
|
|
|
|
public void SaveLayout()
|
|
{
|
|
_persistence?.SaveAll();
|
|
SaveChatWindowFilters();
|
|
}
|
|
|
|
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);
|
|
|
|
/// <summary>
|
|
/// The four floating chat window controllers, indexed <c>[windowId - 1]</c>
|
|
/// (null entries are windows whose required elements were missing —
|
|
/// see <see cref="MountFloatingChatWindows"/>).
|
|
/// </summary>
|
|
public IReadOnlyList<FloatingChatWindowController?> FloatingChatWindows => _floatingChatControllers;
|
|
|
|
/// <summary>
|
|
/// Toggle floating chat window <paramref name="windowId"/> (1-4) — the
|
|
/// <c>ToggleFloatingChatWindow1..4</c> keybind's landing point
|
|
/// (research doc §1.3: the keybind mechanism, not a button click, is
|
|
/// what drives this in retail). Routes through the generic
|
|
/// <see cref="UiHost.ToggleWindow"/>, whose visibility-change event
|
|
/// drives both the persisted open state
|
|
/// (<see cref="OnWindowVisibilityChanged"/> syncing
|
|
/// <see cref="ChatWindowState"/>) and the main window's indicator-button
|
|
/// mirror — one chokepoint for every trigger.
|
|
/// </summary>
|
|
public bool ToggleFloatingChatWindow(int windowId)
|
|
=> Host.ToggleWindow(FloatingChatWindowName(windowId));
|
|
|
|
private static string FloatingChatWindowName(int windowId) => windowId switch
|
|
{
|
|
1 => WindowNames.ChatWindow1,
|
|
2 => WindowNames.ChatWindow2,
|
|
3 => WindowNames.ChatWindow3,
|
|
4 => WindowNames.ChatWindow4,
|
|
_ => throw new ArgumentOutOfRangeException(nameof(windowId), windowId, "floating chat window id must be 1-4."),
|
|
};
|
|
|
|
/// <summary>
|
|
/// Capture the four floating windows' current text-type filters into
|
|
/// the local settings store (research doc §4.4/§6.1 — local-only until
|
|
/// CH6f's wire format). No-op when no store was wired.
|
|
///
|
|
/// <para>
|
|
/// CH6a/b REJECT-review NIT 1: unlike window geometry/visibility (which
|
|
/// <see cref="_persistence"/> auto-saves on every change), a filter
|
|
/// change is only captured here — i.e. only when <see cref="SaveLayout"/>
|
|
/// runs, which today is exclusively the explicit <c>/saveautoui</c>
|
|
/// command. Harmless in practice (nothing currently mutates a window's
|
|
/// filter live — no options UI exists yet), but worth tightening to
|
|
/// auto-save-on-change once CH6e/CH6f gives filters a live settings
|
|
/// surface a user can actually edit mid-session.
|
|
/// </para>
|
|
/// </summary>
|
|
private void SaveChatWindowFilters()
|
|
{
|
|
if (_bindings.Chat.Store is not { } store) return;
|
|
ChatWindowState windows = _bindings.Chat.Windows;
|
|
ChatSettings current = store.LoadChat();
|
|
store.SaveChat(current with
|
|
{
|
|
ChatWindow1Filter = windows.GetFilter(1),
|
|
ChatWindow2Filter = windows.GetFilter(2),
|
|
ChatWindow3Filter = windows.GetFilter(3),
|
|
ChatWindow4Filter = windows.GetFilter(4),
|
|
});
|
|
}
|
|
|
|
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);
|
|
|
|
if (TryGetFloatingChatWindowId(windowName, out int chatWindowId))
|
|
{
|
|
// Keep Runtime's canonical open flag in sync regardless of what
|
|
// triggered the visibility change (keybind, restored layout,
|
|
// or the window's own close button) — one write path, matching
|
|
// gmFloatyChatUI::SetVisible's own single call site
|
|
// (persistence research doc §4.2).
|
|
_bindings.Chat.Windows.SetOpen(chatWindowId, visible);
|
|
_chatWindowController?.SetIndicatorOpen(chatWindowId, visible);
|
|
}
|
|
}
|
|
|
|
private static bool TryGetFloatingChatWindowId(string windowName, out int windowId)
|
|
{
|
|
windowId = windowName switch
|
|
{
|
|
WindowNames.ChatWindow1 => 1,
|
|
WindowNames.ChatWindow2 => 2,
|
|
WindowNames.ChatWindow3 => 3,
|
|
WindowNames.ChatWindow4 => 4,
|
|
_ => 0,
|
|
};
|
|
return windowId != 0;
|
|
}
|
|
|
|
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 0x2100006F not found.");
|
|
return;
|
|
}
|
|
|
|
ChatWindowController? controller = ChatWindowController.Bind(
|
|
info,
|
|
layout,
|
|
_bindings.Chat.ViewModel,
|
|
_bindings.Chat.CommandBus,
|
|
_bindings.Chat.Windows,
|
|
_bindings.Assets.DefaultFont,
|
|
_bindings.Assets.DebugFont,
|
|
_bindings.Assets.ResolveSprite);
|
|
if (controller is null)
|
|
{
|
|
Console.WriteLine("[D.2b] chat: required role elements missing in 0x2100006F.");
|
|
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,
|
|
// 0x2100006F's root already IS the complete retail window — its own
|
|
// 8 border/corner elements (the live Resizebar/Dragbar grips, or their
|
|
// _Locked cosmetic twins) are its chrome, at its authored 410x100
|
|
// extent. No universal nine-slice wrapper or content crop needed
|
|
// (Campaign CH slice CH6a; the wrapper + 490px crop were compensations
|
|
// for importing the wrong 0x21000006 layout).
|
|
Chrome = RetailWindowChrome.Imported,
|
|
Left = 10f,
|
|
Top = 440f,
|
|
DatConstraintSource = controller.DatWindowInfo,
|
|
// Bumping this resets any locally-persisted width/height captured
|
|
// under the old, wrong-layout mount (490px-cropped content, patched
|
|
// 9px height) to the newly authored 410x100 default on next load.
|
|
AuthoredGeometryRevision = 1,
|
|
ResizeX = true,
|
|
ResizeY = true,
|
|
// All 4 edges + 4 corners resize via the authored Resizebar grips
|
|
// (UiRoot gives a directly-hit grip's own edges priority over this
|
|
// blanket mask); the default already covers every edge, and the
|
|
// plain top-STRIP correctly remains a move-only affordance because
|
|
// its own element is a Type-2 Dragbar, not a grip (research doc §2.1,
|
|
// §2.3 — the top edge authors NO Resizebar of its own; only its two
|
|
// corners do).
|
|
// Opacity: retail's whole-window alpha (ChatInterface::SetOpacity
|
|
// @0x004F3120) is CH6c's scope — UiRenderContext.AlphaMod currently has
|
|
// no draw-path consumer, so any value here is presentation-inert; leave
|
|
// the frame at the default 1f rather than assert a value with no effect.
|
|
Controller = controller,
|
|
StateController = controller,
|
|
});
|
|
controller.AttachWindow(handle);
|
|
Host.Root.DefaultTextInput = controller.Input;
|
|
_chatWindowController = controller;
|
|
Console.WriteLine("[D.2b] retail chat window from LayoutDesc importer (0x2100006F).");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mount the four retail floating chat windows — always-resident
|
|
/// authored children per <c>gmGamePlayUI::SetupChildren @0x004E9EC0</c>
|
|
/// (research doc §1.1), all four sharing LayoutDesc <c>0x2100005B</c>
|
|
/// but each with its own widget tree, window-registry name, and
|
|
/// screen position; every one starts hidden
|
|
/// (<c>gmFloatyChatUI</c> instances are born closed and toggled by
|
|
/// keybind/indicator button — research doc §1.3-§1.4). Must run after
|
|
/// <see cref="MountChat"/> so <see cref="_chatWindowController"/> is
|
|
/// available for the indicator-button mirror wired in
|
|
/// <see cref="OnWindowVisibilityChanged"/>.
|
|
/// </summary>
|
|
private void MountFloatingChatWindows()
|
|
{
|
|
ElementInfo? info;
|
|
lock (_bindings.Assets.DatLock)
|
|
{
|
|
info = LayoutImporter.ImportInfos(_bindings.Assets.Dats, FloatingChatWindowController.LayoutId);
|
|
}
|
|
if (info is null)
|
|
{
|
|
Console.WriteLine("[D.2b] floating chat: LayoutDesc 0x2100005B not found.");
|
|
return;
|
|
}
|
|
|
|
ChatWindowState windowFilters = _bindings.Chat.Windows;
|
|
if (_bindings.Chat.Store is { } store)
|
|
{
|
|
// Local-only persistence (research doc §4.4/§6.1 — the retail
|
|
// 0x1000008B/0x1000008C gameplay-options wire is explicitly
|
|
// deferred to CH6f). Open/visible + geometry persist for free
|
|
// through the generic RetailWindowLayoutPersistence path once
|
|
// each window registers below; only the filter mask needs its
|
|
// own load/save leg.
|
|
ChatSettings chat = store.LoadChat();
|
|
windowFilters.SetFilter(1, chat.ChatWindow1Filter);
|
|
windowFilters.SetFilter(2, chat.ChatWindow2Filter);
|
|
windowFilters.SetFilter(3, chat.ChatWindow3Filter);
|
|
windowFilters.SetFilter(4, chat.ChatWindow4Filter);
|
|
}
|
|
|
|
// acdream-chosen default screen positions — retail's own authored
|
|
// rect (0,80 in the shared LayoutDesc's local space) is identical
|
|
// for all four windows, so a literal port would stack every window
|
|
// at the same spot on first open; stagger them the same way the
|
|
// main chat window's own (10,440) default is an acdream placement
|
|
// choice, not a retail-authored screen position (MountChat above).
|
|
(string windowName, float left, float top)[] slots =
|
|
{
|
|
(WindowNames.ChatWindow1, 440f, 40f),
|
|
(WindowNames.ChatWindow2, 440f, 170f),
|
|
(WindowNames.ChatWindow3, 440f, 300f),
|
|
(WindowNames.ChatWindow4, 440f, 430f),
|
|
};
|
|
|
|
var strings = new DatStringResolver(_bindings.Assets.Dats);
|
|
for (int windowId = 1; windowId <= 4; windowId++)
|
|
{
|
|
ImportedLayout layout;
|
|
lock (_bindings.Assets.DatLock)
|
|
{
|
|
layout = LayoutImporter.Build(
|
|
info,
|
|
_bindings.Assets.ResolveSprite,
|
|
_bindings.Assets.DefaultFont,
|
|
_bindings.Assets.ResolveFont,
|
|
strings.Resolve);
|
|
}
|
|
|
|
FloatingChatWindowController? controller = FloatingChatWindowController.Bind(
|
|
windowId,
|
|
info,
|
|
layout,
|
|
_bindings.Chat.ViewModel,
|
|
_bindings.Chat.CommandBus,
|
|
windowFilters,
|
|
_bindings.Assets.DefaultFont,
|
|
_bindings.Assets.DebugFont,
|
|
_bindings.Assets.ResolveSprite);
|
|
if (controller is null)
|
|
{
|
|
Console.WriteLine($"[D.2b] floating chat window {windowId}: required role elements missing in 0x2100005B.");
|
|
continue;
|
|
}
|
|
|
|
controller.Transcript.Keyboard = Host.Keyboard;
|
|
controller.Input.Keyboard = Host.Keyboard;
|
|
(string windowName, float left, float top) = slots[windowId - 1];
|
|
UiElement root = controller.Root;
|
|
RetailWindowHandle handle = RetailWindowFrame.Mount(
|
|
Host.Root,
|
|
root,
|
|
_bindings.Assets.ResolveSprite,
|
|
new RetailWindowFrame.Options
|
|
{
|
|
WindowName = windowName,
|
|
// Same reasoning as MountChat: 0x2100005B's root is its
|
|
// own complete chrome (title bar, close button, and
|
|
// whichever mix of Resizebar/Dragbar grips the DAT
|
|
// authors for this layout — CH6a's importer/UiRoot
|
|
// priority logic is fully data-driven and needs no
|
|
// per-layout grip-count knowledge here).
|
|
Chrome = RetailWindowChrome.Imported,
|
|
Left = left,
|
|
Top = top,
|
|
DatConstraintSource = controller.DatWindowInfo,
|
|
AuthoredGeometryRevision = 1,
|
|
ResizeX = true,
|
|
ResizeY = true,
|
|
// Born hidden (research doc §1.1) — opened only via
|
|
// ToggleFloatingChatWindow (keybind) or a saved
|
|
// "visible" layout entry restored by
|
|
// RetailWindowLayoutPersistence.
|
|
Visible = false,
|
|
Controller = controller,
|
|
});
|
|
controller.AttachWindow(handle);
|
|
_floatingChatControllers[windowId - 1] = controller;
|
|
}
|
|
|
|
Console.WriteLine("[D.2b] retail floating chat windows 1-4 from LayoutDesc importer (0x2100005B).");
|
|
}
|
|
|
|
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,
|
|
selection: b.Selection,
|
|
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,
|
|
b.IsVendorSplitExempt);
|
|
|
|
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 (info is null || layout is null || labels is null)
|
|
{
|
|
Console.WriteLine("[M2] combat: LayoutDesc 0x21000073 not found.");
|
|
return;
|
|
}
|
|
float combatWidth = RetailCombatLayout.FitFavoriteSlots(layout);
|
|
|
|
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,
|
|
examineSpell: spellId => AppraisalController?.ExamineSpell(spellId));
|
|
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 = 0f,
|
|
Top = Math.Max(0f, Host.Root.Height - root.Height - 10f),
|
|
ContentWidth = combatWidth,
|
|
DatConstraintSource = info,
|
|
OuterAnchors = AnchorEdges.Left | AnchorEdges.Bottom,
|
|
Visible = false,
|
|
Draggable = 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 MountAppraisal()
|
|
{
|
|
ImportedLayout? layout;
|
|
CreatureAppraisalRowTemplateFactory? creatureRows;
|
|
SpellExamineComponentTemplateFactory? spellComponentTemplates;
|
|
CreatureDisplayNameResolver? creatureNames;
|
|
RetailAppraisalNameResolver? itemNames;
|
|
lock (_bindings.Assets.DatLock)
|
|
{
|
|
layout = LayoutImporter.Import(
|
|
_bindings.Assets.Dats,
|
|
AppraisalUiController.LayoutId,
|
|
AppraisalUiController.RootId,
|
|
_bindings.Assets.ResolveSprite,
|
|
_bindings.Assets.DefaultFont,
|
|
_bindings.Assets.ResolveFont);
|
|
creatureRows = CreatureAppraisalRowTemplateFactory.TryLoad(
|
|
_bindings.Assets.Dats,
|
|
_bindings.Assets.ResolveSprite,
|
|
_bindings.Assets.DefaultFont,
|
|
_bindings.Assets.ResolveFont);
|
|
spellComponentTemplates =
|
|
SpellExamineComponentTemplateFactory.TryLoad(
|
|
_bindings.Assets.Dats,
|
|
_bindings.Assets.ResolveSprite,
|
|
_bindings.Assets.DefaultFont,
|
|
_bindings.Assets.ResolveFont);
|
|
creatureNames = CreatureDisplayNameResolver.Load(
|
|
_bindings.Assets.Dats);
|
|
itemNames = RetailAppraisalNameResolver.Load(
|
|
_bindings.Assets.Dats,
|
|
creatureNames);
|
|
}
|
|
if (layout is null)
|
|
{
|
|
Console.WriteLine(
|
|
"[M4] examination: LayoutDesc 0x2100006B root 0x100005F2 not found.");
|
|
return;
|
|
}
|
|
|
|
AppraisalUiController? controller = AppraisalUiController.Bind(
|
|
layout,
|
|
_bindings.Inventory.Objects,
|
|
_bindings.Inventory.ItemInteraction,
|
|
_bindings.Inventory.Selection,
|
|
_bindings.Combat.State,
|
|
_bindings.Magic.Spellbook,
|
|
_bindings.Appraisal.PlayerName,
|
|
_bindings.Appraisal.SendSetInscription,
|
|
_bindings.Appraisal.DisplaySystemMessage,
|
|
show: () => Host.ShowWindow(WindowNames.Examination),
|
|
close: () => CloseWindow(WindowNames.Examination),
|
|
creatureRowTemplates: creatureRows,
|
|
creatureNames: creatureNames,
|
|
itemNames: itemNames,
|
|
resolveSpellIcon: _bindings.Magic.ResolveSpellIcon,
|
|
resolveComponentIcon: _bindings.Magic.ResolveComponentIcon,
|
|
spellComponents: _bindings.Magic.SpellComponents,
|
|
magicSkill: _bindings.Magic.MagicSkill,
|
|
spellComponentTemplates: spellComponentTemplates);
|
|
if (controller is null)
|
|
{
|
|
Console.WriteLine(
|
|
"[M4] examination: required authored controls are missing.");
|
|
return;
|
|
}
|
|
|
|
AppraisalController = controller;
|
|
UiElement root = layout.Root;
|
|
RetailWindowHandle handle = RetailWindowFrame.Mount(
|
|
Host.Root,
|
|
root,
|
|
_bindings.Assets.ResolveSprite,
|
|
new RetailWindowFrame.Options
|
|
{
|
|
WindowName = WindowNames.Examination,
|
|
Chrome = RetailWindowChrome.Imported,
|
|
Left = root.Left,
|
|
Top = root.Top,
|
|
ContentWidth = root.Width,
|
|
ContentHeight = root.Height,
|
|
AuthoredGeometryRevision = 1,
|
|
Visible = false,
|
|
ResizeX = true,
|
|
ResizeY = true,
|
|
MinWidth = 310f,
|
|
MinHeight = 400f,
|
|
ConstrainDragToParent = true,
|
|
ConstrainResizeToParent = true,
|
|
ContentClickThrough = false,
|
|
Controller = controller,
|
|
});
|
|
CreatureAppraisalViewportWidget =
|
|
layout.FindElement(AppraisalUiController.CreatureViewportId)
|
|
as UiViewport;
|
|
ExaminationFrame = handle.OuterFrame;
|
|
Console.WriteLine(
|
|
"[M4] retail examination window from LayoutDesc 0x2100006B.");
|
|
}
|
|
|
|
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;
|
|
|
|
CharacterInformationUiController controller = CharacterController.Bind(
|
|
layout,
|
|
_bindings.Character.Provider.BuildSheet,
|
|
_bindings.Assets.DefaultFont,
|
|
() => CloseWindow(WindowNames.CharacterInformation),
|
|
strings,
|
|
_bindings.Character.Provider.SubscribeChanged);
|
|
RegisterIndicatorDetailPanel(
|
|
RetailPanelCatalog.CharacterInformation,
|
|
WindowNames.CharacterInformation,
|
|
rootInfo,
|
|
layout.Root,
|
|
controller);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<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,
|
|
];
|
|
_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<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;
|
|
CharacterSheet currentSheet = provider.BuildSheet();
|
|
_characterSheetSubscription = provider.SubscribeChanged(
|
|
() => currentSheet = provider.BuildSheet());
|
|
CharacterStatController.Bind(
|
|
layout,
|
|
() => currentSheet,
|
|
_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<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.SendPutItemInContainer, b.SendStackableSplitToContainer, b.SendStackableMerge,
|
|
notifyMergeAttempt, b.ItemInteraction,
|
|
() => CloseWindow(WindowNames.Inventory),
|
|
StackSplitQuantity,
|
|
b.ResolveDragIcon,
|
|
b.Spellbook);
|
|
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.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Slice 5.4: the retail vendor "Items" browse tab (LayoutDesc
|
|
/// 0x21000012, root 0x100000B7 — see
|
|
/// docs/research/2026-08-08-slice5-vendor-browse-research.md §B.3/§B.4
|
|
/// for the discovery + D0 category-filter read). Two-step mount
|
|
/// (Mount then Bind then AttachController) matches
|
|
/// <see cref="MountExternalContainer"/>: the controller needs the
|
|
/// <see cref="RetailWindowHandle"/> to Show/Hide itself as
|
|
/// <see cref="VendorState"/> opens/closes, so the handle must exist
|
|
/// before the controller is constructed.
|
|
/// </summary>
|
|
private void MountVendor()
|
|
{
|
|
ImportedLayout? layout;
|
|
uint emptySlotSprite;
|
|
uint buyingEmptySlotSprite;
|
|
uint sellingEmptySlotSprite;
|
|
lock (_bindings.Assets.DatLock)
|
|
{
|
|
layout = LayoutImporter.Import(
|
|
_bindings.Assets.Dats,
|
|
VendorUiController.LayoutId,
|
|
VendorUiController.RootId,
|
|
_bindings.Assets.ResolveSprite,
|
|
_bindings.Assets.DefaultFont,
|
|
_bindings.Assets.ResolveFont);
|
|
// F7b (Slice 5.4 review): the authored empty-slot background for
|
|
// the item strip, same resolution path ExternalContainerController
|
|
// uses for its own lists.
|
|
emptySlotSprite = ItemListCellTemplate.ResolveEmptySprite(
|
|
_bindings.Assets.Dats,
|
|
VendorUiController.LayoutId,
|
|
VendorUiController.ItemListId);
|
|
// G2 (vendor gate finding): the Buying/Selling pages author
|
|
// their OWN item strips (same cell-template prototype as the
|
|
// Items list per the fixture, but resolved independently here
|
|
// rather than assumed identical).
|
|
buyingEmptySlotSprite = ItemListCellTemplate.ResolveEmptySprite(
|
|
_bindings.Assets.Dats,
|
|
VendorUiController.LayoutId,
|
|
VendorUiController.BuyingListId);
|
|
sellingEmptySlotSprite = ItemListCellTemplate.ResolveEmptySprite(
|
|
_bindings.Assets.Dats,
|
|
VendorUiController.LayoutId,
|
|
VendorUiController.SellingListId);
|
|
}
|
|
if (layout is null)
|
|
{
|
|
Console.WriteLine("[M4] vendor: LayoutDesc 0x21000012 root 0x100000B7 not found.");
|
|
return;
|
|
}
|
|
|
|
UiElement root = layout.Root;
|
|
RetailWindowHandle handle = RetailWindowFrame.Mount(
|
|
Host.Root,
|
|
root,
|
|
_bindings.Assets.ResolveSprite,
|
|
new RetailWindowFrame.Options
|
|
{
|
|
WindowName = WindowNames.Vendor,
|
|
// Root 0x100000B7 authors only the tiled center surface
|
|
// (backdrop element 0x1000008D provides its own fill); the
|
|
// common floating-window bevel surrounds it — same shape as
|
|
// ExternalContainerController's 0x21000008/0x10000063.
|
|
Chrome = RetailWindowChrome.NineSlice,
|
|
Left = root.Left,
|
|
Top = root.Top,
|
|
ContentWidth = root.Width,
|
|
ContentHeight = root.Height,
|
|
MinWidth = root.Width,
|
|
MinHeight = root.Height,
|
|
Visible = false,
|
|
ResizeX = false,
|
|
ResizeY = false,
|
|
ConstrainDragToParent = true,
|
|
ConstrainResizeToParent = true,
|
|
DrawChromeCenter = false,
|
|
});
|
|
|
|
VendorRuntimeBindings b = _bindings.Vendor;
|
|
// F1/F2/F3/F7b (Slice 5.4 review): the category dropdown needs
|
|
// fonts/sprites to draw at all; the price text needs the local
|
|
// player's coin total, sourced from the SAME ClientObjectTable/
|
|
// PlayerGuid pair InventoryRuntimeBindings already exposes (no new
|
|
// binding record needed — this mirrors how MountExternalContainer
|
|
// reads its own sibling binding).
|
|
VendorController = VendorUiController.Bind(
|
|
layout,
|
|
b.State,
|
|
handle,
|
|
b.ResolveIcon,
|
|
_bindings.Inventory.Objects,
|
|
_bindings.Inventory.PlayerGuid,
|
|
b.ItemInteraction,
|
|
b.Selection,
|
|
StackSplitQuantity,
|
|
_bindings.Assets.DefaultFont,
|
|
_bindings.Assets.DebugFont,
|
|
_bindings.Assets.ResolveSprite,
|
|
emptySlotSprite,
|
|
buyingEmptySlotSprite,
|
|
sellingEmptySlotSprite,
|
|
// Slice 6b/6c: the X-close staging confirmation and
|
|
// InqAcceptability rejection strings.
|
|
DialogFactory,
|
|
b.DisplaySystemMessage);
|
|
if (VendorController is null)
|
|
{
|
|
Console.WriteLine("[M4] vendor: required authored controls are missing.");
|
|
return;
|
|
}
|
|
|
|
Host.WindowManager.AttachController(WindowNames.Vendor, VendorController);
|
|
Console.WriteLine("[M4] retail vendor browse panel mounted from LayoutDesc 0x21000012.");
|
|
}
|
|
|
|
private void MountItemCooldowns()
|
|
{
|
|
ItemCooldownAssets? assets;
|
|
lock (_bindings.Assets.DatLock)
|
|
assets = ItemCooldownAssets.TryLoad(_bindings.Assets.Dats);
|
|
|
|
if (assets is null)
|
|
{
|
|
Console.WriteLine(
|
|
"[UI] shared UIItem cooldown overlays missing from LayoutDesc 0x21000037.");
|
|
return;
|
|
}
|
|
|
|
_itemCooldownController = ItemCooldownUiController.Bind(
|
|
Host.Root,
|
|
_bindings.Magic.Spellbook,
|
|
_bindings.Magic.Objects,
|
|
_bindings.Magic.ServerTime,
|
|
assets.Value);
|
|
Console.WriteLine(
|
|
"[UI] retail shared item cooldown overlays ready (10 DAT-authored steps).");
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
|
|
_shutdown ??= CreateShutdownTransaction(
|
|
() => _automation?.Dispose(),
|
|
() => _persistence?.Dispose(),
|
|
() =>
|
|
{
|
|
_characterSheetSubscription?.Dispose();
|
|
Host.WindowManager.WindowVisibilityChanged -= OnWindowVisibilityChanged;
|
|
},
|
|
() => _itemConfirmationController?.Dispose(),
|
|
() => _gameplayConfirmationController?.Dispose(),
|
|
() => DialogFactory?.Dispose(),
|
|
_panelUi.Dispose,
|
|
Host.Dispose);
|
|
_shutdown.CompleteOrThrow();
|
|
_disposed = _shutdown.IsComplete;
|
|
}
|
|
|
|
internal static ResourceShutdownTransaction CreateShutdownTransaction(
|
|
Action disposeAutomation,
|
|
Action disposePersistence,
|
|
Action unsubscribeWindowVisibility,
|
|
Action disposeItemConfirmation,
|
|
Action disposeGameplayConfirmation,
|
|
Action disposeDialogFactory,
|
|
Action disposePanelController,
|
|
Action disposeHost)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(disposeAutomation);
|
|
ArgumentNullException.ThrowIfNull(disposePersistence);
|
|
ArgumentNullException.ThrowIfNull(unsubscribeWindowVisibility);
|
|
ArgumentNullException.ThrowIfNull(disposeItemConfirmation);
|
|
ArgumentNullException.ThrowIfNull(disposeGameplayConfirmation);
|
|
ArgumentNullException.ThrowIfNull(disposeDialogFactory);
|
|
ArgumentNullException.ThrowIfNull(disposePanelController);
|
|
ArgumentNullException.ThrowIfNull(disposeHost);
|
|
|
|
return new ResourceShutdownTransaction(
|
|
new ResourceShutdownStage("retail UI observers",
|
|
[
|
|
new("UI automation", disposeAutomation),
|
|
new("window persistence", disposePersistence),
|
|
new("window visibility", unsubscribeWindowVisibility),
|
|
]),
|
|
new ResourceShutdownStage("retail UI semantic controllers",
|
|
[
|
|
new("item confirmation", disposeItemConfirmation),
|
|
new("gameplay confirmation", disposeGameplayConfirmation),
|
|
]),
|
|
new ResourceShutdownStage("retail UI panel composition",
|
|
[
|
|
new("dialog factory", disposeDialogFactory),
|
|
new("panel controller", disposePanelController),
|
|
]),
|
|
new ResourceShutdownStage("retained UI host",
|
|
[
|
|
new("host", disposeHost),
|
|
]));
|
|
}
|
|
}
|