diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md
index e4559047..c316f062 100644
--- a/docs/architecture/retail-divergence-register.md
+++ b/docs/architecture/retail-divergence-register.md
@@ -206,7 +206,6 @@ AP-94..AP-110 for the confirmed retail-UI completion gaps.
| ~~AP-99~~ | **RETIRED 2026-07-11 (Wave 3.2)** — Core `ItemInteractionPolicy` ports the complete ordered `DetermineUseResult`/`UseObject`/`AttemptPlaceIn3D` matrix; PWD flags and `CombatUse` survive CreateObject; component-pack membership comes from portal.dat; App owns throttle, UseDone-balanced busy state, target mode, wire/optimistic dispatch, and typed confirmation/auxiliary seams. The two corrupt decompiler operands were pinned from matching x86 as `BF_REQUIRES_PACKSLOT` and `BF_VENDOR`. | `src/AcDream.Core/Items/ItemInteractionPolicy.cs`; `src/AcDream.App/UI/ItemInteractionController.cs` | — | — | `ItemHolder::DetermineUseResult @ 0x00588460`; `UseObject @ 0x00588A80`; `AttemptPlaceIn3D @ 0x00588600` |
| AP-101 | Most toolbar panel/Use/Examine controls are not fully dispatched; selected-object mana and stack entry/slider are absent | `src/AcDream.App/UI/Layout/ToolbarController.cs`; `GameWindow.OnInputAction` | Item click/use, health/name, quick-slot use/select/create actions, some panel toggles, and all four combat indicator buttons work; combat clicks and the bound key share `ToggleLiveCombatMode` | Remaining bound retail buttons do nothing and item/stack selections show incomplete state | `gmToolbarUI @ 0x004BD0C0..0x004BF380`; LayoutDesc `0x21000016` |
| AP-102 | Inventory-to-occupied toolbar drop treats the inventory source index as though it were a vacated toolbar slot for the displaced shortcut | `src/AcDream.App/UI/Layout/ToolbarController.cs` drop planner | Normal empty-slot and toolbar-reorder paths work; Wave 4 makes the transaction source-aware | The displaced shortcut is lost or moved to an unrelated slot | `gmToolbarUI::HandleDropRelease @ 0x004BE7C0` |
-| AP-103 | Shortcut session storage keeps only object guid and discards raw non-object fields; parser/builder names the final uint as `u16 spellId/u16 layer` instead of preserving retail `spellID_` as raw data | `src/AcDream.Core/Items/ShortcutStore.cs`; `src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs`; `InventoryActions.cs` | `gmToolbarUI` itself displays only nonzero object ids, so current item shortcuts work | Round-trip mutation corrupts unknown/raw fields and encourages invented spell support in the item toolbar | `ShortCutData` in `acclient.h:36484`; `gmToolbarUI::UpdateFromPlayerDesc @ 0x004BF810` |
| AP-104 | Vitals detail element `0x100004A9` and root `HideDetail`/`ShowDetail` transitions are not wired | `src/AcDream.App/UI/Layout/VitalsController.cs` | Compact vitals values/bars are correct | Detail click does nothing and expanded retail state is unreachable | `gmVitalsUI::ListenToElementMessage @ 0x004BFC00`; `PostInit @ 0x004BFCE0` |
| AP-105 | **PARTIAL 2026-07-10 (Wave 2.2)** — inherited scrollbar media/roles now come from DAT (decrement/top `0x06004C69`, increment/bottom `0x06004C6C`). Retained chat still lacks complete tab/filter/unread, social availability, squelch, and focus-opacity behavior; a second ChatVM still loses FPS/position providers. | `src/AcDream.App/UI/Layout/DatWidgetFactory.cs`; `ChatWindowController.cs`; chat mount in `GameWindow.cs` | Shared log/send path, wrapping, scrollbar roles, and outer maximize geometry work; Wave 5 consolidates remaining chat state | Commands degrade, tabs are no-ops, moderation/channel state is wrong, and focus visuals diverge | `gmMainChatUI @ 0x004CCCC0..0x004CE2A0`; `UIElement_Scrollbar::OnSetAttribute @ 0x004714D0`; `ChatInterface` methods |
| AP-106 | The retained UI has no external/ground-container window lifecycle, while the original owned-side-bag `NoLongerViewingContents` premise was incorrect (#196) | `src/AcDream.App/UI`; window runtime; absent external-container controller | Owned inventory navigation remains usable and must not gain an unproven `0x0195` send; Wave 2/6 adds lifecycle ownership and the separate external surface | External container replacement/close cannot notify the server exactly once; adding the packet to owned bag close would itself diverge from the named retail call graph | `ClientUISystem.groundObject`; `CM_Inventory::Event_NoLongerViewingContents @ 0x006ABC50`; `docs/research/2026-07-10-retail-panel-behavior-pseudocode.md` |
diff --git a/docs/plans/2026-04-11-roadmap.md b/docs/plans/2026-04-11-roadmap.md
index 86435775..f8726b0d 100644
--- a/docs/plans/2026-04-11-roadmap.md
+++ b/docs/plans/2026-04-11-roadmap.md
@@ -486,11 +486,12 @@ behavior. Estimated 17–26 days focused work, 3–5 weeks calendar.
- **✓ SHIPPED — Importer dat-fidelity** (Fix A/B/C, 2026-06-26, same branch). **Boundary established: look = importer (font/justification/color from dat); state/behavior = runtime controller logic.** Fix A: justification property `0x14`/`0x15` → `UiText` Centered/RightAligned/VerticalJustify. Fix B: FontColor property `0x1B` → `UiText.DefaultColor`; character colors proved RUNTIME (no importer fix). Fix C: FontDid → per-element dat font via a resolver in `DatWidgetFactory` — **STUDIO path only**; GameWindow passes `null` (Issue #157). Fix 4 (UIState-group activation): INVESTIGATED — no importer fix exists; the dat has no Visible encoding; runtime `gm*UI` is correct.
- **✓ SHIPPED — Wave 3.3 unified primary-click router.** `ItemInteractionController.OfferPrimaryClick` returns `NotActive` / `ConsumedSuccess` / `ConsumedRejected`; inventory, bags, main pack, paperdoll, toolbar, radar, and world clicks all offer target mode before local fallback. Rejected targets cannot drift selection or open/use the clicked object. Issue #197 and AP-107 retired; retained matrix and live ACE-provisioned healing gate user-confirmed. The 3-D doll routes through authored hit mask `0x100001D6` (resolved `UiButton` above viewport `0x100001D5`), and local health prefers authoritative private-vital state.
- **✓ SHIPPED — Wave 4.1 retail quick-slot input.** Bare `1..9` use toolbar slots 0..8, Ctrl+`1..9` selects them, Alt+`5..9` uses slots 13..17, and `0` creates a shortcut to the selected owned/eligible object in retail's first empty slot. `ToolbarInputController` is the focused semantic adapter; `ToolbarController` owns `UseShortcut` target-before-use/select ordering and auto-slot eligibility. Keybinding schema v2 migrates only the exact old default Ctrl+number chords, so custom bindings retain their chosen action. Full automated conformance and live user gate passed 2026-07-11.
+- **✓ SHIPPED — Wave 4.2 exact shortcut records.** One Core-owned `ShortcutEntry` preserves retail's signed `index_`, unsigned `objectID_`, and raw 32-bit `spellID_` through PlayerDescription parse, the nullable 18-slot session store, drag snapshots/reindexing, WorldSession, and exact 12-byte AddShortcut packing. The visible `gmToolbarUI` remains object-only; `favorite_spells_[8]` remains a separate future spell-bar source. AP-103 retired. Full automated conformance and live mutate→clean-relog persistence gate passed 2026-07-11.
- **✓ SHIPPED — Wave 4.3a shortcut-alias boundary.** Drag feedback now preserves retail's neutral/accept/reject distinction. Inventory and paperdoll apply the `(DropItemFlags & 0xE) == 0` physical-item gate, so a toolbar alias can reorder within the toolbar but can never move, unwield, wield, or drop the object it references. Live equipped-helmet gate passed 2026-07-11: inventory drop removed only the shortcut.
- **✓ SHIPPED — Character window** (`LayoutDesc 0x2100002E`, `CharacterStatController`, 2026-06-26, same branch). **Visually user-confirmed 2026-06-26 — Attributes tab reads as retail.** Three tabs, header (name/heritage/PK), large-gold level number (dat font, `largeDatFont` 18px), "Total Experience (XP):" + "XP for next level:" captions, 9-row attribute list (icons + right-aligned values + Health/Stamina/Mana vitals), click-to-select (top/bottom selection bars + footer State-B "{Attr}: {value}" / "Experience To Raise: Infinity!" + affordability-gated raise triangles), centered footer. User noted "still needs some polish for later" — deferred to Issue #158.
- **✓ SHIPPED — D.5.4 — Client object/item data model (foundation).** Shipped 2026-06-18 (`b506f53`..`a33e897`, 11 commits). Renamed `ItemRepository`→`ClientObjectTable` / `ItemInstance`→`ClientObject`; broadened the table to hold EVERY server object (retail `weenie_object_table` shape). `CreateObject` is now the canonical merge-upsert (`ClientObjectTable.Ingest`, retail `SetWeenieDesc` semantics) via a new Core.Net `ObjectTableWiring` (off GameWindow); `DeleteObject` evicts; `PlayerDescription` is a membership manifest (`RecordMembership`); live container-membership index (`GetContents`, retail `object_inventory_table`). `_liveEntityInfoByGuid` retired (selection/describe resolve from the one table). Root fix: the old enrich-existing-only `EnrichItem` dropped `CreateObject`s for items with no `PlayerDescription` stub — live-Coldeve 4/6 hotbar slots blank; items are now created, not dropped. **Crux resolved:** retail is TWO tables (`object_table` + `weenie_object_table`), NOT one — acdream's `WorldEntity` (3D system) + `ClientObjectTable` (data/UI) split was already architecturally faithful; the fix was the ingestion path, not a table unification. 2671 tests green.
- **Roadmap correction (2026-07-10):** the completion order is now the architecture-first campaign in `docs/superpowers/plans/2026-07-10-retail-ui-fidelity-completion.md`. Retail `gmToolbarUI` is object-only: preserve `ShortCutData.index_`, `objectID_`, and `spellID_`, but do not invent spell glyphs on this bar. `PlayerModule::favorite_spells_[8]` feeds separate spell bars.
-- **☐ D.5.3 — Toolbar selected-object display (issue #141) + exact shortcut preservation.** Wire unified selection to the hidden mana meter (`0x100001A2`), stack entry (`0x100001A3`), stack slider (`0x100001A4`), and complete health/name behavior. Preserve all raw `ShortCutData` fields while keeping `gmToolbarUI` object-only, matching `UpdateFromPlayerDesc @ 0x004BF810`. The eight `favorite_spells_` lists belong to the separate spellbook/spell-bar phase. (Click-to-use + peace/war + health/name already landed.)
+- **☐ D.5.3 — Toolbar selected-object display (issue #141).** Wire unified selection to the hidden mana meter (`0x100001A2`), stack entry (`0x100001A3`), stack slider (`0x100001A4`), and complete health/name behavior. Exact raw `ShortCutData` preservation is already shipped in Wave 4.2; the eight `favorite_spells_` lists belong to the separate spellbook/spell-bar phase. (Click-to-use + peace/war + health/name already landed.)
- **☐ D.5.5+ — Core panels.** Inventory (`gmInventoryUI`/`gmBackpackUI`), equipment/paperdoll (`gmPaperDollUI`/`gm3DItemsUI` + the `UiViewport` 3D doll), vendor, trade, spellbook. Research drop done (`docs/research/2026-06-16-*`). Depends on **D.5.4** (data model) + the item-slot/list/icon spine (D.5.1/D.5.2) + the **window manager** (Plan 2: open/close/z-order/persist + faithful grip/dragbar drag-resize) + the drag-drop spine wired (`UiRoot` has the chain; the per-cell accept/drop hooks are still stubs in `UiField`). Also deferred from D.5.1: drag/reorder + the `AddShortcut`/`RemoveShortcut` mutate wire.
- **D.6 — HUD.** **Radar/compass IMPLEMENTED 2026-07-10; live-world visual gate pending.** It is the retained retail `gmRadarUI` (`LayoutDesc 0x21000074`): a 120×140 circular radar (`0x06004CC1`) with N/E/S/W token sprites orbiting the face, a fixed bright-green player marker, and the coordinate footer (`0x06004CC0`). Projection is `radarPixelRadius / radarRange` with 75 m outdoor / 25 m indoor ranges — there is no 1.18 factor or scrolling compass strip. It lives in `AcDream.App/UI` beside the other imported retail panels. Remaining D.6: target name plate, damage floaters, and other world-space HUD elements through the raw `TextRenderer` path. See slice 06 §A.2, `docs/research/2026-07-10-retail-radar-pseudocode.md`, and the named `gmRadarUI` decomp.
- **✓ SHIPPED — D.7 — Cursor manager (user-confirmed 2026-07-11).** All reachable `ClientUISystem::UpdateCursorState @ 0x00564630` branches resolve through production DAT EnumIDMap table 6, with event-ordered global-default versus captured/hovered `MediaDescCursor` layering from `UIElementManager::CheckCursor @ 0x0045ABF0`. OS fallback remains only AP-72 and emits a visible diagnostic. The live gate covered world/object, combat, targeted-use, text, move/resize, and drag contexts; combat-indicator mouse clicks and the quick key share the same toggle command.
diff --git a/docs/research/2026-06-16-action-bar-toolbar-deep-dive.md b/docs/research/2026-06-16-action-bar-toolbar-deep-dive.md
index de3e30de..49f74694 100644
--- a/docs/research/2026-06-16-action-bar-toolbar-deep-dive.md
+++ b/docs/research/2026-06-16-action-bar-toolbar-deep-dive.md
@@ -128,19 +128,26 @@ Client-side restore: `gmToolbarUI::UpdateFromPlayerDesc` (decomp 198838) → `Fl
Opcode values triple-confirmed: decomp `Event_AddShortCut` packs `*(uint32_t*)var_c = 0x19c` (decomp 679733) and `Event_RemoveShortCut` packs `0x19d` (decomp 680332); ACE `GameActionType.cs:77-78` (`AddShortCut=0x019C, RemoveShortCut=0x019D`); holtburger `opcodes.rs:371-374` (commented, same values).
-**Wire field order — `ShortCutData` payload (16 bytes), CONFIRMED across 3 refs:**
+**Correction (2026-07-11) — packed `ShortCutData` is 12 bytes, while the C++
+object is 16 bytes only because it includes a vtable pointer.** Retail's header,
+constructor, `ShortCutManager::AddShortCut`, and vtable-routed packer establish
+three native 32-bit fields:
```
-Index : u32 (slot 0..17)
+Index : i32 (slot 0..17 when valid)
ObjectId : u32 (item guid; 0 for spell)
-SpellId : u16 (LayeredSpell.id; 0 for item)
-Layer : u16 (LayeredSpell.layer; 0 for item)
+SpellId : u32 (raw retail spell word; 0 for toolbar-created items)
```
-- Chorizite `ShortCutData.generated.cs:41-46` (`Index`, `ObjectId`, then `LayeredSpellId.Read` = u16 id + u16 layer).
-- ACE `Shortcut.cs:33-42` `ReadShortcut` (`Index`, `ObjectId`, `ReadLayeredSpell`).
-- holtburger `shortcuts.rs:13-34` (`index u32`, `object_id Guid`, `spell_id u16`, `layer u16`).
+- Retail `ShortCutData` in `acclient.h:36484`, constructor `0x005D55E0`,
+ manager copy `0x005D5790`, and 12-byte packer `0x005CABB0` are authoritative.
+- Chorizite, ACE, and holtburger split the final four bytes into a layered-spell
+ `u16/u16` view. That view is wire-compatible but is not retail's field model;
+ acdream preserves the complete raw `uint` losslessly.
RemoveShortCut payload = just `Index:u32` (Chorizite `Character_RemoveShortCut.generated.cs:33`; ACE `GameActionRemoveShortcut.cs:9`; decomp packs `*(uint32_t*)eax_3 = arg1` at 680335).
-**⚠ acdream builder field-naming bug to fix at port time (not a wire bug).** `InventoryActions.BuildAddShortcut(seq, slotIndex, objectType, targetId)` (`InventoryActions.cs:99-110`) writes 24 bytes = 8-byte envelope (`0xF7B1` + seq) + `slotIndex`(u32) + `objectType`(u32) + `targetId`(u32). The **byte layout is correct for item shortcuts** (slot, then guid, then a final dword that for items is `0` = SpellId|Layer), but the parameter names are wrong/misleading: the 2nd field is `Index`, the 3rd is `ObjectId`, and the 4th dword is `SpellId(u16)|Layer(u16)` — there is no separate "objectType". A faithful builder should take `(seq, uint index, uint objectGuid, ushort spellId, ushort layer)` and pack the spell as two u16s. For the toolbar's item-only use, callers must pass `objectGuid` as the 3rd arg and `0` as the 4th. LIKELY a latent bug if anyone wired a "objectType" semantic; flag in the divergence register when the toolbar lands. (CONFIRMED file contents; the "bug" judgment is mine.)
+**Resolved 2026-07-11:** `AcDream.Core.Items.ShortcutEntry` now owns the exact
+signed-index/object-id/raw-spell-word value. Parser, store, toolbar mutation, and
+`InventoryActions.BuildAddShortcut` all carry that value without a layered-spell
+reinterpretation. The production toolbar still renders only nonzero object ids.
**ACE's reorder note (important UX contract):** *"When a shortcut is added on top of an existing item, the client automatically sends the RemoveShortcut command for that existing item first, then will add the new item, and re-add the existing item to the appropriate place."* (`Player_Character.cs:254`). This is exactly the `HandleDropRelease` sequence in §5. CONFIRMED.
diff --git a/docs/superpowers/plans/2026-07-10-retail-ui-fidelity-completion.md b/docs/superpowers/plans/2026-07-10-retail-ui-fidelity-completion.md
index eb16dd09..8dc44e69 100644
--- a/docs/superpowers/plans/2026-07-10-retail-ui-fidelity-completion.md
+++ b/docs/superpowers/plans/2026-07-10-retail-ui-fidelity-completion.md
@@ -450,16 +450,25 @@ skipped.
### 4.2 Preserve the exact shortcut model; keep the toolbar object-only
-- [ ] Model the 18 raw `ShortCutData` entries exactly as signed `index_`, unsigned
+- [x] Model the 18 raw `ShortCutData` entries exactly as signed `index_`, unsigned
`objectID_`, and unsigned `spellID_`; verify packing against the retail packer and
protocol captures before changing the existing builder.
-- [ ] Preserve all three fields through parse, session storage, mutation, and wire
+- [x] Preserve all three fields through parse, session storage, mutation, and wire
serialization even though `gmToolbarUI::UpdateFromPlayerDesc @ 0x004BF810`
restores only entries with a nonzero `objectID_`.
-- [ ] Keep the main toolbar item/object-only. Feed `PlayerModule::favorite_spells_[8]`
+- [x] Keep the main toolbar item/object-only. Feed `PlayerModule::favorite_spells_[8]`
and the already parsed `HotbarSpells` into the separate spell-bar subsystem in
Wave 10.
+**Wave 4.2 implementation (2026-07-11):** Core-owned `ShortcutEntry` now models
+the exact signed index, object id, and raw 32-bit spell word. PlayerDescription
+parsing, the 18-slot nullable store, retained-cell drag snapshots, displaced-item
+reindexing, runtime delegates, WorldSession, and AddShortcut serialization carry
+the same lossless value. The visible toolbar continues to bind only entries with
+nonzero object ids. AP-103 is retired; automated conformance is green and the
+live relog/persistence gate passed: ACE restored the mutated shortcut in the
+same slot after a clean reconnect.
+
### 4.3 Port every drag/drop branch
- [ ] Add a pure `ShortcutDropPlanner` keyed by source kind: toolbar reorder,
diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs
index 7c4d28b2..15039dd5 100644
--- a/src/AcDream.App/Rendering/GameWindow.cs
+++ b/src/AcDream.App/Rendering/GameWindow.cs
@@ -737,8 +737,8 @@ public sealed class GameWindow : IDisposable
public readonly AcDream.Core.Spells.Spellbook SpellBook = null!;
public readonly AcDream.Core.Items.ClientObjectTable Objects = new();
/// Persisted hotbar shortcuts from the last PlayerDescription (D.5.1 toolbar source).
- public IReadOnlyList Shortcuts { get; private set; }
- = System.Array.Empty();
+ public IReadOnlyList Shortcuts { get; private set; }
+ = System.Array.Empty();
// Issue #5 — caches CreatureProfile.{Stamina, Mana, *Max} from
// PlayerDescription so the Vitals HUD can render those bars.
// Issue #6 — wired to SpellBook so GetMaxApprox folds enchantment
@@ -2114,7 +2114,7 @@ public sealed class GameWindow : IDisposable
Combat,
ToggleLiveCombatMode,
_itemInteractionController,
- (index, guid) => _liveSession?.SendAddShortcut(index, guid),
+ entry => _liveSession?.SendAddShortcut(entry),
index => _liveSession?.SendRemoveShortcut(index),
_selection,
handler => Combat.HealthChanged += handler,
diff --git a/src/AcDream.App/Studio/FixtureProvider.cs b/src/AcDream.App/Studio/FixtureProvider.cs
index 3d8e130f..61c3d79b 100644
--- a/src/AcDream.App/Studio/FixtureProvider.cs
+++ b/src/AcDream.App/Studio/FixtureProvider.cs
@@ -2,7 +2,6 @@ using AcDream.App.Rendering;
using AcDream.App.UI;
using AcDream.App.UI.Layout;
using AcDream.Core.Items;
-using AcDream.Core.Net.Messages;
using DatReaderWriter;
namespace AcDream.App.Studio;
@@ -92,7 +91,7 @@ public static class FixtureProvider
ToolbarController.Bind(
layout,
objects,
- shortcuts: () => System.Array.Empty(),
+ shortcuts: () => System.Array.Empty(),
iconIds: MakeIconIds(stack),
useItem: _ => { },
combatState: null,
diff --git a/src/AcDream.App/UI/ItemDragPayload.cs b/src/AcDream.App/UI/ItemDragPayload.cs
index b06728d4..f4f93234 100644
--- a/src/AcDream.App/UI/ItemDragPayload.cs
+++ b/src/AcDream.App/UI/ItemDragPayload.cs
@@ -1,3 +1,5 @@
+using AcDream.Core.Items;
+
namespace AcDream.App.UI;
///
@@ -21,4 +23,5 @@ public sealed record ItemDragPayload(
uint ObjId, // dragged weenie guid (retail itemID, +0x5FC)
ItemDragSource SourceKind, // what kind of slot it left
int SourceSlot, // the source cell's SlotIndex (retail m_lastShortcutNumDragged)
- UiItemSlot SourceCell); // back-ref: reorder-restore / clear source state / ghost
+ UiItemSlot SourceCell, // back-ref: reorder-restore / clear source state / ghost
+ ShortcutEntry? Shortcut = null); // lossless raw entry for shortcut-alias mutation
diff --git a/src/AcDream.App/UI/Layout/ToolbarController.cs b/src/AcDream.App/UI/Layout/ToolbarController.cs
index f22e697d..c9d0fe62 100644
--- a/src/AcDream.App/UI/Layout/ToolbarController.cs
+++ b/src/AcDream.App/UI/Layout/ToolbarController.cs
@@ -63,12 +63,12 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
private readonly UiButton? _inventoryButton;
private readonly ClientObjectTable _repo;
private readonly CombatState? _combatState;
- private readonly Func> _shortcuts;
+ 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.Items.ShortcutStore _store = new();
+ private readonly ShortcutStore _store = new();
private bool _storeLoaded;
- private readonly Action? _sendAddShortcut; // (index, objectGuid)
+ private readonly Action? _sendAddShortcut;
private readonly Action? _sendRemoveShortcut; // (index)
private readonly ItemInteractionController? _itemInteraction;
private readonly Action? _selectItem;
@@ -89,7 +89,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
private ToolbarController(
ImportedLayout layout,
ClientObjectTable repo,
- Func> shortcuts,
+ Func> shortcuts,
Func iconIds,
Action useItem,
CombatState? combatState,
@@ -97,7 +97,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
uint[]? warDigits,
uint[]? emptyDigits,
ItemInteractionController? itemInteraction = null,
- Action? sendAddShortcut = null,
+ Action? sendAddShortcut = null,
Action? sendRemoveShortcut = null,
Action? toggleCombat = null,
Action? selectItem = null)
@@ -189,7 +189,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
}
// Store not yet loaded — fall back to the shortcuts provider (pre-PD window).
foreach (var sc in _shortcuts())
- if (sc.ObjectGuid == guid) return true;
+ if (sc.ObjectId == guid) return true;
return false;
}
@@ -226,7 +226,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
public static ToolbarController Bind(
ImportedLayout layout,
ClientObjectTable repo,
- Func> shortcuts,
+ Func> shortcuts,
Func iconIds,
Action useItem,
CombatState? combatState = null,
@@ -234,7 +234,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
uint[]? warDigits = null,
uint[]? emptyDigits = null,
ItemInteractionController? itemInteraction = null,
- Action? sendAddShortcut = null,
+ Action? sendAddShortcut = null,
Action? sendRemoveShortcut = null,
Action? toggleCombat = null,
Action? selectItem = null)
@@ -288,7 +288,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
// after Bind); thereafter the store is authoritative and drag ops mutate it directly.
if (!_storeLoaded && _shortcuts().Count > 0)
{
- _store.Load(System.Linq.Enumerable.Select(_shortcuts(), e => ((int)e.Index, e.ObjectGuid)));
+ _store.Load(_shortcuts());
_storeLoaded = true;
}
@@ -296,14 +296,15 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
for (int slot = 0; slot < _slots.Length; slot++)
{
- uint guid = _store.Get(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);
- list.Cell.SetItem(guid, tex);
+ list.Cell.SetItem(guid, tex, entry);
}
// Re-stamp slot number labels after any item change.
@@ -466,8 +467,9 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
if (empty < 0)
return false;
- _store.Set(empty, itemId);
- _sendAddShortcut?.Invoke((uint)empty, itemId);
+ var entry = new ShortcutEntry(empty, itemId, 0u);
+ _store.Set(entry);
+ _sendAddShortcut?.Invoke(entry);
Populate();
return true;
}
@@ -476,7 +478,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
{
if (_storeLoaded)
return;
- _store.Load(System.Linq.Enumerable.Select(_shortcuts(), e => ((int)e.Index, e.ObjectGuid)));
+ _store.Load(_shortcuts());
_storeLoaded = true;
}
@@ -510,14 +512,18 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
// 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)
+ ShortcutEntry? evictedEntry = _store.GetEntry(target); // RemoveShortCutInSlotNum(target)
+ uint evicted = evictedEntry?.ObjectId ?? 0u;
if (evicted != 0) { _store.Remove(target); _sendRemoveShortcut?.Invoke((uint)target); }
- _store.Set(target, payload.ObjId); // AddShortcut(dragged, target)
- _sendAddShortcut?.Invoke((uint)target, payload.ObjId);
+ ShortcutEntry dragged = (payload.Shortcut ?? new ShortcutEntry(payload.SourceSlot, payload.ObjId, 0u))
+ .WithIndex(target);
+ _store.Set(dragged); // AddShortcut(dragged, target)
+ _sendAddShortcut?.Invoke(dragged);
if (evicted != 0 && evicted != payload.ObjId && _store.IsEmpty(payload.SourceSlot))
{ // displaced → vacated source slot
- _store.Set(payload.SourceSlot, evicted);
- _sendAddShortcut?.Invoke((uint)payload.SourceSlot, evicted);
+ ShortcutEntry displaced = evictedEntry!.Value.WithIndex(payload.SourceSlot);
+ _store.Set(displaced);
+ _sendAddShortcut?.Invoke(displaced);
}
Populate();
}
diff --git a/src/AcDream.App/UI/RetailUiRuntime.cs b/src/AcDream.App/UI/RetailUiRuntime.cs
index b8ebbf52..31881c9b 100644
--- a/src/AcDream.App/UI/RetailUiRuntime.cs
+++ b/src/AcDream.App/UI/RetailUiRuntime.cs
@@ -37,13 +37,13 @@ public sealed record RadarRuntimeBindings(
public sealed record ToolbarRuntimeBindings(
ClientObjectTable Objects,
- Func> Shortcuts,
+ Func> Shortcuts,
Func ResolveIcon,
Action UseItem,
CombatState Combat,
Action ToggleCombat,
ItemInteractionController ItemInteraction,
- Action? SendAddShortcut,
+ Action? SendAddShortcut,
Action? SendRemoveShortcut,
SelectionState Selection,
Action> SubscribeHealthChanged,
diff --git a/src/AcDream.App/UI/UiItemSlot.cs b/src/AcDream.App/UI/UiItemSlot.cs
index e655b0c6..c54945d9 100644
--- a/src/AcDream.App/UI/UiItemSlot.cs
+++ b/src/AcDream.App/UI/UiItemSlot.cs
@@ -1,5 +1,6 @@
using System;
using System.Numerics;
+using AcDream.Core.Items;
namespace AcDream.App.UI;
@@ -21,6 +22,12 @@ public sealed class UiItemSlot : UiElement
/// Pre-composited icon GL texture for the bound item (0 = none).
public uint IconTexture { get; private set; }
+ ///
+ /// Lossless shortcut record when this cell belongs to the toolbar. Physical item
+ /// lists leave it null. Snapshotted into the drag payload before remove-on-lift.
+ ///
+ public ShortcutEntry? Shortcut { get; private set; }
+
/// This cell's own index within its panel (0..17 toolbar; container slot
/// for inventory). Distinct from (the 1–9 label, -1 on the
/// bottom row). Set by the controller; used as the drag payload's SourceSlot and to
@@ -79,17 +86,18 @@ public sealed class UiItemSlot : UiElement
/// RenderSurface id -> (GL texture, w, h). Set by the factory/controller.
public Func? SpriteResolve { get; set; }
- public void SetItem(uint itemId, uint iconTexture)
+ public void SetItem(uint itemId, uint iconTexture, ShortcutEntry? shortcut = null)
{
ItemId = itemId;
IconTexture = iconTexture;
+ Shortcut = shortcut;
}
- public void Clear() { ItemId = 0; IconTexture = 0; }
+ public void Clear() { ItemId = 0; IconTexture = 0; Shortcut = null; }
///
public override object? GetDragPayload()
- => ItemId != 0 ? new ItemDragPayload(ItemId, SourceKind, SlotIndex, this) : null;
+ => ItemId != 0 ? new ItemDragPayload(ItemId, SourceKind, SlotIndex, this, Shortcut) : null;
///
public override (uint tex, int w, int h)? GetDragGhost()
diff --git a/src/AcDream.Core.Net/GameEventWiring.cs b/src/AcDream.Core.Net/GameEventWiring.cs
index 962295c4..c1910d33 100644
--- a/src/AcDream.Core.Net/GameEventWiring.cs
+++ b/src/AcDream.Core.Net/GameEventWiring.cs
@@ -65,7 +65,7 @@ public static class GameEventWiring
// D.5.1 Task 4: persists Shortcuts from each PlayerDescription so the
// toolbar can populate itself at login without keeping a parser reference.
// Optional so all existing callers and tests compile unchanged.
- Action>? onShortcuts = null,
+ Action>? onShortcuts = null,
// B-Wire: the local player's server guid. When provided, the PD handler upserts
// the player's own PropertyBundle (EncumbranceVal etc.) into the player ClientObject.
Func? playerGuid = null,
diff --git a/src/AcDream.Core.Net/Messages/InventoryActions.cs b/src/AcDream.Core.Net/Messages/InventoryActions.cs
index cebe9a82..1989c491 100644
--- a/src/AcDream.Core.Net/Messages/InventoryActions.cs
+++ b/src/AcDream.Core.Net/Messages/InventoryActions.cs
@@ -1,4 +1,5 @@
using System.Buffers.Binary;
+using AcDream.Core.Items;
namespace AcDream.Core.Net.Messages;
@@ -98,20 +99,20 @@ public static class InventoryActions
return body;
}
- /// Pin an item/spell to a quickbar slot. ShortCutData = Index(u32), ObjectId(u32),
- /// SpellId(u16), Layer(u16) — CONFIRMED across ACE/Chorizite/holtburger (action-bar deep-dive
- /// §131-145). For an ITEM: objectGuid = item guid, spellId = layer = 0.
- public static byte[] BuildAddShortcut(
- uint seq, uint index, uint objectGuid, ushort spellId, ushort layer)
+ ///
+ /// Pack retail ShortCutData for AddShortcut: signed index, object id,
+ /// and raw 32-bit spell word. Retail uses the same three-u32 packer as
+ /// InventoryPlacement::Pack @ 0x005CABB0 through the shortcut vtable.
+ ///
+ public static byte[] BuildAddShortcut(uint seq, ShortcutEntry entry)
{
byte[] body = new byte[24];
BinaryPrimitives.WriteUInt32LittleEndian(body, GameActionEnvelope);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(4), seq);
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(8), AddShortcutOpcode);
- BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), index);
- BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(16), objectGuid);
- BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(20), spellId);
- BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(22), layer);
+ BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(12), entry.Index);
+ BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(16), entry.ObjectId);
+ BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(20), entry.SpellId);
return body;
}
diff --git a/src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs b/src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs
index 73cb9f43..34433492 100644
--- a/src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs
+++ b/src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs
@@ -197,18 +197,6 @@ public static class PlayerDescriptionParser
SpellLists8 = 0x00000400,
}
- /// One shortcut bar entry. 16 bytes wire size.
- /// holtburger shortcuts.rs:13-34. Named ShortcutEntry
- /// (not Shortcut) to avoid a homograph with the
- /// flag bit, which is
- /// referenced from the same scope as instances of this type in the
- /// trailer walker.
- public readonly record struct ShortcutEntry(
- uint Index,
- uint ObjectGuid,
- ushort SpellId,
- ushort Layer);
-
/// One inventory entry — a guid plus a ContainerType
/// discriminator (0=NonContainer, 1=Container, 2=Foci). Holtburger
/// events.rs:143-168 validates ContainerType <= 2
@@ -348,11 +336,10 @@ public static class PlayerDescriptionParser
if (count > 10_000) throw new FormatException("unreasonable shortcut count");
for (uint i = 0; i < count; i++)
{
- uint idx = ReadU32(payload, ref pos);
- uint guid = ReadU32(payload, ref pos);
- ushort spellId = ReadU16(payload, ref pos);
- ushort layer = ReadU16(payload, ref pos);
- shortcuts.Add(new ShortcutEntry(idx, guid, spellId, layer));
+ int index = unchecked((int)ReadU32(payload, ref pos));
+ uint objectId = ReadU32(payload, ref pos);
+ uint spellId = ReadU32(payload, ref pos);
+ shortcuts.Add(new ShortcutEntry(index, objectId, spellId));
}
}
diff --git a/src/AcDream.Core.Net/WorldSession.cs b/src/AcDream.Core.Net/WorldSession.cs
index 4446c09e..1003f8c5 100644
--- a/src/AcDream.Core.Net/WorldSession.cs
+++ b/src/AcDream.Core.Net/WorldSession.cs
@@ -3,6 +3,7 @@ using System.Diagnostics;
using System.Net;
using System.Threading.Channels;
using AcDream.Core.Combat;
+using AcDream.Core.Items;
using AcDream.Core.Net.Cryptography;
using AcDream.Core.Net.Messages;
using AcDream.Core.Net.Packets;
@@ -1273,12 +1274,11 @@ public sealed class WorldSession : IDisposable
SendGameAction(CharacterActions.BuildTrainSkill(seq, skillId, credits));
}
- /// Send AddShortcut (0x019C) — pin an item to toolbar slot .
- /// Retail: CM_Character::Event_AddShortCut. Mirrors the SendChangeCombatMode pattern.
- public void SendAddShortcut(uint index, uint objectGuid, ushort spellId = 0, ushort layer = 0)
+ /// Send lossless retail AddShortcut (0x019C).
+ public void SendAddShortcut(ShortcutEntry entry)
{
uint seq = NextGameActionSequence();
- SendGameAction(InventoryActions.BuildAddShortcut(seq, index, objectGuid, spellId, layer));
+ SendGameAction(InventoryActions.BuildAddShortcut(seq, entry));
}
/// Send RemoveShortcut (0x019D) — clear toolbar slot .
diff --git a/src/AcDream.Core/Items/ShortcutEntry.cs b/src/AcDream.Core/Items/ShortcutEntry.cs
new file mode 100644
index 00000000..f498f550
--- /dev/null
+++ b/src/AcDream.Core/Items/ShortcutEntry.cs
@@ -0,0 +1,14 @@
+namespace AcDream.Core.Items;
+
+///
+/// Lossless retail ShortCutData value: signed slot index, object id,
+/// and raw 32-bit spell word. The C++ object is 16 bytes including its vtable;
+/// the packed value is exactly 12 bytes. Retail anchors:
+/// ShortCutData::ShortCutData @ 0x005D55E0,
+/// ShortCutManager::AddShortCut @ 0x005D5790, and
+/// acclient.h:36484.
+///
+public readonly record struct ShortcutEntry(int Index, uint ObjectId, uint SpellId)
+{
+ public ShortcutEntry WithIndex(int index) => this with { Index = index };
+}
diff --git a/src/AcDream.Core/Items/ShortcutStore.cs b/src/AcDream.Core/Items/ShortcutStore.cs
index 0f581a2d..636609b4 100644
--- a/src/AcDream.Core/Items/ShortcutStore.cs
+++ b/src/AcDream.Core/Items/ShortcutStore.cs
@@ -4,31 +4,53 @@ using System.Collections.Generic;
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 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.
+/// Mutable client-side model of retail ShortCutManager::shortCuts_[18]
+/// (acclient.h:36492). Every present entry retains all three raw
+/// fields, including non-object records that the object-only
+/// gmToolbarUI does not render.
///
public sealed class ShortcutStore
{
public const int SlotCount = 18;
- private readonly uint[] _objIds = new uint[SlotCount];
+ private readonly ShortcutEntry?[] _entries = new ShortcutEntry?[SlotCount];
- /// 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)
+ ///
+ /// Replace all slots from packed entries. Invalid signed indices are rejected exactly
+ /// like ShortCutManager::AddShortCut @ 0x005D5790; valid entries are retained
+ /// even when their object id is zero.
+ ///
+ public void Load(IEnumerable entries)
{
- Array.Clear(_objIds);
- foreach (var (slot, objGuid) in entries)
- if ((uint)slot < SlotCount && objGuid != 0) _objIds[slot] = objGuid;
+ Array.Clear(_entries);
+ foreach (var entry in entries)
+ Set(entry);
}
- /// Bound object guid at , or 0 (empty / out of range).
- public uint Get(int slot) => (uint)slot < SlotCount ? _objIds[slot] : 0u;
+ /// Raw entry at , or null for absent/out-of-range.
+ public ShortcutEntry? GetEntry(int slot)
+ => (uint)slot < SlotCount ? _entries[slot] : null;
+
+ /// Visible object id at , or zero.
+ public uint Get(int slot) => GetEntry(slot)?.ObjectId ?? 0u;
+
+ ///
+ /// Visual availability used by gmToolbarUI. A retained non-object entry remains
+ /// invisible and therefore counts as an empty toolbar cell.
+ ///
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; }
+
+ public void Set(ShortcutEntry entry)
+ {
+ if ((uint)entry.Index < SlotCount)
+ _entries[entry.Index] = entry;
+ }
+
+ /// Create/replace a retail object shortcut; toolbar-created records use spell word zero.
+ public void SetItem(int slot, uint objectId) => Set(new ShortcutEntry(slot, objectId, 0u));
+
+ public void Remove(int slot)
+ {
+ if ((uint)slot < SlotCount)
+ _entries[slot] = null;
+ }
}
diff --git a/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs
index 2abd14c4..0679432e 100644
--- a/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs
+++ b/tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs
@@ -71,8 +71,8 @@ public class ToolbarControllerTests
var (layout, slots, _) = FakeToolbar();
var repo = new ClientObjectTable();
repo.AddOrUpdate(new ClientObject { ObjectId = 0x5001u, WeenieClassId = 1u, IconId = 0x06001234u });
- var shortcuts = new List
- { new(Index: 0, ObjectGuid: 0x5001u, SpellId: 0, Layer: 0) };
+ var shortcuts = new List
+ { new(Index: 0, ObjectId: 0x5001u, SpellId: 0) };
ToolbarController.Bind(layout, repo, () => shortcuts,
iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { });
@@ -87,8 +87,8 @@ public class ToolbarControllerTests
{
var (layout, slots, _) = FakeToolbar();
var repo = new ClientObjectTable(); // item NOT present yet
- var shortcuts = new List
- { new(Index: 2, ObjectGuid: 0x5002u, SpellId: 0, Layer: 0) };
+ var shortcuts = new List
+ { new(Index: 2, ObjectId: 0x5002u, SpellId: 0) };
ToolbarController.Bind(layout, repo, () => shortcuts,
iconIds: (_,_,_,_,_) => 0x88u, useItem: _ => { });
@@ -104,8 +104,8 @@ public class ToolbarControllerTests
{
var (layout, slots, _) = FakeToolbar();
var repo = new ClientObjectTable();
- var shortcuts = new List
- { new(Index: 2, ObjectGuid: 0x5002u, SpellId: 0, Layer: 0) };
+ var shortcuts = new List
+ { new(Index: 2, ObjectId: 0x5002u, SpellId: 0) };
var controller = ToolbarController.Bind(layout, repo, () => shortcuts,
iconIds: (_,_,_,_,_) => 0x88u, useItem: _ => { });
@@ -127,8 +127,8 @@ public class ToolbarControllerTests
var (layout, slots, _) = FakeToolbar();
var repo = new ClientObjectTable();
repo.AddOrUpdate(new ClientObject { ObjectId = 0x5001u, WeenieClassId = 1u, IconId = 0x06001234u });
- var shortcuts = new List
- { new(Index: 0, ObjectGuid: 0x5001u, SpellId: 0, Layer: 0) };
+ var shortcuts = new List
+ { new(Index: 0, ObjectId: 0x5001u, SpellId: 0) };
uint used = 0;
ToolbarController.Bind(layout, repo, () => shortcuts,
@@ -148,7 +148,7 @@ public class ToolbarControllerTests
int characterClicks = 0;
var ctrl = ToolbarController.Bind(layout, repo,
- () => Array.Empty(),
+ () => Array.Empty(),
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
ctrl.BindWindowToggles(
toggleInventory: () => inventoryClicks++,
@@ -191,7 +191,7 @@ public class ToolbarControllerTests
int inventoryClicks = 0;
var ctrl = ToolbarController.Bind(layout, repo,
- () => Array.Empty(),
+ () => Array.Empty(),
iconIds: (_,_,_,_,_) => 0u,
useItem: _ => { },
itemInteraction: interaction);
@@ -213,7 +213,7 @@ public class ToolbarControllerTests
var (layout, _, _) = FakeToolbar();
var repo = new ClientObjectTable();
var ctrl = ToolbarController.Bind(layout, repo,
- () => Array.Empty(),
+ () => Array.Empty(),
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
var inventoryButton = (UiButton)layout.FindElement(InventoryButtonId)!;
var characterButton = (UiButton)layout.FindElement(CharacterButtonId)!;
@@ -243,7 +243,7 @@ public class ToolbarControllerTests
ToolbarController.Bind(
layout,
repo,
- () => Array.Empty(),
+ () => Array.Empty(),
iconIds: (_, _, _, _, _) => 0u,
useItem: _ => { },
toggleCombat: () => toggles++);
@@ -263,7 +263,7 @@ public class ToolbarControllerTests
repo.AddOrUpdate(new ClientObject { ObjectId = itemId, Type = ItemType.Misc });
var shortcuts = new[]
{
- new PlayerDescriptionParser.ShortcutEntry(0, itemId, 0, 0),
+ new ShortcutEntry(0, itemId, 0),
};
uint used = 0;
uint selected = 0;
@@ -316,7 +316,7 @@ public class ToolbarControllerTests
nowMs: () => 1_000);
var shortcuts = new[]
{
- new PlayerDescriptionParser.ShortcutEntry(0, target, 0, 0),
+ new ShortcutEntry(0, target, 0),
};
var controller = ToolbarController.Bind(
layout,
@@ -358,11 +358,11 @@ public class ToolbarControllerTests
var controller = ToolbarController.Bind(
layout,
repo,
- () => Array.Empty(),
+ () => Array.Empty(),
iconIds: (_, _, _, _, _) => 1u,
useItem: _ => { },
itemInteraction: interaction,
- sendAddShortcut: (slot, id) => sends.Add((slot, id)));
+ sendAddShortcut: entry => sends.Add(((uint)entry.Index, entry.ObjectId)));
Assert.True(controller.CreateShortcutToItem(item));
Assert.Equal(item, slots[Row1[0]].Cell.ItemId);
@@ -384,7 +384,7 @@ public class ToolbarControllerTests
var repo = new ClientObjectTable();
ToolbarController.Bind(layout, repo,
- () => Array.Empty(),
+ () => Array.Empty(),
iconIds: (_,_,_,_,_) =>0u, useItem: _ => { });
// Only peace indicator (index 0 = 0x10000192) is visible.
@@ -404,7 +404,7 @@ public class ToolbarControllerTests
var repo = new ClientObjectTable();
var ctrl = ToolbarController.Bind(layout, repo,
- () => Array.Empty(),
+ () => Array.Empty(),
iconIds: (_,_,_,_,_) =>0u, useItem: _ => { });
ctrl.SetCombatMode(CombatMode.Melee);
@@ -426,7 +426,7 @@ public class ToolbarControllerTests
var combat = new CombatState();
ToolbarController.Bind(layout, repo,
- () => Array.Empty(),
+ () => Array.Empty(),
iconIds: (_,_,_,_,_) =>0u, useItem: _ => { },
combatState: combat);
@@ -465,7 +465,7 @@ public class ToolbarControllerTests
var repo = new ClientObjectTable();
ToolbarController.Bind(layout, repo,
- () => Array.Empty(),
+ () => Array.Empty(),
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
peaceDigits: FakePeace, warDigits: FakeWar);
@@ -490,7 +490,7 @@ public class ToolbarControllerTests
var (layout, slots, _) = FakeToolbar();
var repo = new ClientObjectTable();
var ctrl = ToolbarController.Bind(layout, repo,
- () => Array.Empty(),
+ () => Array.Empty(),
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
peaceDigits: FakePeace, warDigits: FakeWar);
@@ -517,7 +517,7 @@ public class ToolbarControllerTests
var (layout, slots, _) = FakeToolbar();
var repo = new ClientObjectTable();
var ctrl = ToolbarController.Bind(layout, repo,
- () => Array.Empty(),
+ () => Array.Empty(),
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
peaceDigits: FakePeace, warDigits: FakeWar);
@@ -539,7 +539,7 @@ public class ToolbarControllerTests
var repo = new ClientObjectTable();
ToolbarController.Bind(layout, repo,
- () => Array.Empty(),
+ () => Array.Empty(),
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
peaceDigits: FakePeace, warDigits: FakeWar);
@@ -561,7 +561,7 @@ public class ToolbarControllerTests
var repo = new ClientObjectTable();
ToolbarController.Bind(layout, repo,
- () => Array.Empty(),
+ () => Array.Empty(),
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
peaceDigits: FakePeace, warDigits: FakeWar, emptyDigits: FakeEmpty);
@@ -582,7 +582,7 @@ public class ToolbarControllerTests
var repo = new ClientObjectTable();
ToolbarController.Bind(layout, repo,
- () => Array.Empty(),
+ () => Array.Empty(),
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { },
peaceDigits: FakePeace, warDigits: FakeWar, emptyDigits: null);
@@ -604,8 +604,8 @@ public class ToolbarControllerTests
var (layout, _, _) = FakeToolbar();
var repo = new ClientObjectTable();
repo.AddOrUpdate(new ClientObject { ObjectId = 0x5001u, WeenieClassId = 1u, IconId = 0x06001234u });
- var shortcuts = new List
- { new(Index: 0, ObjectGuid: 0x5001u, SpellId: 0, Layer: 0) };
+ var shortcuts = new List
+ { new(Index: 0, ObjectId: 0x5001u, SpellId: 0) };
int iconCallCount = 0;
ToolbarController.Bind(layout, repo, () => shortcuts,
@@ -629,8 +629,8 @@ public class ToolbarControllerTests
{
var (layout, slots, _) = FakeToolbar();
var repo = new ClientObjectTable(); // item NOT present yet
- var shortcuts = new List
- { new(Index: 1, ObjectGuid: 0x5003u, SpellId: 0, Layer: 0) };
+ var shortcuts = new List
+ { new(Index: 1, ObjectId: 0x5003u, SpellId: 0) };
int iconCallCount = 0;
ToolbarController.Bind(layout, repo, () => shortcuts,
@@ -656,8 +656,8 @@ public class ToolbarControllerTests
var (layout, slots, _) = FakeToolbar();
var repo = new ClientObjectTable();
repo.AddOrUpdate(new ClientObject { ObjectId = 0x5004u, WeenieClassId = 1u, IconId = 0x06001234u });
- var shortcuts = new List
- { new(Index: 3, ObjectGuid: 0x5004u, SpellId: 0, Layer: 0) };
+ var shortcuts = new List
+ { new(Index: 3, ObjectId: 0x5004u, SpellId: 0) };
ToolbarController.Bind(layout, repo, () => shortcuts,
iconIds: (_,_,_,_,_) => 0xAAu, useItem: _ => { });
@@ -682,8 +682,8 @@ public class ToolbarControllerTests
var repo = new ClientObjectTable();
repo.AddOrUpdate(new ClientObject { ObjectId = 0x5005u, WeenieClassId = 1u, IconId = 0x06001234u });
repo.AddOrUpdate(new ClientObject { ObjectId = 0xCAFEBABEu, WeenieClassId = 99u, IconId = 0u });
- var shortcuts = new List
- { new(Index: 4, ObjectGuid: 0x5005u, SpellId: 0, Layer: 0) };
+ var shortcuts = new List
+ { new(Index: 4, ObjectId: 0x5005u, SpellId: 0) };
int iconCallCount = 0;
ToolbarController.Bind(layout, repo, () => shortcuts,
@@ -708,7 +708,7 @@ public class ToolbarControllerTests
var repo = new ClientObjectTable();
var ctrl = ToolbarController.Bind(layout, repo,
- () => Array.Empty(),
+ () => Array.Empty(),
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
for (int i = 0; i < Row1.Length; i++)
@@ -733,7 +733,7 @@ public class ToolbarControllerTests
{
var (layout, slots, _) = FakeToolbar();
var ctrl = ToolbarController.Bind(layout, new ClientObjectTable(),
- () => Array.Empty(),
+ () => Array.Empty(),
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
var list = slots[Row1[0]];
@@ -748,7 +748,7 @@ public class ToolbarControllerTests
{
var (layout, slots, _) = FakeToolbar();
var ctrl = ToolbarController.Bind(layout, new ClientObjectTable(),
- () => Array.Empty(),
+ () => Array.Empty(),
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
var list = slots[Row1[0]];
var payload = new ItemDragPayload(0u, ItemDragSource.Inventory, 0, new UiItemSlot());
@@ -756,12 +756,12 @@ public class ToolbarControllerTests
}
// ── B.2: live drag handler (store + reorder/remove + wire) ───────────────
- private static (System.Collections.Generic.List<(uint i,uint g)> adds,
- System.Collections.Generic.List removes) NewSpies(out System.Action add, out System.Action rem)
+ private static (System.Collections.Generic.List adds,
+ System.Collections.Generic.List removes) NewSpies(out System.Action add, out System.Action rem)
{
- var adds = new System.Collections.Generic.List<(uint,uint)>();
+ var adds = new System.Collections.Generic.List();
var removes = new System.Collections.Generic.List();
- add = (i, g) => adds.Add((i, g));
+ add = entry => adds.Add(entry);
rem = i => removes.Add(i);
return (adds, removes);
}
@@ -772,8 +772,8 @@ public class ToolbarControllerTests
var (layout, slots, _) = FakeToolbar();
var repo = new ClientObjectTable();
repo.AddOrUpdate(new ClientObject { ObjectId = 0x5001u, WeenieClassId = 1u, IconId = 0x06001234u });
- var shortcuts = new System.Collections.Generic.List
- { new(Index: 3, ObjectGuid: 0x5001u, SpellId: 0, Layer: 0) };
+ var shortcuts = new System.Collections.Generic.List
+ { new(Index: 3, ObjectId: 0x5001u, SpellId: 0) };
var (adds, removes) = NewSpies(out var add, out var rem);
var ctrl = ToolbarController.Bind(layout, repo, () => shortcuts,
@@ -795,9 +795,9 @@ public class ToolbarControllerTests
var repo = new ClientObjectTable();
repo.AddOrUpdate(new ClientObject { ObjectId = 0x5001u, WeenieClassId = 1u, IconId = 0x06001234u }); // A
repo.AddOrUpdate(new ClientObject { ObjectId = 0x5002u, WeenieClassId = 1u, IconId = 0x06005678u }); // B
- var shortcuts = new System.Collections.Generic.List
- { new(Index: 3, ObjectGuid: 0x5001u, SpellId: 0, Layer: 0),
- new(Index: 5, ObjectGuid: 0x5002u, SpellId: 0, Layer: 0) };
+ var shortcuts = new System.Collections.Generic.List
+ { new(Index: 3, ObjectId: 0x5001u, SpellId: 0),
+ new(Index: 5, ObjectId: 0x5002u, SpellId: 0) };
var (adds, removes) = NewSpies(out var add, out var rem);
var ctrl = ToolbarController.Bind(layout, repo, () => shortcuts,
iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { },
@@ -810,8 +810,36 @@ public class ToolbarControllerTests
Assert.Equal(0x5001u, slots[Row1[5]].Cell.ItemId);
Assert.Equal(0x5002u, slots[Row1[3]].Cell.ItemId);
Assert.Equal(new[] { 3u, 5u }, removes.ToArray());
- Assert.Contains((5u, 0x5001u), adds);
- Assert.Contains((3u, 0x5002u), adds);
+ Assert.Contains(new ShortcutEntry(5, 0x5001u, 0u), adds);
+ Assert.Contains(new ShortcutEntry(3, 0x5002u, 0u), adds);
+ }
+
+ [Fact]
+ public void HandleDropRelease_reindexesWithoutLosingRawShortcutFields()
+ {
+ var (layout, slots, _) = FakeToolbar();
+ var repo = new ClientObjectTable();
+ repo.AddOrUpdate(new ClientObject { ObjectId = 0x5001u, IconId = 1u });
+ repo.AddOrUpdate(new ClientObject { ObjectId = 0x5002u, IconId = 2u });
+ var shortcuts = new[]
+ {
+ new ShortcutEntry(3, 0x5001u, 0x11223344u),
+ new ShortcutEntry(5, 0x5002u, 0xA5C31234u),
+ };
+ var (adds, _) = NewSpies(out var add, out var remove);
+ var controller = ToolbarController.Bind(
+ layout, repo, () => shortcuts,
+ iconIds: (_, _, _, _, _) => 1u,
+ useItem: _ => { },
+ sendAddShortcut: add,
+ sendRemoveShortcut: remove);
+
+ var payload = Assert.IsType(slots[Row1[3]].Cell.GetDragPayload());
+ controller.OnDragLift(slots[Row1[3]], slots[Row1[3]].Cell, payload);
+ controller.HandleDropRelease(slots[Row1[5]], slots[Row1[5]].Cell, payload);
+
+ Assert.Contains(new ShortcutEntry(5, 0x5001u, 0x11223344u), adds);
+ Assert.Contains(new ShortcutEntry(3, 0x5002u, 0xA5C31234u), adds);
}
[Fact]
@@ -820,8 +848,8 @@ public class ToolbarControllerTests
var (layout, slots, _) = FakeToolbar();
var repo = new ClientObjectTable();
repo.AddOrUpdate(new ClientObject { ObjectId = 0x5001u, WeenieClassId = 1u, IconId = 0x06001234u });
- var shortcuts = new System.Collections.Generic.List
- { new(Index: 3, ObjectGuid: 0x5001u, SpellId: 0, Layer: 0) };
+ var shortcuts = new System.Collections.Generic.List
+ { new(Index: 3, ObjectId: 0x5001u, SpellId: 0) };
var (adds, removes) = NewSpies(out var add, out var rem);
var ctrl = ToolbarController.Bind(layout, repo, () => shortcuts,
iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { },
@@ -833,8 +861,8 @@ public class ToolbarControllerTests
Assert.Equal(0x5001u, slots[Row1[7]].Cell.ItemId);
Assert.Equal(0u, slots[Row1[3]].Cell.ItemId);
- Assert.Contains((7u, 0x5001u), adds);
- Assert.DoesNotContain(adds, a => a.i == 3u);
+ Assert.Contains(new ShortcutEntry(7, 0x5001u, 0u), adds);
+ Assert.DoesNotContain(adds, a => a.Index == 3);
}
[Fact]
@@ -843,8 +871,8 @@ public class ToolbarControllerTests
var (layout, slots, _) = FakeToolbar();
var repo = new ClientObjectTable();
repo.AddOrUpdate(new ClientObject { ObjectId = 0x5001u, WeenieClassId = 1u, IconId = 0x06001234u });
- var shortcuts = new System.Collections.Generic.List
- { new(Index: 3, ObjectGuid: 0x5001u, SpellId: 0, Layer: 0) };
+ var shortcuts = new System.Collections.Generic.List
+ { new(Index: 3, ObjectId: 0x5001u, SpellId: 0) };
var (adds, removes) = NewSpies(out var add, out var rem);
var ctrl = ToolbarController.Bind(layout, repo, () => shortcuts,
iconIds: (_,_,_,_,_) => 0x77u, useItem: _ => { },
@@ -855,7 +883,7 @@ public class ToolbarControllerTests
ctrl.HandleDropRelease(slots[Row1[3]], slots[Row1[3]].Cell, payload); // drop back on slot 3
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.Contains(new ShortcutEntry(3, 0x5001u, 0u), adds); // AddShortcut(3, A) sent
Assert.Single(removes); // exactly RemoveShortcut(3) [from the lift]
Assert.Single(adds); // exactly AddShortcut(3, A) [the re-place]
}
@@ -865,7 +893,7 @@ public class ToolbarControllerTests
{
var (layout, slots, _) = FakeToolbar();
ToolbarController.Bind(layout, new ClientObjectTable(),
- () => System.Array.Empty(),
+ () => System.Array.Empty(),
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
Assert.Equal(0x060011FAu, slots[Row1[0]].Cell.DragAcceptSprite); // green cross, not the ring F9
}
diff --git a/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs b/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs
index 455b6c4d..658cd99b 100644
--- a/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs
+++ b/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs
@@ -572,7 +572,7 @@ public sealed class GameEventWiringTests
// for the harness pattern; adds the Shortcut flag (0x1) + one 12-byte
// entry, followed by the legacy-hotbar count (0) + spellbook_filters (0)
// then empty inventory and equipped.
- IReadOnlyList? got = null;
+ IReadOnlyList? got = null;
var dispatcher = new GameEventDispatcher();
var items = new ClientObjectTable();
@@ -629,7 +629,7 @@ public sealed class GameEventWiringTests
Assert.NotNull(got);
Assert.Single(got!);
- Assert.Equal(0x5001u, got![0].ObjectGuid);
+ Assert.Equal(0x5001u, got![0].ObjectId);
}
[Fact]
diff --git a/tests/AcDream.Core.Net.Tests/Messages/InventoryActionsTests.cs b/tests/AcDream.Core.Net.Tests/Messages/InventoryActionsTests.cs
index 750147d0..fc4bcb9f 100644
--- a/tests/AcDream.Core.Net.Tests/Messages/InventoryActionsTests.cs
+++ b/tests/AcDream.Core.Net.Tests/Messages/InventoryActionsTests.cs
@@ -1,5 +1,6 @@
using System;
using System.Buffers.Binary;
+using AcDream.Core.Items;
using AcDream.Core.Net.Messages;
using Xunit;
@@ -76,23 +77,23 @@ public sealed class InventoryActionsTests
[Fact]
public void BuildAddShortcut_ItemShortcut_FieldLayout()
{
- // ShortCutData = Index(u32), ObjectId(u32), SpellId(u16), Layer(u16). Item → spell/layer 0.
- byte[] body = InventoryActions.BuildAddShortcut(seq: 1, index: 0, objectGuid: 0x3E1, spellId: 0, layer: 0);
+ var entry = new ShortcutEntry(Index: 0, ObjectId: 0x3E1u, SpellId: 0u);
+ byte[] body = InventoryActions.BuildAddShortcut(seq: 1, entry);
Assert.Equal(24, body.Length);
Assert.Equal(InventoryActions.AddShortcutOpcode,
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
- Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(12))); // index
- Assert.Equal(0x3E1u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(16))); // objectGuid
- Assert.Equal((ushort)0, BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(20))); // spellId
- Assert.Equal((ushort)0, BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(22))); // layer
+ Assert.Equal(0, BinaryPrimitives.ReadInt32LittleEndian(body.AsSpan(12)));
+ Assert.Equal(0x3E1u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(16)));
+ Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(20)));
}
[Fact]
- public void BuildAddShortcut_SpellShortcut_PacksSpellAndLayerAsU16s()
+ public void BuildAddShortcut_PreservesSignedIndexAndRawSpellWord()
{
- byte[] body = InventoryActions.BuildAddShortcut(seq: 1, index: 2, objectGuid: 0, spellId: 0x1234, layer: 3);
- Assert.Equal(0x1234, BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(20)));
- Assert.Equal(3, BinaryPrimitives.ReadUInt16LittleEndian(body.AsSpan(22)));
+ var entry = new ShortcutEntry(Index: -1, ObjectId: 0u, SpellId: 0xA5C31234u);
+ byte[] body = InventoryActions.BuildAddShortcut(seq: 1, entry);
+ Assert.Equal(-1, BinaryPrimitives.ReadInt32LittleEndian(body.AsSpan(12)));
+ Assert.Equal(0xA5C31234u, BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(20)));
}
[Fact]
diff --git a/tests/AcDream.Core.Net.Tests/PlayerDescriptionParserTests.cs b/tests/AcDream.Core.Net.Tests/PlayerDescriptionParserTests.cs
index c74df049..9c08931a 100644
--- a/tests/AcDream.Core.Net.Tests/PlayerDescriptionParserTests.cs
+++ b/tests/AcDream.Core.Net.Tests/PlayerDescriptionParserTests.cs
@@ -390,21 +390,20 @@ public sealed class PlayerDescriptionParserTests
writer.Write(0x01u); // option_flags = SHORTCUT
writer.Write(0xCAFEu); // options1 sentinel
- // Shortcut count + 2 entries (16 B each).
+ // Shortcut count + 2 packed ShortCutData entries (12 B each).
writer.Write(2u);
- writer.Write(0u); writer.Write(0xAABBCCDDu); writer.Write((ushort)0); writer.Write((ushort)0);
- writer.Write(7u); writer.Write(0u); writer.Write((ushort)1234); writer.Write((ushort)5);
+ writer.Write(0); writer.Write(0xAABBCCDDu); writer.Write(0u);
+ writer.Write(7); writer.Write(0u); writer.Write(0x000504D2u);
var parsed = PlayerDescriptionParser.TryParse(sb.ToArray());
Assert.NotNull(parsed);
Assert.Equal(2, parsed!.Value.Shortcuts.Count);
- Assert.Equal(0u, parsed.Value.Shortcuts[0].Index);
- Assert.Equal(0xAABBCCDDu, parsed.Value.Shortcuts[0].ObjectGuid);
- Assert.Equal((ushort)0, parsed.Value.Shortcuts[0].SpellId);
- Assert.Equal(7u, parsed.Value.Shortcuts[1].Index);
- Assert.Equal((ushort)1234, parsed.Value.Shortcuts[1].SpellId);
- Assert.Equal((ushort)5, parsed.Value.Shortcuts[1].Layer);
+ Assert.Equal(0, parsed.Value.Shortcuts[0].Index);
+ Assert.Equal(0xAABBCCDDu, parsed.Value.Shortcuts[0].ObjectId);
+ Assert.Equal(0u, parsed.Value.Shortcuts[0].SpellId);
+ Assert.Equal(7, parsed.Value.Shortcuts[1].Index);
+ Assert.Equal(0x000504D2u, parsed.Value.Shortcuts[1].SpellId);
}
[Fact]
@@ -424,9 +423,9 @@ public sealed class PlayerDescriptionParserTests
writer.Write(3u); // claimed shortcut count = 3
// First entry complete (16 B).
writer.Write(1u); writer.Write(0xAAAAu); writer.Write((ushort)10); writer.Write((ushort)1);
- // Second entry truncated to 8 bytes — ReadU16 will throw FormatException.
+ // Second entry truncated to 8 bytes — the raw spell-word read throws.
writer.Write(2u); writer.Write(0xBBBBu);
- // (no SpellId/Layer — payload ends here)
+ // (no raw SpellId word — payload ends here)
var parsed = PlayerDescriptionParser.TryParse(sb.ToArray());
@@ -435,7 +434,7 @@ public sealed class PlayerDescriptionParserTests
Assert.True(parsed!.Value.TrailerTruncated);
// First entry survives in the partial list.
Assert.Single(parsed.Value.Shortcuts);
- Assert.Equal(1u, parsed.Value.Shortcuts[0].Index);
+ Assert.Equal(1, parsed.Value.Shortcuts[0].Index);
}
[Fact]
@@ -725,7 +724,7 @@ public sealed class PlayerDescriptionParserTests
// Shortcuts: count=1
writer.Write(1u);
- writer.Write(3u); writer.Write(0xCAFEFACEu); writer.Write((ushort)100); writer.Write((ushort)2);
+ writer.Write(3); writer.Write(0xCAFEFACEu); writer.Write(0x00020064u);
// 8 hotbars, all empty for brevity.
for (int i = 0; i < 8; i++) writer.Write(0u);
@@ -754,7 +753,8 @@ public sealed class PlayerDescriptionParserTests
Assert.Equal(0xBB000002u, v.Options2);
Assert.Equal(0xF11Du, v.SpellbookFilters);
Assert.Single(v.Shortcuts);
- Assert.Equal(0xCAFEFACEu, v.Shortcuts[0].ObjectGuid);
+ Assert.Equal(0xCAFEFACEu, v.Shortcuts[0].ObjectId);
+ Assert.Equal(0x00020064u, v.Shortcuts[0].SpellId);
Assert.Equal(8, v.HotbarSpells.Count);
Assert.All(v.HotbarSpells, l => Assert.Empty(l));
Assert.Single(v.DesiredComps);
diff --git a/tests/AcDream.Core.Tests/Items/ShortcutStoreTests.cs b/tests/AcDream.Core.Tests/Items/ShortcutStoreTests.cs
index a58dabd9..c927daee 100644
--- a/tests/AcDream.Core.Tests/Items/ShortcutStoreTests.cs
+++ b/tests/AcDream.Core.Tests/Items/ShortcutStoreTests.cs
@@ -6,34 +6,39 @@ namespace AcDream.Core.Tests.Items;
public class ShortcutStoreTests
{
[Fact]
- public void Load_mapsSlotToObjId_skipsEmptyAndOutOfRange()
+ public void Load_preservesAllRawFieldsAndRejectsOutOfRangeIndex()
{
var store = new ShortcutStore();
- store.Load(new (int, uint)[]
+ store.Load(new[]
{
- (0, 0x5001u),
- (3, 0x5002u),
- (5, 0u), // empty/spell → skipped
- (99, 0x5003u), // out of range → skipped
+ new ShortcutEntry(0, 0x5001u, 0x11223344u),
+ new ShortcutEntry(3, 0x5002u, 0u),
+ new ShortcutEntry(5, 0u, 0xA5C31234u),
+ new ShortcutEntry(-1, 0x5003u, 1u),
+ new ShortcutEntry(18, 0x5004u, 2u),
});
Assert.Equal(0x5001u, store.Get(0));
Assert.Equal(0x5002u, store.Get(3));
Assert.Equal(0u, store.Get(5));
+ Assert.Equal(new ShortcutEntry(5, 0u, 0xA5C31234u), store.GetEntry(5));
+ Assert.Equal(0x11223344u, store.GetEntry(0)!.Value.SpellId);
Assert.True(store.IsEmpty(1));
+ Assert.Null(store.GetEntry(-1));
+ Assert.Null(store.GetEntry(18));
}
[Fact]
public void SetRemoveGet_roundtrip_andBoundsSafe()
{
var store = new ShortcutStore();
- store.Set(4, 0xABCDu);
+ store.SetItem(4, 0xABCDu);
Assert.Equal(0xABCDu, store.Get(4));
Assert.False(store.IsEmpty(4));
store.Remove(4);
Assert.True(store.IsEmpty(4));
Assert.Equal(0u, store.Get(-1));
Assert.Equal(0u, store.Get(18));
- store.Set(18, 1u); // no-op, no throw
+ store.SetItem(18, 1u); // no-op, no throw
store.Remove(-1); // no-op, no throw
}
}