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, IRetainedPanelController { // 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, }; // SelectedObjectController owns the health/mana meters and both stack controls, // including retail's initial-hidden state and selection-driven visibility. // 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 const uint PanelIdAttribute = 0x10000029u; private const uint UseButtonId = 0x1000019Du; private const uint ExamineButtonId = 0x100001A5u; private const uint AmmoIndicatorId = 0x10000194u; private const uint InventoryButtonId = 0x100001B1u; private static readonly uint[] PanelButtonIds = { 0x10000197u, 0x10000198u, 0x10000199u, 0x1000055Au, 0x1000019Au, 0x1000019Bu, InventoryButtonId }; private readonly UiItemList?[] _slots = new UiItemList?[SlotIds.Length]; private readonly UiElement?[] _combatIndicators = new UiElement?[CombatIndicatorIds.Length]; private readonly UiButton? _inventoryButton; private readonly UiButton? _useButton; private readonly UiButton? _examineButton; private readonly UiButton? _ammoIndicator; private readonly List<(uint PanelId, UiButton Button)> _panelButtons = new(); private readonly ClientObjectTable _repo; private readonly CombatState? _combatState; private readonly Func> _shortcuts; private readonly Func _iconIds; // (itemType, icon, underlay, overlay, effects) → GL tex private readonly Func? _dragIconIds; private readonly Action _useItem; // guid → fire UseObject private readonly ShortcutStore _store = new(); private bool _storeLoaded; private readonly Action? _sendAddShortcut; private readonly Action? _sendRemoveShortcut; // (index) private readonly ItemInteractionController? _itemInteraction; private readonly Action? _selectItem; private readonly Func _selectedObjectId; private readonly Func? _playerGuid; private readonly Action? _sendPutItemInContainer; private uint _ammoObjectId; private bool _disposed; // 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 ghosting. // Occupancy branch (decomp 229481): // occupied → regular 0x10000042 / ghosted 0x10000043 // empty → background digit 0x1000005e (stance-independent) private uint[]? _regularDigits; private uint[]? _ghostedDigits; private uint[]? _emptyDigits; private bool _shortcutsGhosted; private ToolbarController( ImportedLayout layout, ClientObjectTable repo, Func> shortcuts, Func iconIds, Action useItem, CombatState? combatState, uint[]? regularDigits, uint[]? ghostedDigits, uint[]? emptyDigits, ItemInteractionController? itemInteraction = null, Action? sendAddShortcut = null, Action? sendRemoveShortcut = null, Action? toggleCombat = null, Action? selectItem = null, Func? selectedObjectId = null, Func? playerGuid = null, Action? sendPutItemInContainer = null, UiDatFont? ammoFont = null, Func? dragIconIds = null) { _repo = repo; _combatState = combatState; _shortcuts = shortcuts; _iconIds = iconIds; _dragIconIds = dragIconIds; _useItem = useItem; _regularDigits = regularDigits; _ghostedDigits = ghostedDigits; _emptyDigits = emptyDigits; _itemInteraction = itemInteraction; _sendAddShortcut = sendAddShortcut; _sendRemoveShortcut = sendRemoveShortcut; _selectItem = selectItem; _selectedObjectId = selectedObjectId ?? (() => 0u); _playerGuid = playerGuid; _sendPutItemInContainer = sendPutItemInContainer; 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. // Retail gmToolbarUI::ListenToElementMessage @ 0x004BEE90 handles // Click (message 1) from ANY id 0x10000192..0x10000195 by calling // ClientCombatSystem::ToggleCombatMode. Exactly one is visible, but all // four retain the same command binding as stance changes swap the art. for (int i = 0; i < CombatIndicatorIds.Length; i++) { _combatIndicators[i] = layout.FindElement(CombatIndicatorIds[i]); if (_combatIndicators[i] is UiButton button) button.OnClick = toggleCombat; } _ammoIndicator = layout.FindElement(AmmoIndicatorId) as UiButton; if (_ammoIndicator is not null) { _ammoIndicator.LabelFont = ammoFont; _ammoIndicator.LabelColor = System.Numerics.Vector4.One; } foreach (uint elementId in PanelButtonIds) { if (layout.FindElement(elementId) is UiButton panelButton && panelButton.TryGetEnumAttribute(PanelIdAttribute, out uint panelId)) _panelButtons.Add((panelId, panelButton)); } _inventoryButton = layout.FindElement(InventoryButtonId) as UiButton; if (_inventoryButton is not null) { // gmToolbarUI::HandleInventoryButtonDragOver @ 0x004BD180 uses the // authored child overlay's Accept state (0x10000046 = 0x060011F7). // UiButton owns the retained drop-target seam; the panel owns policy. _inventoryButton.ItemDragAcceptSprite = 0x060011F7u; _inventoryButton.OnItemDragOver = InventoryButtonDragOver; _inventoryButton.OnItemDrop = HandleInventoryButtonDrop; } _useButton = layout.FindElement(UseButtonId) as UiButton; _examineButton = layout.FindElement(ExamineButtonId) as UiButton; if (_useButton is not null) _useButton.OnClick = () => _itemInteraction?.UseSelectedOrEnterMode(_selectedObjectId()); if (_examineButton is not null) _examineButton.OnClick = () => _itemInteraction?.ExamineSelectedOrEnterMode(_selectedObjectId()); // 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 += OnRepositoryObjectChanged; repo.ObjectUpdated += OnRepositoryObjectChanged; repo.ObjectRemoved += OnRepositoryObjectChanged; repo.ObjectMoved += OnRepositoryObjectMoved; repo.Cleared += OnRepositoryCleared; RefreshAmmo(); } private void OnRepositoryObjectChanged(ClientObject obj) { if (IsAmmoRelated(obj)) RefreshAmmo(); if (IsShortcutGuid(obj.ObjectId)) Populate(); } private void OnRepositoryObjectMoved(ClientObjectMove move) { uint player = _playerGuid?.Invoke() ?? 0u; if (move.ItemId == _ammoObjectId || (player != 0 && (move.Previous.ContainerId == player || move.Current.ContainerId == player || move.Previous.WielderId == player || move.Current.WielderId == player))) RefreshAmmo(); if (IsShortcutGuid(move.ItemId)) Populate(); } private void OnRepositoryCleared() { RefreshAmmo(); Populate(); } private bool IsAmmoRelated(ClientObject obj) { uint player = _playerGuid?.Invoke() ?? 0u; return obj.ObjectId == _ammoObjectId || (player != 0 && obj.ObjectId == player) || (player != 0 && (obj.WielderId == player || obj.ContainerId == player) && (obj.CurrentlyEquippedLocation & (EquipMask.MissileWeapon | EquipMask.MissileAmmo)) != 0); } private void RefreshAmmo() { uint player = _playerGuid?.Invoke() ?? 0u; IReadOnlyList placements = player != 0 ? _repo.GetEquippedBy(player) : Array.Empty(); ToolbarAmmoPolicy.Result result = ToolbarAmmoPolicy.Resolve(placements); _ammoObjectId = result.ObjectId; if (_ammoIndicator is not null) { _ammoIndicator.Label = result.IsVisible ? result.DisplayCount.ToString(System.Globalization.CultureInfo.InvariantCulture) : null; } } /// /// 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.ObjectId == 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). /// /// /// Regular 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). /// /// Ghosted 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[]? regularDigits = null, uint[]? ghostedDigits = null, uint[]? emptyDigits = null, ItemInteractionController? itemInteraction = null, Action? sendAddShortcut = null, Action? sendRemoveShortcut = null, Action? toggleCombat = null, Action? selectItem = null, Func? selectedObjectId = null, Func? playerGuid = null, Action? sendPutItemInContainer = null, UiDatFont? ammoFont = null, Func? dragIconIds = null) { var c = new ToolbarController(layout, repo, shortcuts, iconIds, useItem, combatState, regularDigits, ghostedDigits, emptyDigits, itemInteraction, sendAddShortcut, sendRemoveShortcut, toggleCombat, selectItem, selectedObjectId, playerGuid, sendPutItemInContainer, ammoFont, dragIconIds); c.Populate(); return c; } /// /// Binds all seven authored panel buttons by DAT panel id. Buttons whose panel /// is not registered are ghosted, matching retail unavailable-panel behavior. /// public void BindPanelButtons(Func isAvailable, Action togglePanel) { ArgumentNullException.ThrowIfNull(isAvailable); ArgumentNullException.ThrowIfNull(togglePanel); foreach (var (panelId, button) in _panelButtons) { bool available = isAvailable(panelId); button.Enabled = available; button.OnClick = !available ? null : () => { if (ReferenceEquals(button, _inventoryButton) && _itemInteraction?.OfferSelfPrimaryClick() is not null and not ItemPrimaryClickResult.NotActive) return; togglePanel(panelId); }; } } public void SetPanelOpen(uint panelId, bool open) { // gmToolbarUI::RecvNotice_SetPanelVisibility @ 0x004BD300 maps visible // to state 6 (Highlight) and hidden to state 1 (Normal). foreach (var entry in _panelButtons) { if (entry.PanelId != panelId || !entry.Button.Enabled) continue; entry.Button.TrySetRetailState(open ? UiButtonStateMachine.Highlight : UiButtonStateMachine.Normal); return; } } /// /// 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(_shortcuts()); _storeLoaded = true; } foreach (var list in _slots) list?.Cell.Clear(); for (int slot = 0; slot < _slots.Length; slot++) { ShortcutEntry? entry = _store.GetEntry(slot); uint guid = entry?.ObjectId ?? 0u; 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); uint dragTex = _dragIconIds?.Invoke( item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects) ?? 0u; list.Cell.SetItem(guid, tex, entry, dragTex); } // Re-stamp slot number labels after any item change. // Digit SPRITE SOURCE depends on occupancy (decomp UIElement_UIItem::SetShortcutNum:229481): // occupied → regular 0x10000042 / ghosted 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]; } // The bool passed to SetShortcutNum is m_bShortcutGhosted, not a peace/war // selector. Retail ghosts physical-item shortcuts only in Magic mode. // Retail ref: gmToolbarUI::RecvNotice_SetCombatMode @ 0x004BD610. _shortcutsGhosted = mode == CombatMode.Magic; RestampShortcutNumbers(); } /// /// Push digit-array references and shortcut-number state into every slot cell. /// Top row (indices 0–8): SetShortcutNum(i, _shortcutsGhosted) — 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 → regular 0x10000042 / ghosted 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.RegularDigits = _regularDigits; cell.GhostedDigits = _ghostedDigits; cell.EmptyDigits = _emptyDigits; if (i < 9) cell.SetShortcutNum(i, _shortcutsGhosted); // top row: slot labels 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) { if (_itemInteraction is not null) _itemInteraction.ActivateItem(list.Cell.ItemId); else _useItem(list.Cell.ItemId); } }; } private static ItemDragAcceptance InventoryButtonDragOver(ItemDragPayload payload) => payload.ObjId != 0 && payload.SourceKind != ItemDragSource.ShortcutBar ? ItemDragAcceptance.Accept : ItemDragAcceptance.None; private void HandleInventoryButtonDrop(ItemDragPayload payload) { // gmToolbarUI::HandleDropRelease @ 0x004BE7C0 inventory-button branch: // only fresh physical items are placed into the player's main inventory. // A shortcut alias remains neutral and its remove-on-lift result stands. if (payload.ObjId == 0 || payload.SourceKind == ItemDragSource.ShortcutBar) return; uint player = _playerGuid?.Invoke() ?? 0u; if (player == 0 || _repo.Get(payload.ObjId) is null) return; _sendPutItemInContainer?.Invoke(payload.ObjId, player, 0); } /// /// Port of gmToolbarUI::UseShortcut @ 0x004BD350. Target mode /// is one-shot and precedes both normal use and selection. /// public bool UseShortcut(int slot, bool use) { if ((uint)slot >= _slots.Length || _slots[slot]?.Cell is not { } cell) return false; uint itemId = cell.ItemId; if (_itemInteraction?.IsTargetModeActive == true) { if (itemId != 0) _itemInteraction.OfferPrimaryClick(itemId); else _itemInteraction.CancelTargetMode(); return true; } if (itemId == 0) return false; if (use) { if (_itemInteraction is not null) _itemInteraction.ActivateItem(itemId); else _useItem(itemId); } else { _selectItem?.Invoke(itemId); } return true; } /// /// Auto-slot form of retail gmToolbarUI::CreateShortcutToItem @ /// 0x004BDAC0, used by the global CreateShortcut action. It rejects /// stuck/non-player objects, non-player creatures, unowned objects, /// duplicates, and a full bar before emitting AddShortcut. /// public bool CreateShortcutToItem(uint itemId) { if (itemId == 0 || _repo.Get(itemId) is not { } item) return false; var flags = (PublicWeenieFlags)(item.PublicWeenieBitfield ?? 0u); bool isPlayer = itemId == _itemInteraction?.PlayerGuid || (flags & PublicWeenieFlags.Player) != 0; if ((flags & PublicWeenieFlags.Stuck) != 0 && !isPlayer) return false; if ((item.Type & ItemType.Creature) != 0 && !isPlayer) return false; if (_itemInteraction?.IsOwnedByPlayer(itemId) != true) return false; EnsureStoreLoadedForMutation(); for (int slot = 0; slot < _slots.Length; slot++) if (_store.Get(slot) == itemId) return false; int empty = -1; for (int slot = 0; slot < _slots.Length; slot++) { if (_store.IsEmpty(slot)) { empty = slot; break; } } if (empty < 0) return false; var entry = new ShortcutEntry(empty, itemId, 0u); _store.Set(entry); _sendAddShortcut?.Invoke(entry); Populate(); return true; } private void EnsureStoreLoadedForMutation() { if (_storeLoaded) return; _store.Load(_shortcuts()); _storeLoaded = true; } // ── 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) { // UIElement_ItemList::ItemList_BeginDrag @ 0x004E32D0 selects first; the toolbar's // RecvNotice_ItemListBeginDrag handler then removes the shortcut alias. if (payload.ObjId != 0 && _selectedObjectId() != payload.ObjId) _selectItem?.Invoke(payload.ObjId); // 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 ItemDragAcceptance OnDragOver(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload) => payload.ObjId != 0 ? ItemDragAcceptance.Accept : ItemDragAcceptance.None; /// public void HandleDropRelease(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload) { EnsureStoreLoadedForMutation(); ShortcutDropSource source = payload.SourceKind == ItemDragSource.ShortcutBar ? ShortcutDropSource.ShortcutAlias : ShortcutDropSource.FreshItem; ShortcutEntry dragged = payload.Shortcut ?? new ShortcutEntry(payload.SourceSlot, payload.ObjId, 0u); ShortcutMutation[] plan = ShortcutDropPlanner.PlanDrop( _store.Snapshot(), source, payload.SourceSlot, targetCell.SlotIndex, dragged); ApplyShortcutPlan(plan); Populate(); } /// /// Toolbar notice port for gmToolbarUI::RecvNotice_FullMergingItem @ 0x004BE9B0. /// The owning inventory pipeline calls this when ACE confirms a source stack was fully merged. /// public void ReplaceFullyMergedShortcut(uint oldObjectId, uint newObjectId) { EnsureStoreLoadedForMutation(); ShortcutMutation[] plan = ShortcutDropPlanner.PlanFullStackMerge( _store.Snapshot(), oldObjectId, newObjectId); ApplyShortcutPlan(plan); if (plan.Length > 0) Populate(); } private void ApplyShortcutPlan(IReadOnlyList plan) { // Apply the already-validated transaction locally first, then reproduce the // planner's exact Remove/Add wire order. Shortcut events have no rejection reply. foreach (var mutation in plan) { if (mutation.Kind == ShortcutMutationKind.Remove) _store.Remove(mutation.Slot); else if (mutation.Entry is { } entry) _store.Set(entry); } foreach (var mutation in plan) { if (mutation.Kind == ShortcutMutationKind.Remove) _sendRemoveShortcut?.Invoke((uint)mutation.Slot); else if (mutation.Entry is { } entry) _sendAddShortcut?.Invoke(entry); } } public void Dispose() { if (_disposed) return; _disposed = true; foreach (var (_, button) in _panelButtons) button.OnClick = null; foreach (var indicator in _combatIndicators) if (indicator is UiButton button) button.OnClick = null; if (_useButton is not null) _useButton.OnClick = null; if (_examineButton is not null) _examineButton.OnClick = null; if (_inventoryButton is not null) { _inventoryButton.OnItemDragOver = null; _inventoryButton.OnItemDrop = null; } if (_combatState is not null) _combatState.CombatModeChanged -= SetCombatMode; _repo.ObjectAdded -= OnRepositoryObjectChanged; _repo.ObjectUpdated -= OnRepositoryObjectChanged; _repo.ObjectRemoved -= OnRepositoryObjectChanged; _repo.ObjectMoved -= OnRepositoryObjectMoved; _repo.Cleared -= OnRepositoryCleared; } }