feat(ui): preserve exact retail shortcut records

Carry signed index, object id, and raw spell word losslessly through PlayerDescription, session storage, drag mutation, and AddShortcut wire serialization while keeping gmToolbarUI object-only. Retire AP-103 and record the live ACE relog persistence gate.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-11 09:17:33 +02:00
parent e65119f0c6
commit b5b230c860
21 changed files with 271 additions and 181 deletions

View file

@ -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<IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>> _shortcuts;
private readonly Func<IReadOnlyList<ShortcutEntry>> _shortcuts;
private readonly Func<ItemType, uint, uint, uint, uint, uint> _iconIds; // (itemType, icon, underlay, overlay, effects) → GL tex
private readonly Action<uint> _useItem; // guid → fire UseObject
private readonly AcDream.Core.Items.ShortcutStore _store = new();
private readonly ShortcutStore _store = new();
private bool _storeLoaded;
private readonly Action<uint, uint>? _sendAddShortcut; // (index, objectGuid)
private readonly Action<ShortcutEntry>? _sendAddShortcut;
private readonly Action<uint>? _sendRemoveShortcut; // (index)
private readonly ItemInteractionController? _itemInteraction;
private readonly Action<uint>? _selectItem;
@ -89,7 +89,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
private ToolbarController(
ImportedLayout layout,
ClientObjectTable repo,
Func<IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>> shortcuts,
Func<IReadOnlyList<ShortcutEntry>> shortcuts,
Func<ItemType, uint, uint, uint, uint, uint> iconIds,
Action<uint> useItem,
CombatState? combatState,
@ -97,7 +97,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
uint[]? warDigits,
uint[]? emptyDigits,
ItemInteractionController? itemInteraction = null,
Action<uint, uint>? sendAddShortcut = null,
Action<ShortcutEntry>? sendAddShortcut = null,
Action<uint>? sendRemoveShortcut = null,
Action? toggleCombat = null,
Action<uint>? 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<IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>> shortcuts,
Func<IReadOnlyList<ShortcutEntry>> shortcuts,
Func<ItemType, uint, uint, uint, uint, uint> iconIds,
Action<uint> useItem,
CombatState? combatState = null,
@ -234,7 +234,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
uint[]? warDigits = null,
uint[]? emptyDigits = null,
ItemInteractionController? itemInteraction = null,
Action<uint, uint>? sendAddShortcut = null,
Action<ShortcutEntry>? sendAddShortcut = null,
Action<uint>? sendRemoveShortcut = null,
Action? toggleCombat = null,
Action<uint>? 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();
}