feat(ui): finish retail toolbar controls

Discover all seven panel launchers from their DAT panel-id attributes, route mounted panels through event-driven retained window state, and ghost unavailable panels. Port Use and Examine selection/target behavior with exact Appraise dispatch and retail cursor modes.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-11 12:11:53 +02:00
parent 21fefce0e0
commit d3d1c895a0
19 changed files with 478 additions and 72 deletions

View file

@ -45,13 +45,19 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
private static readonly uint[] CombatIndicatorIds =
{ 0x10000192u, 0x10000193u, 0x10000194u, 0x10000195u };
private const uint CharacterButtonId = 0x10000199u;
private const uint PanelIdAttribute = 0x10000029u;
private const uint UseButtonId = 0x1000019Du;
private const uint ExamineButtonId = 0x100001A5u;
private const uint InventoryButtonId = 0x100001B1u;
private static readonly uint[] PanelButtonIds =
{ 0x10000197u, 0x10000198u, 0x10000199u, 0x1000055Au, 0x1000019Au, 0x1000019Bu, InventoryButtonId };
private readonly UiItemList?[] _slots = new UiItemList?[SlotIds.Length];
private readonly UiElement?[] _combatIndicators = new UiElement?[CombatIndicatorIds.Length];
private readonly UiButton? _characterButton;
private readonly UiButton? _inventoryButton;
private readonly UiButton? _useButton;
private readonly UiButton? _examineButton;
private readonly List<(uint PanelId, UiButton Button)> _panelButtons = new();
private readonly ClientObjectTable _repo;
private readonly CombatState? _combatState;
private readonly Func<IReadOnlyList<ShortcutEntry>> _shortcuts;
@ -63,6 +69,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
private readonly Action<uint>? _sendRemoveShortcut; // (index)
private readonly ItemInteractionController? _itemInteraction;
private readonly Action<uint>? _selectItem;
private readonly Func<uint> _selectedObjectId;
private readonly Func<uint>? _playerGuid;
private readonly Action<uint, uint, int>? _sendPutItemInContainer;
private bool _disposed;
@ -94,6 +101,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
Action<uint>? sendRemoveShortcut = null,
Action? toggleCombat = null,
Action<uint>? selectItem = null,
Func<uint>? selectedObjectId = null,
Func<uint>? playerGuid = null,
Action<uint, uint, int>? sendPutItemInContainer = null)
{
@ -109,6 +117,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
_sendAddShortcut = sendAddShortcut;
_sendRemoveShortcut = sendRemoveShortcut;
_selectItem = selectItem;
_selectedObjectId = selectedObjectId ?? (() => 0u);
_playerGuid = playerGuid;
_sendPutItemInContainer = sendPutItemInContainer;
@ -140,7 +149,13 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
button.OnClick = toggleCombat;
}
_characterButton = layout.FindElement(CharacterButtonId) as UiButton;
foreach (uint elementId in PanelButtonIds)
{
if (layout.FindElement(elementId) is UiButton panelButton
&& panelButton.TryGetEnumAttribute(PanelIdAttribute, out uint panelId))
_panelButtons.Add((panelId, panelButton));
}
_inventoryButton = layout.FindElement(InventoryButtonId) as UiButton;
if (_inventoryButton is not null)
{
@ -152,6 +167,13 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
_inventoryButton.OnItemDrop = HandleInventoryButtonDrop;
}
_useButton = layout.FindElement(UseButtonId) as UiButton;
_examineButton = layout.FindElement(ExamineButtonId) as UiButton;
if (_useButton is not null)
_useButton.OnClick = () => _itemInteraction?.UseSelectedOrEnterMode(_selectedObjectId());
if (_examineButton is not null)
_examineButton.OnClick = () => _itemInteraction?.ExamineSelectedOrEnterMode(_selectedObjectId());
// Port of gmToolbarUI::RecvNotice_SetCombatMode (acclient_2013_pseudo_c.txt:196632-196669):
// exactly one indicator visible at a time. Default to NonCombat (peace) — the player
// always spawns in peace mode; retail has not yet called SetVisible when PostInit runs.
@ -240,42 +262,54 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
Action<uint>? sendRemoveShortcut = null,
Action? toggleCombat = null,
Action<uint>? selectItem = null,
Func<uint>? selectedObjectId = null,
Func<uint>? playerGuid = null,
Action<uint, uint, int>? sendPutItemInContainer = null)
{
var c = new ToolbarController(layout, repo, shortcuts, iconIds, useItem, combatState,
peaceDigits, warDigits, emptyDigits, itemInteraction,
sendAddShortcut, sendRemoveShortcut, toggleCombat, selectItem,
playerGuid, sendPutItemInContainer);
selectedObjectId, playerGuid, sendPutItemInContainer);
c.Populate();
return c;
}
/// <summary>Wire the retail status-bar launch buttons to top-level window toggles.</summary>
public void BindWindowToggles(Action? toggleInventory, Action? toggleCharacter)
/// <summary>
/// Binds all seven authored panel buttons by DAT panel id. Buttons whose panel
/// is not registered are ghosted, matching retail unavailable-panel behavior.
/// </summary>
public void BindPanelButtons(Func<uint, bool> isAvailable, Action<uint> togglePanel)
{
if (_inventoryButton is not null)
ArgumentNullException.ThrowIfNull(isAvailable);
ArgumentNullException.ThrowIfNull(togglePanel);
foreach (var (panelId, button) in _panelButtons)
{
_inventoryButton.OnClick = () =>
bool available = isAvailable(panelId);
button.Enabled = available;
button.OnClick = !available ? null : () =>
{
if (_itemInteraction?.AcquireSelfTarget() == true) return;
toggleInventory?.Invoke();
if (ReferenceEquals(button, _inventoryButton)
&& _itemInteraction?.OfferSelfPrimaryClick()
is not null and not ItemPrimaryClickResult.NotActive)
return;
togglePanel(panelId);
};
}
if (_characterButton is not null)
_characterButton.OnClick = toggleCharacter;
}
public void SetInventoryOpen(bool open) => SetButtonOpen(_inventoryButton, open);
public void SetCharacterOpen(bool open) => SetButtonOpen(_characterButton, open);
private static void SetButtonOpen(UiButton? button, bool open)
public void SetPanelOpen(uint panelId, bool open)
{
if (button is not null)
button.TrySetRetailState(open
// gmToolbarUI::RecvNotice_SetPanelVisibility @ 0x004BD300 maps visible
// to state 6 (Highlight) and hidden to state 1 (Normal).
foreach (var entry in _panelButtons)
{
if (entry.PanelId != panelId || !entry.Button.Enabled) continue;
entry.Button.TrySetRetailState(open
? UiButtonStateMachine.Highlight
: UiButtonStateMachine.Normal);
return;
}
}
/// <summary>
@ -584,6 +618,21 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
if (_disposed) return;
_disposed = true;
foreach (var (_, button) in _panelButtons)
button.OnClick = null;
foreach (var indicator in _combatIndicators)
if (indicator is UiButton button)
button.OnClick = null;
if (_useButton is not null)
_useButton.OnClick = null;
if (_examineButton is not null)
_examineButton.OnClick = null;
if (_inventoryButton is not null)
{
_inventoryButton.OnItemDragOver = null;
_inventoryButton.OnItemDrop = null;
}
if (_combatState is not null)
_combatState.CombatModeChanged -= SetCombatMode;
_repo.ObjectAdded -= OnRepositoryObjectChanged;