feat(ui): D.5.3/B.2 — toolbar reorder/remove via ShortcutStore + wire + green-cross overlay
ToolbarController is now the LIVE drag handler: - OnDragLift: removes the source slot from ShortcutStore, sends RemoveShortcut (0x019D) wire immediately (retail remove-on-lift model). - HandleDropRelease: evicts occupant from target (RemoveShortcut), places dragged item (AddShortcut 0x019C), bumps evicted item into the vacated source slot if empty (swap path); off-bar release leaves the lift's removal standing. - Populate() now lazy-loads ShortcutStore from the PD shortcut list on first call; store is authoritative thereafter. - IsShortcutGuid() uses the store (O(18)) when loaded, falls back to scanning _shortcuts() in the pre-PD window. - All 18 slot cells now get DragAcceptSprite=0x060011FA (green cross, distinct from the inventory ring 0x060011F9). - GameWindow.Bind wired: sendAddShortcut + sendRemoveShortcut lambdas forwarded to _liveSession?.Send*(). - Divergence register: AP-47 Divergence+Risk cells updated (opacity corrected, underlay-backing framing improved). TS-33 row deleted (stopgap retired). Section header 32→31 rows. - Tests: HandleDropRelease_isInertStub removed; 5 new B.2 tests added (lift removes + sends, swap sequence, empty-target place, self-drop re-adds, green-cross sprite). 24/24 ToolbarController tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
41bb70c11a
commit
250f7827be
4 changed files with 179 additions and 38 deletions
|
|
@ -2018,7 +2018,9 @@ public sealed class GameWindow : IDisposable
|
|||
combatState: Combat,
|
||||
peaceDigits: toolbarPeaceDigits,
|
||||
warDigits: toolbarWarDigits,
|
||||
emptyDigits: toolbarEmptyDigits);
|
||||
emptyDigits: toolbarEmptyDigits,
|
||||
sendAddShortcut: (i, g) => _liveSession?.SendAddShortcut(i, g),
|
||||
sendRemoveShortcut: i => _liveSession?.SendRemoveShortcut(i));
|
||||
|
||||
// Phase D.5.3a — selected-object strip (name, overlay state, health meter).
|
||||
// Analogue of retail gmToolbarUI::HandleSelectionChanged
|
||||
|
|
|
|||
|
|
@ -60,6 +60,10 @@ public sealed class ToolbarController : IItemListDragHandler
|
|||
private readonly Func<IReadOnlyList<PlayerDescriptionParser.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.Net.Items.ShortcutStore _store = new();
|
||||
private bool _storeLoaded;
|
||||
private readonly Action<uint, uint>? _sendAddShortcut; // (index, objectGuid)
|
||||
private readonly Action<uint>? _sendRemoveShortcut; // (index)
|
||||
|
||||
// Digit sprite DID arrays for slot labels (top row, numbers 1-9).
|
||||
// Read from LayoutDesc 0x21000037, element 0x1000034A under composite 0x10000346.
|
||||
|
|
@ -82,7 +86,9 @@ public sealed class ToolbarController : IItemListDragHandler
|
|||
CombatState? combatState,
|
||||
uint[]? peaceDigits,
|
||||
uint[]? warDigits,
|
||||
uint[]? emptyDigits)
|
||||
uint[]? emptyDigits,
|
||||
Action<uint, uint>? sendAddShortcut = null,
|
||||
Action<uint>? sendRemoveShortcut = null)
|
||||
{
|
||||
_repo = repo;
|
||||
_shortcuts = shortcuts;
|
||||
|
|
@ -91,6 +97,8 @@ public sealed class ToolbarController : IItemListDragHandler
|
|||
_peaceDigits = peaceDigits;
|
||||
_warDigits = warDigits;
|
||||
_emptyDigits = emptyDigits;
|
||||
_sendAddShortcut = sendAddShortcut;
|
||||
_sendRemoveShortcut = sendRemoveShortcut;
|
||||
|
||||
for (int i = 0; i < SlotIds.Length; i++)
|
||||
{
|
||||
|
|
@ -104,6 +112,7 @@ public sealed class ToolbarController : IItemListDragHandler
|
|||
list.RegisterDragHandler(this);
|
||||
list.Cell.SlotIndex = i;
|
||||
list.Cell.SourceKind = ItemDragSource.ShortcutBar;
|
||||
list.Cell.DragAcceptSprite = 0x060011FAu; // green cross (toolbar), not the ring 0x060011F9 (inventory)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -133,11 +142,20 @@ public sealed class ToolbarController : IItemListDragHandler
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if <paramref name="guid"/> is one of the currently-active shortcut guids.
|
||||
/// Returns true if <paramref name="guid"/> is one of the currently-active shortcut guids
|
||||
/// (i.e., present in the live <see cref="AcDream.Core.Net.Items.ShortcutStore"/>).
|
||||
/// Used to gate repo-event subscriptions so we don't re-populate on every creature spawn.
|
||||
/// Falls back to scanning <c>_shortcuts()</c> before the store is loaded (pre-PD window).
|
||||
/// </summary>
|
||||
private bool IsShortcutGuid(uint guid)
|
||||
{
|
||||
if (_storeLoaded)
|
||||
{
|
||||
for (int s = 0; s < AcDream.Core.Net.Items.ShortcutStore.SlotCount; s++)
|
||||
if (_store.Get(s) == guid) return true;
|
||||
return false;
|
||||
}
|
||||
// Store not yet loaded — fall back to the shortcuts provider (pre-PD window).
|
||||
foreach (var sc in _shortcuts())
|
||||
if (sc.ObjectGuid == guid) return true;
|
||||
return false;
|
||||
|
|
@ -182,10 +200,13 @@ public sealed class ToolbarController : IItemListDragHandler
|
|||
CombatState? combatState = null,
|
||||
uint[]? peaceDigits = null,
|
||||
uint[]? warDigits = null,
|
||||
uint[]? emptyDigits = null)
|
||||
uint[]? emptyDigits = null,
|
||||
Action<uint, uint>? sendAddShortcut = null,
|
||||
Action<uint>? sendRemoveShortcut = null)
|
||||
{
|
||||
var c = new ToolbarController(layout, repo, shortcuts, iconIds, useItem, combatState,
|
||||
peaceDigits, warDigits, emptyDigits);
|
||||
peaceDigits, warDigits, emptyDigits,
|
||||
sendAddShortcut, sendRemoveShortcut);
|
||||
c.Populate();
|
||||
return c;
|
||||
}
|
||||
|
|
@ -196,24 +217,27 @@ public sealed class ToolbarController : IItemListDragHandler
|
|||
/// Entries whose item is not yet in the repo are silently skipped here; the
|
||||
/// <c>ObjectAdded</c> event re-fires this method when the item arrives
|
||||
/// (matching retail's <c>SetDelayedShortcutNum</c> deferred-rebind path).
|
||||
/// As of B.2: the <see cref="AcDream.Core.Net.Items.ShortcutStore"/> is the
|
||||
/// authoritative in-session slot map; <c>_shortcuts()</c> seeds it on first call.
|
||||
/// </summary>
|
||||
public void Populate()
|
||||
{
|
||||
// Clear all slot cells first (flush).
|
||||
// Lazy-load the store from the login shortcut list the first time it's present (PD arrives
|
||||
// after Bind); thereafter the store is authoritative and drag ops mutate it directly.
|
||||
if (!_storeLoaded && _shortcuts().Count > 0) { _store.Load(_shortcuts()); _storeLoaded = true; }
|
||||
|
||||
foreach (var list in _slots) list?.Cell.Clear();
|
||||
|
||||
foreach (var sc in _shortcuts())
|
||||
for (int slot = 0; slot < _slots.Length; slot++)
|
||||
{
|
||||
if (sc.ObjectGuid == 0) continue; // spell-only shortcut — inventory phase
|
||||
if (sc.Index >= (uint)_slots.Length) continue;
|
||||
var list = _slots[(int)sc.Index];
|
||||
uint guid = _store.Get(slot);
|
||||
if (guid == 0) continue;
|
||||
var list = _slots[slot];
|
||||
if (list is null) continue;
|
||||
|
||||
var item = _repo.Get(sc.ObjectGuid);
|
||||
if (item is null) continue; // deferred: ObjectAdded will re-call Populate
|
||||
|
||||
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(sc.ObjectGuid, tex);
|
||||
list.Cell.SetItem(guid, tex);
|
||||
}
|
||||
|
||||
// Re-stamp slot number labels after any item change.
|
||||
|
|
@ -296,13 +320,24 @@ public sealed class ToolbarController : IItemListDragHandler
|
|||
};
|
||||
}
|
||||
|
||||
// ── IItemListDragHandler (B.1 spine) ─────────────────────────────────────
|
||||
// ── IItemListDragHandler (B.2 live handler) ──────────────────────────────
|
||||
// Retail: gmToolbarUI is the m_dragHandler for every shortcut slot list.
|
||||
// The accept/reject gate (IsShortcutEligible) and the AddShortcut/RemoveShortcut
|
||||
// wire are Stream B.2; this stub accepts any real item and LOGS the drop (TS-33).
|
||||
// Retail model (remove-on-lift / place-on-drop / no-restore):
|
||||
// lift → RemoveShortcut (0x019D) + store.Remove (slot empties immediately)
|
||||
// drop → AddShortcut (0x019C) + optional swap of evicted item into source
|
||||
// off-bar release → the lift's removal stands (no restore)
|
||||
// Retail ref: gmToolbarUI::HandleDropRelease acclient_2013_pseudo_c.txt:197971
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void OnDragLift(UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload) { /* Task 3: remove + wire */ }
|
||||
public void OnDragLift(UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload)
|
||||
{
|
||||
// Retail RecvNotice_ItemListBeginDrag → RemoveShortcut (0x004bd930/0x004bd450): the lifted
|
||||
// shortcut leaves the bar (+ wire) the instant the drag starts; it's re-placed only on a
|
||||
// drop onto a slot. Off-bar release leaves it removed.
|
||||
_store.Remove(payload.SourceSlot);
|
||||
_sendRemoveShortcut?.Invoke((uint)payload.SourceSlot);
|
||||
Populate();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool OnDragOver(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
|
||||
|
|
@ -311,9 +346,19 @@ public sealed class ToolbarController : IItemListDragHandler
|
|||
/// <inheritdoc/>
|
||||
public void HandleDropRelease(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
|
||||
{
|
||||
// TS-33: no wire yet. Stream B.2 replaces this with the ShortcutStore mutation +
|
||||
// AddShortcut 0x019C / RemoveShortcut 0x019D sends (gmToolbarUI::HandleDropRelease :197971).
|
||||
Console.WriteLine($"[B.2 TODO] drop obj 0x{payload.ObjId:X8} from {payload.SourceKind} " +
|
||||
$"slot {payload.SourceSlot} → toolbar slot {targetCell.SlotIndex}");
|
||||
// Retail gmToolbarUI::HandleDropRelease (0x004be7c0) within-bar reorder branch (deep-dive §5):
|
||||
// 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)
|
||||
if (evicted != 0) { _store.Remove(target); _sendRemoveShortcut?.Invoke((uint)target); }
|
||||
_store.Set(target, payload.ObjId); // AddShortcut(dragged, target)
|
||||
_sendAddShortcut?.Invoke((uint)target, payload.ObjId);
|
||||
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);
|
||||
}
|
||||
Populate();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue