feat(core): D.5.3/B.2 — ShortcutStore + AddShortcut/RemoveShortcut wire + senders

ShortcutStore lands in AcDream.Core.Net.Items (not AcDream.Core.Items) because
it depends on PlayerDescriptionParser.ShortcutEntry; placing it in AcDream.Core
would create a circular dependency (Core.Net already references Core). The test
lives in AcDream.Core.Tests which gets Core.Net transitively via App.

BuildAddShortcut signature corrected from the old (seq, slotIndex, objectType,
targetId) 4×u32 layout to the retail ShortCutData wire format confirmed in the
action-bar deep-dive: Index(u32), ObjectId(u32), SpellId(u16), Layer(u16).
The old BuildAddShortcut_ThreeFields test is replaced by two new tests that
verify both item and spell shortcut packing.

WorldSession gains SendAddShortcut / SendRemoveShortcut following the
SendChangeCombatMode sender pattern.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-20 15:09:58 +02:00
parent 7193bed309
commit 745a92bbae
5 changed files with 115 additions and 15 deletions

View file

@ -0,0 +1,32 @@
using System.Collections.Generic;
using AcDream.Core.Net.Messages;
namespace AcDream.Core.Net.Items;
/// <summary>
/// Mutable client-side model of the 18 toolbar shortcut slots — port of retail
/// <c>ShortCutManager::shortCuts_[18]</c> (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.
/// </summary>
public sealed class ShortcutStore
{
public const int SlotCount = 18;
private readonly uint[] _objIds = new uint[SlotCount];
/// <summary>Replace all slots from the login PlayerDescription shortcut list (item entries only).</summary>
public void Load(IReadOnlyList<PlayerDescriptionParser.ShortcutEntry> entries)
{
System.Array.Clear(_objIds);
foreach (var e in entries)
if (e.Index < SlotCount && e.ObjectGuid != 0) _objIds[(int)e.Index] = e.ObjectGuid;
}
/// <summary>Bound object guid at <paramref name="slot"/>, or 0 (empty / out of range).</summary>
public uint Get(int slot) => (uint)slot < SlotCount ? _objIds[slot] : 0u;
public bool IsEmpty(int slot) => Get(slot) == 0u;
public void Set(int slot, uint objId) { if ((uint)slot < SlotCount) _objIds[slot] = objId; }
public void Remove(int slot) { if ((uint)slot < SlotCount) _objIds[slot] = 0u; }
}