fix(ui): port source-aware toolbar drops

Plan retail shortcut drops from a complete post-lift snapshot so fresh inventory items use cyclic-right displacement while toolbar aliases restore to their vacated slot. Apply local mutations atomically, preserve raw shortcut fields, emit exact Remove/Add ordering, and retire AP-102 after the live ACE gate.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-11 09:26:57 +02:00
parent b5b230c860
commit 3281e0acb4
8 changed files with 462 additions and 23 deletions

View file

@ -508,26 +508,54 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
/// <inheritdoc/>
public void HandleDropRelease(UiItemList targetList, UiItemSlot targetCell, ItemDragPayload payload)
{
// 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;
ShortcutEntry? evictedEntry = _store.GetEntry(target); // RemoveShortCutInSlotNum(target)
uint evicted = evictedEntry?.ObjectId ?? 0u;
if (evicted != 0) { _store.Remove(target); _sendRemoveShortcut?.Invoke((uint)target); }
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
ShortcutEntry displaced = evictedEntry!.Value.WithIndex(payload.SourceSlot);
_store.Set(displaced);
_sendAddShortcut?.Invoke(displaced);
}
EnsureStoreLoadedForMutation();
ShortcutDropSource source = payload.SourceKind == ItemDragSource.ShortcutBar
? ShortcutDropSource.ShortcutAlias
: ShortcutDropSource.FreshItem;
ShortcutEntry dragged = payload.Shortcut
?? new ShortcutEntry(payload.SourceSlot, payload.ObjId, 0u);
ShortcutMutation[] plan = ShortcutDropPlanner.PlanDrop(
_store.Snapshot(), source, payload.SourceSlot, targetCell.SlotIndex, dragged);
ApplyShortcutPlan(plan);
Populate();
}
/// <summary>
/// Toolbar notice port for <c>gmToolbarUI::RecvNotice_FullMergingItem @ 0x004BE9B0</c>.
/// The owning inventory pipeline calls this when ACE confirms a source stack was fully merged.
/// </summary>
public void ReplaceFullyMergedShortcut(uint oldObjectId, uint newObjectId)
{
EnsureStoreLoadedForMutation();
ShortcutMutation[] plan = ShortcutDropPlanner.PlanFullStackMerge(
_store.Snapshot(), oldObjectId, newObjectId);
ApplyShortcutPlan(plan);
if (plan.Length > 0)
Populate();
}
private void ApplyShortcutPlan(IReadOnlyList<ShortcutMutation> plan)
{
// Apply the already-validated transaction locally first, then reproduce the
// planner's exact Remove/Add wire order. Shortcut events have no rejection reply.
foreach (var mutation in plan)
{
if (mutation.Kind == ShortcutMutationKind.Remove)
_store.Remove(mutation.Slot);
else if (mutation.Entry is { } entry)
_store.Set(entry);
}
foreach (var mutation in plan)
{
if (mutation.Kind == ShortcutMutationKind.Remove)
_sendRemoveShortcut?.Invoke((uint)mutation.Slot);
else if (mutation.Entry is { } entry)
_sendAddShortcut?.Invoke(entry);
}
}
public void Dispose()
{
if (_disposed) return;