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, icon, underlay, overlay, effects) → GL tex private readonly Action _useItem; // guid → fire UseObject // 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, ItemRepository repo, Func> shortcuts, Func iconIds, Action useItem, CombatState? combatState, uint[]? peaceDigits, uint[]? warDigits, uint[]? emptyDigits) { _repo = repo; _shortcuts = shortcuts; _iconIds = iconIds; _useItem = useItem; _peaceDigits = peaceDigits; _warDigits = warDigits; _emptyDigits = emptyDigits; 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, 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, ItemRepository repo, Func> shortcuts, Func iconIds, Action useItem, CombatState? combatState = null, uint[]? peaceDigits = null, uint[]? warDigits = null, uint[]? emptyDigits = null) { var c = new ToolbarController(layout, repo, shortcuts, iconIds, useItem, combatState, peaceDigits, warDigits, emptyDigits); 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, item.Effects); list.Cell.SetItem(sc.ObjectGuid, 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); }; } }