using System;
using System.Collections.Generic;
using AcDream.Core.Combat;
using AcDream.Core.Items;
using AcDream.Core.Net.Messages;
namespace AcDream.App.UI.Layout;
///
/// Binds the imported gmToolbarUI window (LayoutDesc 0x21000016) to live data —
/// the gm*UI::PostInit analogue. Finds the 18 shortcut slots (UiItemList) by id,
/// populates them from the persisted PlayerDescription shortcuts
/// (UpdateFromPlayerDesc), re-binds deferred slots when an item's CreateObject
/// arrives (SetDelayedShortcutNum), and on click uses the bound item
/// (UseShortcut -> ItemHolder::UseObject -> use-item callback).
///
///
/// Retail reference: gmToolbarUI::PostInit grabs each slot widget by its
/// id, calls UpdateFromPlayerDesc to flush-and-bind shortcuts from the
/// PlayerDescription trailer, and hooks OnEvent for the Click case to fire
/// UseShortcut. The deferred-rebind path matches
/// gmToolbarUI::SetDelayedShortcutNum which re-tries binding after
/// CreateObject resolves a formerly-unknown guid.
///
///
public sealed class ToolbarController
{
// Slot element ids in slot-index order (toolbar LayoutDesc 0x21000016, pre-dump).
// Row 1 = slots 0-8 (0x100001A7..0x100001AF), Row 2 = slots 9-17 (0x100006B7..0x100006BF).
private static readonly uint[] SlotIds =
{
0x100001A7, 0x100001A8, 0x100001A9, 0x100001AA, 0x100001AB,
0x100001AC, 0x100001AD, 0x100001AE, 0x100001AF,
0x100006B7, 0x100006B8, 0x100006B9, 0x100006BA, 0x100006BB,
0x100006BC, 0x100006BD, 0x100006BE, 0x100006BF,
};
// Elements hidden by default in retail gmToolbarUI::PostInit: the selected-object
// vitals meters (health/stamina/mana bars that track your target) and the stack slider.
// Ids confirmed from the toolbar LayoutDesc dump.
private static readonly uint[] HiddenIds = { 0x100001A1, 0x100001A2, 0x100001A4 };
// Four mutually-exclusive combat-mode indicator elements — exactly one visible at a time.
// Index 0 = NonCombat (peace), 1 = Melee, 2 = Missile, 3 = Magic.
// Retail ref: gmToolbarUI::RecvNotice_SetCombatMode (acclient_2013_pseudo_c.txt:196632-196669)
// SetVisible's exactly one element depending on the incoming mode.
private static readonly uint[] CombatIndicatorIds =
{ 0x10000192u, 0x10000193u, 0x10000194u, 0x10000195u };
private readonly UiItemList?[] _slots = new UiItemList?[SlotIds.Length];
private readonly UiElement?[] _combatIndicators = new UiElement?[CombatIndicatorIds.Length];
private readonly ItemRepository _repo;
private readonly Func> _shortcuts;
private readonly Func _iconIds; // (itemType, iconId, underlayId, overlayId) → GL tex
private readonly Action _useItem; // guid → fire UseObject
private ToolbarController(
ImportedLayout layout,
ItemRepository repo,
Func> shortcuts,
Func iconIds,
Action useItem,
CombatState? combatState)
{
_repo = repo;
_shortcuts = shortcuts;
_iconIds = iconIds;
_useItem = useItem;
for (int i = 0; i < SlotIds.Length; i++)
{
_slots[i] = layout.FindElement(SlotIds[i]) as UiItemList;
if (_slots[i] is { } list)
WireClick(list);
}
// Cache the four mutually-exclusive combat-mode indicator elements.
for (int i = 0; i < CombatIndicatorIds.Length; i++)
_combatIndicators[i] = layout.FindElement(CombatIndicatorIds[i]);
// Hide target-object meters + stack slider (gmToolbarUI::PostInit).
foreach (var id in HiddenIds)
if (layout.FindElement(id) is { } e) e.Visible = false;
// 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.
SetCombatMode(CombatMode.NonCombat);
// Wire live combat-mode changes if a CombatState was provided.
if (combatState is not null)
combatState.CombatModeChanged += SetCombatMode;
// Re-bind any deferred slot whenever the repo learns about a new/updated item.
repo.ItemAdded += _ => Populate();
repo.ItemPropertiesUpdated += _ => Populate();
}
///
/// Create and bind a to .
/// Calls immediately (binds whatever items are in the repo now).
/// Returns the controller so the caller can call again
/// if the shortcut list is refreshed outside the repo-event path.
///
/// Imported toolbar layout (LayoutDesc 0x21000016).
/// Live item repository — must stay alive for the controller's lifetime.
/// Provider for the current shortcut bar list.
/// Resolves (itemType, iconId, underlayId, overlayId) → GL texture handle.
/// Callback fired when a bound slot is clicked; receives the item guid.
///
/// Optional live combat state — when provided, the toolbar subscribes to
/// and updates the four mutually-exclusive
/// combat-mode indicator elements accordingly.
/// Pass null to skip live wiring (e.g. in unit tests that don't exercise the indicator).
///
public static ToolbarController Bind(
ImportedLayout layout,
ItemRepository repo,
Func> shortcuts,
Func iconIds,
Action useItem,
CombatState? combatState = null)
{
var c = new ToolbarController(layout, repo, shortcuts, iconIds, useItem, combatState);
c.Populate();
return c;
}
///
/// Port of gmToolbarUI::UpdateFromPlayerDesc: clear all slots, then bind
/// each shortcut entry that has a resolved item in the repository.
/// Entries whose item is not yet in the repo are silently skipped here; the
/// ItemAdded event re-fires this method when the item arrives
/// (matching retail's SetDelayedShortcutNum deferred-rebind path).
///
public void Populate()
{
// Clear all slot cells first (flush).
foreach (var list in _slots) list?.Cell.Clear();
foreach (var sc in _shortcuts())
{
if (sc.ObjectGuid == 0) continue; // spell-only shortcut — inventory phase
if (sc.Index >= (uint)_slots.Length) continue;
var list = _slots[(int)sc.Index];
if (list is null) continue;
var item = _repo.GetItem(sc.ObjectGuid);
if (item is null) continue; // deferred: ItemAdded will re-call Populate
uint tex = _iconIds(item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId);
list.Cell.SetItem(sc.ObjectGuid, tex);
}
}
///
/// Port of gmToolbarUI::RecvNotice_SetCombatMode
/// (acclient_2013_pseudo_c.txt:196632-196669): show exactly one of the four
/// mutually-exclusive combat-mode indicator elements and hide the other three.
/// Called at bind-time with (the player
/// always starts in peace mode) and subsequently whenever
/// fires.
///
public void SetCombatMode(CombatMode mode)
{
// Index → mode mapping matches CombatIndicatorIds declaration order:
// 0 = NonCombat (peace), 1 = Melee, 2 = Missile, 3 = Magic.
bool[] show =
{
mode == CombatMode.NonCombat,
mode == CombatMode.Melee,
mode == CombatMode.Missile,
mode == CombatMode.Magic,
};
for (int i = 0; i < _combatIndicators.Length; i++)
{
if (_combatIndicators[i] is { } e)
e.Visible = show[i];
}
}
///
/// Wire the callback on a slot cell so that
/// clicking a bound item fires with the slot's current guid.
/// Mirrors retail's gmToolbarUI click → UseShortcut dispatch.
///
private void WireClick(UiItemList list)
{
list.Cell.Clicked = () =>
{
if (list.Cell.ItemId != 0)
_useItem(list.Cell.ItemId);
};
}
}