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 : IItemListDragHandler
{
// 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.
// Ids confirmed from the toolbar LayoutDesc dump.
// 0x100001A1 (health meter) is now OWNED by SelectedObjectController (D.5.3a) —
// it hides A1 at bind and shows it on a health-target selection, so A1 is removed
// from here to avoid double-ownership. 0x100001A2 (mana meter), 0x100001A3 (stack-size
// entry box) and 0x100001A4 (stack slider) are DEFERRED features (mana #140, stack-split
// UI) with no controller yet, so they stay hidden here — otherwise their dat sprites
// render as stray bars / a black box on the toolbar. Retail hides A3/A4 in
// gmToolbarUI::HandleSelectionChanged (acclient_2013_pseudo_c.txt:198660/198742),
// showing them only for a stacked-item selection.
private static readonly uint[] HiddenIds = { 0x100001A2, 0x100001A3, 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 ClientObjectTable _repo;
private readonly Func> _shortcuts;
private readonly Func _iconIds; // (itemType, icon, underlay, overlay, effects) → GL tex
private readonly Action _useItem; // guid → fire UseObject
private readonly AcDream.Core.Items.ShortcutStore _store = new();
private bool _storeLoaded;
private readonly Action? _sendAddShortcut; // (index, objectGuid)
private readonly Action? _sendRemoveShortcut; // (index)
// Digit sprite DID arrays for slot labels (top row, numbers 1-9).
// Read from LayoutDesc 0x21000037, element 0x1000034A under composite 0x10000346.
// Retail ref: UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465);
// gmToolbarUI::RecvNotice_SetCombatMode (196610-196621) re-stamps per stance.
// Occupancy branch (decomp 229481):
// occupied → peace 0x10000042 / war 0x10000043 (split by stance)
// empty → background digit 0x1000005e (stance-independent)
private uint[]? _peaceDigits;
private uint[]? _warDigits;
private uint[]? _emptyDigits;
private bool _peace = true; // true = NonCombat (peace), false = any war stance
private ToolbarController(
ImportedLayout layout,
ClientObjectTable repo,
Func> shortcuts,
Func iconIds,
Action useItem,
CombatState? combatState,
uint[]? peaceDigits,
uint[]? warDigits,
uint[]? emptyDigits,
Action? sendAddShortcut = null,
Action? sendRemoveShortcut = null)
{
_repo = repo;
_shortcuts = shortcuts;
_iconIds = iconIds;
_useItem = useItem;
_peaceDigits = peaceDigits;
_warDigits = warDigits;
_emptyDigits = emptyDigits;
_sendAddShortcut = sendAddShortcut;
_sendRemoveShortcut = sendRemoveShortcut;
for (int i = 0; i < SlotIds.Length; i++)
{
_slots[i] = layout.FindElement(SlotIds[i]) as UiItemList;
if (_slots[i] is { } list)
{
WireClick(list);
// B.1 drag-drop spine: this controller is the drop handler for every
// toolbar slot list; each cell knows its slot index + that it's a
// shortcut-bar source (retail UIElement_ItemList::RegisterItemListDragHandler).
list.RegisterDragHandler(this);
list.Cell.SlotIndex = i;
list.Cell.SourceKind = ItemDragSource.ShortcutBar;
list.Cell.DragAcceptSprite = 0x060011FAu; // green cross (toolbar), not the ring 0x060011F9 (inventory)
}
}
// 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;
// D.5.4: the table now holds ALL objects (creatures, NPCs, etc.), so filter
// to our 18 shortcut guids — else every creature spawn in a busy zone
// needlessly re-populates the bar (gmToolbarUI::SetDelayedShortcutNum pattern).
repo.ObjectAdded += o => { if (IsShortcutGuid(o.ObjectId)) Populate(); };
repo.ObjectUpdated += o => { if (IsShortcutGuid(o.ObjectId)) Populate(); };
repo.ObjectRemoved += o => { if (IsShortcutGuid(o.ObjectId)) Populate(); };
}
///
/// Returns true if is one of the currently-active shortcut guids
/// (i.e., present in the live ).
/// Used to gate repo-event subscriptions so we don't re-populate on every creature spawn.
/// Falls back to scanning _shortcuts() before the store is loaded (pre-PD window).
///
private bool IsShortcutGuid(uint guid)
{
if (_storeLoaded)
{
for (int s = 0; s < AcDream.Core.Items.ShortcutStore.SlotCount; s++)
if (_store.Get(s) == guid) return true;
return false;
}
// Store not yet loaded — fall back to the shortcuts provider (pre-PD window).
foreach (var sc in _shortcuts())
if (sc.ObjectGuid == guid) return true;
return false;
}
///
/// 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, effects) → 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).
///
///
/// Peace-mode digit DID array (property 0x10000042 from LayoutDesc 0x21000037 element
/// 0x1000034A under composite 0x10000346). Index i → slot label digit (i+1) RenderSurface id.
/// Null if the dat lookup failed (no digits drawn). Retail reference:
/// UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465).
///
/// War-mode digit DID array (property 0x10000043, same element).
///
/// Empty-slot background digit DID array (property 0x1000005e, stance-independent).
/// Used when a slot is EMPTY (ItemId == 0). Retail ref: UIElement_UIItem::SetShortcutNum
/// (decomp 229481) — else branch when m_elem_Icon->m_state == 0x1000001c (empty state).
/// Null if the dat lookup failed (empty slots draw no digit, which is safe).
///
public static ToolbarController Bind(
ImportedLayout layout,
ClientObjectTable repo,
Func> shortcuts,
Func iconIds,
Action useItem,
CombatState? combatState = null,
uint[]? peaceDigits = null,
uint[]? warDigits = null,
uint[]? emptyDigits = null,
Action? sendAddShortcut = null,
Action? sendRemoveShortcut = null)
{
var c = new ToolbarController(layout, repo, shortcuts, iconIds, useItem, combatState,
peaceDigits, warDigits, emptyDigits,
sendAddShortcut, sendRemoveShortcut);
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
/// ObjectAdded event re-fires this method when the item arrives
/// (matching retail's SetDelayedShortcutNum deferred-rebind path).
/// As of B.2: the is the
/// authoritative in-session slot map; _shortcuts() seeds it on first call.
///
public void Populate()
{
// Lazy-load the store from the login shortcut list the first time it's present (PD arrives
// after Bind); thereafter the store is authoritative and drag ops mutate it directly.
if (!_storeLoaded && _shortcuts().Count > 0)
{
_store.Load(System.Linq.Enumerable.Select(_shortcuts(), e => ((int)e.Index, e.ObjectGuid)));
_storeLoaded = true;
}
foreach (var list in _slots) list?.Cell.Clear();
for (int slot = 0; slot < _slots.Length; slot++)
{
uint guid = _store.Get(slot);
if (guid == 0) continue;
var list = _slots[slot];
if (list is null) continue;
var item = _repo.Get(guid);
if (item is null) continue; // deferred: ObjectAdded re-calls Populate
uint tex = _iconIds(item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects);
list.Cell.SetItem(guid, tex);
}
// Re-stamp slot number labels after any item change.
// Digit SPRITE SOURCE depends on occupancy (decomp UIElement_UIItem::SetShortcutNum:229481):
// occupied → peace 0x10000042 / war 0x10000043; empty → background 0x1000005e.
// The digit is ALWAYS shown on top-row slots (SetVisible(1) at decomp 229511).
RestampShortcutNumbers();
}
///
/// 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];
}
// Re-stamp digit set: peace glyphs in NonCombat, war glyphs in any combat stance.
// Retail ref: gmToolbarUI::RecvNotice_SetCombatMode (acclient_2013_pseudo_c.txt:196610-196621).
_peace = (mode == CombatMode.NonCombat);
RestampShortcutNumbers();
}
///
/// Push digit-array references and shortcut-number state into every slot cell.
/// Top row (indices 0–8): SetShortcutNum(i, _peace) — numbers 1–9 always shown
/// (the digit is ALWAYS visible, SetVisible(1) at decomp 229511; only the sprite
/// SOURCE differs by occupancy — see UIElement_UIItem::SetShortcutNum decomp 229481).
/// Bottom row (indices 9–17): ClearShortcutNum() — retail shows no numbers there.
/// Retail ref: UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465);
/// gmToolbarUI::RecvNotice_SetCombatMode (196610-196621).
/// Occupancy → source: occupied → peace 0x10000042 / war 0x10000043;
/// empty → background 0x1000005e (decomp 229481/229493).
///
private void RestampShortcutNumbers()
{
for (int i = 0; i < _slots.Length; i++)
{
var cell = _slots[i]?.Cell;
if (cell is null) continue;
cell.PeaceDigits = _peaceDigits;
cell.WarDigits = _warDigits;
cell.EmptyDigits = _emptyDigits;
if (i < 9)
cell.SetShortcutNum(i, _peace); // top row: slot label digits 1–9 always shown
else
cell.ClearShortcutNum(); // bottom row: no slot labels
}
}
///
/// 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);
};
}
// ── IItemListDragHandler (B.2 live handler) ──────────────────────────────
// Retail: gmToolbarUI is the m_dragHandler for every shortcut slot list.
// Retail model (remove-on-lift / place-on-drop / no-restore):
// lift → RemoveShortcut (0x019D) + store.Remove (slot empties immediately)
// drop → AddShortcut (0x019C) + optional swap of evicted item into source
// off-bar release → the lift's removal stands (no restore)
// Retail ref: gmToolbarUI::HandleDropRelease acclient_2013_pseudo_c.txt:197971
///
public void OnDragLift(UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload)
{
// Retail RecvNotice_ItemListBeginDrag → RemoveShortcut (0x004bd930/0x004bd450): the lifted
// shortcut leaves the bar (+ wire) the instant the drag starts; it's re-placed only on a
// drop onto a slot. Off-bar release leaves it removed.
_store.Remove(payload.SourceSlot);
_sendRemoveShortcut?.Invoke((uint)payload.SourceSlot);
Populate();
}
///
public bool OnDragOver(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
=> payload.ObjId != 0;
///
public void HandleDropRelease(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
{
// Retail gmToolbarUI::HandleDropRelease (0x004be7c0) within-bar reorder branch (deep-dive §5):
// evict the target's occupant, place the dragged item there, and bump the evicted item into
// the (now-vacated) source slot if it's free.
int target = targetCell.SlotIndex;
uint evicted = _store.Get(target); // RemoveShortCutInSlotNum(target)
if (evicted != 0) { _store.Remove(target); _sendRemoveShortcut?.Invoke((uint)target); }
_store.Set(target, payload.ObjId); // AddShortcut(dragged, target)
_sendAddShortcut?.Invoke((uint)target, payload.ObjId);
if (evicted != 0 && evicted != payload.ObjId && _store.IsEmpty(payload.SourceSlot))
{ // displaced → vacated source slot
_store.Set(payload.SourceSlot, evicted);
_sendAddShortcut?.Invoke((uint)payload.SourceSlot, evicted);
}
Populate();
}
}