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(); } }