Give retained buttons a reusable item-drop seam, wire the toolbar backpack target, and port retail stack-merge legality, capacity clamping, wire dispatch, destination selection, and immediate shortcut rekey notice. Record the live ACE merge gate and keep split quantity under AP-101. Co-Authored-By: Codex <codex@openai.com>
187 lines
6.4 KiB
C#
187 lines
6.4 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
|
|
namespace AcDream.Core.Items;
|
|
|
|
/// <summary>Retail source branch selected by <c>DropItemFlags</c>.</summary>
|
|
public enum ShortcutDropSource
|
|
{
|
|
FreshItem,
|
|
ShortcutAlias,
|
|
}
|
|
|
|
public enum ShortcutMutationKind
|
|
{
|
|
Remove,
|
|
Add,
|
|
}
|
|
|
|
/// <summary>One ordered local-and-wire mutation in a retail shortcut transaction.</summary>
|
|
public readonly record struct ShortcutMutation(
|
|
ShortcutMutationKind Kind,
|
|
int Slot,
|
|
ShortcutEntry? Entry)
|
|
{
|
|
public static ShortcutMutation Remove(int slot) => new(ShortcutMutationKind.Remove, slot, null);
|
|
|
|
public static ShortcutMutation Add(ShortcutEntry entry)
|
|
=> new(ShortcutMutationKind.Add, entry.Index, entry);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pure port of the shortcut-slot branches in
|
|
/// <c>gmToolbarUI::HandleDropRelease @ 0x004BE7C0</c>. The complete ordered
|
|
/// mutation list is computed before the caller changes client state or emits wire events.
|
|
/// </summary>
|
|
public static class ShortcutDropPlanner
|
|
{
|
|
/// <summary>
|
|
/// Plan a toolbar-slot drop against the post-lift slot snapshot. A shortcut alias has
|
|
/// already been removed from <paramref name="slots"/> by
|
|
/// <c>RecvNotice_ItemListBeginDrag @ 0x004BD930</c>; a fresh item has not.
|
|
/// </summary>
|
|
public static ShortcutMutation[] PlanDrop(
|
|
IReadOnlyList<ShortcutEntry?> slots,
|
|
ShortcutDropSource source,
|
|
int sourceSlot,
|
|
int targetSlot,
|
|
ShortcutEntry dragged)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(slots);
|
|
if (slots.Count != ShortcutStore.SlotCount
|
|
|| (uint)targetSlot >= ShortcutStore.SlotCount
|
|
|| dragged.ObjectId == 0)
|
|
return Array.Empty<ShortcutMutation>();
|
|
|
|
var state = new ShortcutEntry?[ShortcutStore.SlotCount];
|
|
for (int i = 0; i < state.Length; i++)
|
|
state[i] = slots[i];
|
|
|
|
var mutations = new List<ShortcutMutation>(4);
|
|
ShortcutEntry? displaced = VisibleEntry(state[targetSlot]);
|
|
RemoveVisibleAt(state, targetSlot, mutations);
|
|
|
|
if (source == ShortcutDropSource.FreshItem)
|
|
{
|
|
// CreateShortcutToItem(item, target, pickup=true) removes any prior
|
|
// occurrence of the item before adding the new target entry.
|
|
RemoveFirstVisibleObject(state, dragged.ObjectId, mutations);
|
|
Add(state, dragged.WithIndex(targetSlot), mutations);
|
|
|
|
if (displaced is { } entry && entry.ObjectId != dragged.ObjectId)
|
|
{
|
|
int empty = FirstEmptyToRight(state, targetSlot);
|
|
if (empty >= 0)
|
|
Add(state, entry.WithIndex(empty), mutations);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Add(state, dragged.WithIndex(targetSlot), mutations);
|
|
if (displaced is { } entry
|
|
&& entry.ObjectId != dragged.ObjectId
|
|
&& (uint)sourceSlot < ShortcutStore.SlotCount
|
|
&& IsVisuallyEmpty(state[sourceSlot]))
|
|
{
|
|
Add(state, entry.WithIndex(sourceSlot), mutations);
|
|
}
|
|
}
|
|
|
|
return mutations.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Plan the toolbar side of <c>RecvNotice_FullMergingItem @ 0x004BE9B0</c>.
|
|
/// <c>CreateShortcutToItem</c> removes an existing destination shortcut before
|
|
/// placing it into the source shortcut's former slot.
|
|
/// </summary>
|
|
public static ShortcutMutation[] PlanFullStackMerge(
|
|
IReadOnlyList<ShortcutEntry?> slots,
|
|
uint oldObjectId,
|
|
uint newObjectId)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(slots);
|
|
if (slots.Count != ShortcutStore.SlotCount || oldObjectId == 0 || newObjectId == 0)
|
|
return Array.Empty<ShortcutMutation>();
|
|
|
|
var state = new ShortcutEntry?[ShortcutStore.SlotCount];
|
|
for (int slot = 0; slot < slots.Count; slot++)
|
|
{
|
|
state[slot] = slots[slot];
|
|
}
|
|
|
|
var mutations = new List<ShortcutMutation>(3);
|
|
int sourceSlot = FindFirstVisibleObject(state, oldObjectId);
|
|
if (sourceSlot < 0)
|
|
return Array.Empty<ShortcutMutation>();
|
|
|
|
ShortcutEntry sourceEntry = state[sourceSlot]!.Value;
|
|
RemoveVisibleAt(state, sourceSlot, mutations);
|
|
RemoveFirstVisibleObject(state, newObjectId, mutations);
|
|
Add(state, sourceEntry with { ObjectId = newObjectId }, mutations);
|
|
return mutations.ToArray();
|
|
}
|
|
|
|
private static ShortcutEntry? VisibleEntry(ShortcutEntry? entry)
|
|
=> entry is { ObjectId: not 0 } ? entry : null;
|
|
|
|
private static bool IsVisuallyEmpty(ShortcutEntry? entry) => VisibleEntry(entry) is null;
|
|
|
|
private static void RemoveVisibleAt(
|
|
ShortcutEntry?[] state,
|
|
int slot,
|
|
ICollection<ShortcutMutation> mutations)
|
|
{
|
|
if (!IsVisuallyEmpty(state[slot]))
|
|
{
|
|
state[slot] = null;
|
|
mutations.Add(ShortcutMutation.Remove(slot));
|
|
}
|
|
}
|
|
|
|
private static void RemoveFirstVisibleObject(
|
|
ShortcutEntry?[] state,
|
|
uint objectId,
|
|
ICollection<ShortcutMutation> mutations)
|
|
{
|
|
for (int slot = 0; slot < state.Length; slot++)
|
|
{
|
|
if (VisibleEntry(state[slot]) is not { } entry || entry.ObjectId != objectId)
|
|
continue;
|
|
|
|
state[slot] = null;
|
|
mutations.Add(ShortcutMutation.Remove(slot));
|
|
return;
|
|
}
|
|
}
|
|
|
|
private static int FindFirstVisibleObject(ShortcutEntry?[] state, uint objectId)
|
|
{
|
|
for (int slot = 0; slot < state.Length; slot++)
|
|
if (VisibleEntry(state[slot]) is { } entry && entry.ObjectId == objectId)
|
|
return slot;
|
|
return -1;
|
|
}
|
|
|
|
private static void Add(
|
|
ShortcutEntry?[] state,
|
|
ShortcutEntry entry,
|
|
ICollection<ShortcutMutation> mutations)
|
|
{
|
|
state[entry.Index] = entry;
|
|
mutations.Add(ShortcutMutation.Add(entry));
|
|
}
|
|
|
|
private static int FirstEmptyToRight(ShortcutEntry?[] state, int targetSlot)
|
|
{
|
|
// gmToolbarUI::GetFirstEmptyShortcutToTheRightOf @ 0x004BD560:
|
|
// target+1..17, then 0..target, using visual item-list occupancy.
|
|
for (int slot = targetSlot + 1; slot < state.Length; slot++)
|
|
if (IsVisuallyEmpty(state[slot]))
|
|
return slot;
|
|
for (int slot = 0; slot <= targetSlot; slot++)
|
|
if (IsVisuallyEmpty(state[slot]))
|
|
return slot;
|
|
return -1;
|
|
}
|
|
}
|