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