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; } }