using System;
using System.Collections.Generic;
namespace AcDream.Core.Items;
/// Retail source branch selected by DropItemFlags.
public enum ShortcutDropSource
{
FreshItem,
ShortcutAlias,
}
public enum ShortcutMutationKind
{
Remove,
Add,
}
/// One ordered local-and-wire mutation in a retail shortcut transaction.
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);
}
///
/// Pure port of the shortcut-slot branches in
/// gmToolbarUI::HandleDropRelease @ 0x004BE7C0. The complete ordered
/// mutation list is computed before the caller changes client state or emits wire events.
///
public static class ShortcutDropPlanner
{
///
/// Plan a toolbar-slot drop against the post-lift slot snapshot. A shortcut alias has
/// already been removed from by
/// RecvNotice_ItemListBeginDrag @ 0x004BD930; a fresh item has not.
///
public static ShortcutMutation[] PlanDrop(
IReadOnlyList 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();
var state = new ShortcutEntry?[ShortcutStore.SlotCount];
for (int i = 0; i < state.Length; i++)
state[i] = slots[i];
var mutations = new List(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();
}
///
/// Plan the toolbar side of RecvNotice_FullMergingItem @ 0x004BE9B0.
/// CreateShortcutToItem removes an existing destination shortcut before
/// placing it into the source shortcut's former slot.
///
public static ShortcutMutation[] PlanFullStackMerge(
IReadOnlyList slots,
uint oldObjectId,
uint newObjectId)
{
ArgumentNullException.ThrowIfNull(slots);
if (slots.Count != ShortcutStore.SlotCount || oldObjectId == 0 || newObjectId == 0)
return Array.Empty();
var state = new ShortcutEntry?[ShortcutStore.SlotCount];
for (int slot = 0; slot < slots.Count; slot++)
{
state[slot] = slots[slot];
}
var mutations = new List(3);
int sourceSlot = FindFirstVisibleObject(state, oldObjectId);
if (sourceSlot < 0)
return Array.Empty();
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 mutations)
{
if (!IsVisuallyEmpty(state[slot]))
{
state[slot] = null;
mutations.Add(ShortcutMutation.Remove(slot));
}
}
private static void RemoveFirstVisibleObject(
ShortcutEntry?[] state,
uint objectId,
ICollection 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 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;
}
}