diff --git a/src/AcDream.App/UI/Layout/ToolbarController.cs b/src/AcDream.App/UI/Layout/ToolbarController.cs index 7143e1b1..e4fd00c4 100644 --- a/src/AcDream.App/UI/Layout/ToolbarController.cs +++ b/src/AcDream.App/UI/Layout/ToolbarController.cs @@ -60,7 +60,7 @@ public sealed class ToolbarController : IItemListDragHandler 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.Net.Items.ShortcutStore _store = new(); + private readonly AcDream.Core.Items.ShortcutStore _store = new(); private bool _storeLoaded; private readonly Action? _sendAddShortcut; // (index, objectGuid) private readonly Action? _sendRemoveShortcut; // (index) @@ -143,7 +143,7 @@ public sealed class ToolbarController : IItemListDragHandler /// /// Returns true if is one of the currently-active shortcut guids - /// (i.e., present in the live ). + /// (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). /// @@ -151,7 +151,7 @@ public sealed class ToolbarController : IItemListDragHandler { if (_storeLoaded) { - for (int s = 0; s < AcDream.Core.Net.Items.ShortcutStore.SlotCount; s++) + for (int s = 0; s < AcDream.Core.Items.ShortcutStore.SlotCount; s++) if (_store.Get(s) == guid) return true; return false; } @@ -217,14 +217,18 @@ public sealed class ToolbarController : IItemListDragHandler /// 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 + /// 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; } + 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(); @@ -350,7 +354,7 @@ public sealed class ToolbarController : IItemListDragHandler // 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) + 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); diff --git a/src/AcDream.Core.Net/Items/ShortcutStore.cs b/src/AcDream.Core/Items/ShortcutStore.cs similarity index 50% rename from src/AcDream.Core.Net/Items/ShortcutStore.cs rename to src/AcDream.Core/Items/ShortcutStore.cs index a927451d..0f581a2d 100644 --- a/src/AcDream.Core.Net/Items/ShortcutStore.cs +++ b/src/AcDream.Core/Items/ShortcutStore.cs @@ -1,28 +1,29 @@ using System; using System.Collections.Generic; -using AcDream.Core.Net.Messages; -namespace AcDream.Core.Net.Items; +namespace AcDream.Core.Items; /// /// Mutable client-side model of the 18 toolbar shortcut slots — port of retail /// ShortCutManager::shortCuts_[18] (acclient.h:36492). Holds the bound object guid per -/// slot (0 = empty). Loaded from the login PlayerDescription, then mutated by drag-drop -/// (lift removes, drop places); the server is notified via AddShortcut/RemoveShortcut so the -/// store stays the client's source of truth within a session (retail: client owns the array). -/// Item shortcuts only — spell shortcuts (ObjectGuid 0) are skipped on Load. +/// slot (0 = empty). Loaded from the login shortcut list, then mutated by drag-drop (lift +/// removes, drop places); the server is notified via AddShortcut/RemoveShortcut so the store +/// stays the client's source of truth within a session (retail: client owns the array). +/// Item shortcuts only — entries with ObjGuid 0 (spell-only) are skipped on Load. Pure model +/// (no Core.Net dependency): callers project their wire entries to (slot, objGuid) pairs. /// public sealed class ShortcutStore { public const int SlotCount = 18; private readonly uint[] _objIds = new uint[SlotCount]; - /// Replace all slots from the login PlayerDescription shortcut list (item entries only). - public void Load(IReadOnlyList entries) + /// Replace all slots from a (slot, objectGuid) sequence (item entries only; + /// ObjGuid 0 and out-of-range slots are skipped). + public void Load(IEnumerable<(int Slot, uint ObjGuid)> entries) { Array.Clear(_objIds); - foreach (var e in entries) - if (e.Index < SlotCount && e.ObjectGuid != 0) _objIds[(int)e.Index] = e.ObjectGuid; + foreach (var (slot, objGuid) in entries) + if ((uint)slot < SlotCount && objGuid != 0) _objIds[slot] = objGuid; } /// Bound object guid at , or 0 (empty / out of range). diff --git a/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs index c3e84dd4..6f590738 100644 --- a/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs @@ -581,6 +581,8 @@ public class ToolbarControllerTests Assert.Equal(0x5001u, slots[Row1[3]].Cell.ItemId); // re-added to source (net no-op) Assert.Contains((3u, 0x5001u), adds); // AddShortcut(3, A) sent + Assert.Single(removes); // exactly RemoveShortcut(3) [from the lift] + Assert.Single(adds); // exactly AddShortcut(3, A) [the re-place] } [Fact] diff --git a/tests/AcDream.Core.Net.Tests/Items/ShortcutStoreTests.cs b/tests/AcDream.Core.Tests/Items/ShortcutStoreTests.cs similarity index 53% rename from tests/AcDream.Core.Net.Tests/Items/ShortcutStoreTests.cs rename to tests/AcDream.Core.Tests/Items/ShortcutStoreTests.cs index 97a2b68e..a58dabd9 100644 --- a/tests/AcDream.Core.Net.Tests/Items/ShortcutStoreTests.cs +++ b/tests/AcDream.Core.Tests/Items/ShortcutStoreTests.cs @@ -1,24 +1,21 @@ -using System.Collections.Generic; -using AcDream.Core.Net.Items; -using AcDream.Core.Net.Messages; +using AcDream.Core.Items; using Xunit; -namespace AcDream.Core.Net.Tests.Items; +namespace AcDream.Core.Tests.Items; public class ShortcutStoreTests { [Fact] - public void Load_mapsIndexToObjId_skipsEmptyAndOutOfRange() + public void Load_mapsSlotToObjId_skipsEmptyAndOutOfRange() { var store = new ShortcutStore(); - var entries = new List + store.Load(new (int, uint)[] { - new(Index: 0, ObjectGuid: 0x5001u, SpellId: 0, Layer: 0), - new(Index: 3, ObjectGuid: 0x5002u, SpellId: 0, Layer: 0), - new(Index: 5, ObjectGuid: 0u, SpellId: 7, Layer: 1), // spell-only → skipped (item store) - new(Index: 99, ObjectGuid: 0x5003u, SpellId: 0, Layer: 0), // out of range → skipped - }; - store.Load(entries); + (0, 0x5001u), + (3, 0x5002u), + (5, 0u), // empty/spell → skipped + (99, 0x5003u), // out of range → skipped + }); Assert.Equal(0x5001u, store.Get(0)); Assert.Equal(0x5002u, store.Get(3)); Assert.Equal(0u, store.Get(5));