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

@ -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();
/// <summary>Persisted hotbar shortcuts from the last PlayerDescription (D.5.1 toolbar source).</summary>
public IReadOnlyList<AcDream.Core.Net.Messages.PlayerDescriptionParser.ShortcutEntry> Shortcuts { get; private set; }
= System.Array.Empty<AcDream.Core.Net.Messages.PlayerDescriptionParser.ShortcutEntry>();
public IReadOnlyList<AcDream.Core.Items.ShortcutEntry> Shortcuts { get; private set; }
= System.Array.Empty<AcDream.Core.Items.ShortcutEntry>();
// 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,

View file

@ -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<PlayerDescriptionParser.ShortcutEntry>(),
shortcuts: () => System.Array.Empty<ShortcutEntry>(),
iconIds: MakeIconIds(stack),
useItem: _ => { },
combatState: null,

View file

@ -1,3 +1,5 @@
using AcDream.Core.Items;
namespace AcDream.App.UI;
/// <summary>
@ -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

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();
}

View file

@ -37,13 +37,13 @@ public sealed record RadarRuntimeBindings(
public sealed record ToolbarRuntimeBindings(
ClientObjectTable Objects,
Func<IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>> Shortcuts,
Func<IReadOnlyList<ShortcutEntry>> Shortcuts,
Func<ItemType, uint, uint, uint, uint, uint> ResolveIcon,
Action<uint> UseItem,
CombatState Combat,
Action ToggleCombat,
ItemInteractionController ItemInteraction,
Action<uint, uint>? SendAddShortcut,
Action<ShortcutEntry>? SendAddShortcut,
Action<uint>? SendRemoveShortcut,
SelectionState Selection,
Action<Action<uint, float>> SubscribeHealthChanged,

View file

@ -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
/// <summary>Pre-composited icon GL texture for the bound item (0 = none).</summary>
public uint IconTexture { get; private set; }
/// <summary>
/// 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.
/// </summary>
public ShortcutEntry? Shortcut { get; private set; }
/// <summary>This cell's own index within its panel (0..17 toolbar; container slot
/// for inventory). Distinct from <see cref="ShortcutNum"/> (the 19 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
/// <summary>RenderSurface id -> (GL texture, w, h). Set by the factory/controller.</summary>
public Func<uint, (uint tex, int w, int h)>? 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; }
/// <inheritdoc/>
public override object? GetDragPayload()
=> ItemId != 0 ? new ItemDragPayload(ItemId, SourceKind, SlotIndex, this) : null;
=> ItemId != 0 ? new ItemDragPayload(ItemId, SourceKind, SlotIndex, this, Shortcut) : null;
/// <inheritdoc/>
public override (uint tex, int w, int h)? GetDragGhost()