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:
parent
e65119f0c6
commit
b5b230c860
21 changed files with 271 additions and 181 deletions
14
src/AcDream.Core/Items/ShortcutEntry.cs
Normal file
14
src/AcDream.Core/Items/ShortcutEntry.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
namespace AcDream.Core.Items;
|
||||
|
||||
/// <summary>
|
||||
/// Lossless retail <c>ShortCutData</c> 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:
|
||||
/// <c>ShortCutData::ShortCutData @ 0x005D55E0</c>,
|
||||
/// <c>ShortCutManager::AddShortCut @ 0x005D5790</c>, and
|
||||
/// <c>acclient.h:36484</c>.
|
||||
/// </summary>
|
||||
public readonly record struct ShortcutEntry(int Index, uint ObjectId, uint SpellId)
|
||||
{
|
||||
public ShortcutEntry WithIndex(int index) => this with { Index = index };
|
||||
}
|
||||
|
|
@ -4,31 +4,53 @@ using System.Collections.Generic;
|
|||
namespace AcDream.Core.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 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 <c>ShortCutManager::shortCuts_[18]</c>
|
||||
/// (<c>acclient.h:36492</c>). Every present entry retains all three raw
|
||||
/// <see cref="ShortcutEntry"/> fields, including non-object records that the object-only
|
||||
/// <c>gmToolbarUI</c> does not render.
|
||||
/// </summary>
|
||||
public sealed class ShortcutStore
|
||||
{
|
||||
public const int SlotCount = 18;
|
||||
private readonly uint[] _objIds = new uint[SlotCount];
|
||||
private readonly ShortcutEntry?[] _entries = new ShortcutEntry?[SlotCount];
|
||||
|
||||
/// <summary>Replace all slots from a (slot, objectGuid) sequence (item entries only;
|
||||
/// ObjGuid 0 and out-of-range slots are skipped).</summary>
|
||||
public void Load(IEnumerable<(int Slot, uint ObjGuid)> entries)
|
||||
/// <summary>
|
||||
/// Replace all slots from packed entries. Invalid signed indices are rejected exactly
|
||||
/// like <c>ShortCutManager::AddShortCut @ 0x005D5790</c>; valid entries are retained
|
||||
/// even when their object id is zero.
|
||||
/// </summary>
|
||||
public void Load(IEnumerable<ShortcutEntry> 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);
|
||||
}
|
||||
|
||||
/// <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;
|
||||
/// <summary>Raw entry at <paramref name="slot"/>, or null for absent/out-of-range.</summary>
|
||||
public ShortcutEntry? GetEntry(int slot)
|
||||
=> (uint)slot < SlotCount ? _entries[slot] : null;
|
||||
|
||||
/// <summary>Visible object id at <paramref name="slot"/>, or zero.</summary>
|
||||
public uint Get(int slot) => GetEntry(slot)?.ObjectId ?? 0u;
|
||||
|
||||
/// <summary>
|
||||
/// Visual availability used by <c>gmToolbarUI</c>. A retained non-object entry remains
|
||||
/// invisible and therefore counts as an empty toolbar cell.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Create/replace a retail object shortcut; toolbar-created records use spell word zero.</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue