diff --git a/MultiSelect.cs b/MultiSelect.cs index 31c8d6e..b72c596 100644 --- a/MultiSelect.cs +++ b/MultiSelect.cs @@ -238,7 +238,7 @@ namespace UIFixes { if (!allOrNothing || InteractionCount(EItemInfoButton.Equip, itemUiContext) == Count) { - var taskSerializer = itemUiContext.GetOrAddComponent(); + var taskSerializer = itemUiContext.gameObject.AddComponent(); taskSerializer.Initialize(SortedItemContexts(), itemContext => itemUiContext.QuickEquip(itemContext.Item)); itemUiContext.Tooltip?.Close(); } @@ -248,7 +248,7 @@ namespace UIFixes { if (!allOrNothing || InteractionCount(EItemInfoButton.Unequip, itemUiContext) == Count) { - var taskSerializer = itemUiContext.GetOrAddComponent(); + var taskSerializer = itemUiContext.gameObject.AddComponent(); taskSerializer.Initialize(SortedItemContexts(), itemContext => itemUiContext.Uninstall(itemContext.GClass2813_0)); itemUiContext.Tooltip?.Close(); } @@ -260,7 +260,7 @@ namespace UIFixes if (!allOrNothing || InteractionCount(EItemInfoButton.UnloadAmmo, itemUiContext) == Count) { // Call Initialize() before setting UnloadSerializer so that the initial synchronous call to StopProcesses()->StopUnloading() doesn't immediately cancel this - var taskSerializer = itemUiContext.GetOrAddComponent(); + var taskSerializer = itemUiContext.gameObject.AddComponent(); taskSerializer.Initialize(SortedItemContexts(), itemContext => itemUiContext.UnloadAmmo(itemContext.Item)); UnloadSerializer = taskSerializer; @@ -391,6 +391,15 @@ namespace UIFixes return true; } + + public static IEnumerable RepeatUntilEmpty(this ItemContextClass itemContext) + { + while (itemContext.Item.StackObjectsCount > 0) + { + yield return itemContext; + } + } + } } diff --git a/Patches/MultiSelectPatches.cs b/Patches/MultiSelectPatches.cs index cd1d5a7..6dae2c3 100644 --- a/Patches/MultiSelectPatches.cs +++ b/Patches/MultiSelectPatches.cs @@ -11,6 +11,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Threading; using System.Threading.Tasks; using UnityEngine; using UnityEngine.EventSystems; @@ -103,7 +104,7 @@ namespace UIFixes __instance.TransferItemsScreen.GetOrAddComponent(); __instance.ScavengerInventoryScreen.GetOrAddComponent(); - void ToggleDebug() + static void ToggleDebug() { if (Settings.ShowMultiSelectDebug.Value) { @@ -475,55 +476,97 @@ namespace UIFixes Item targetItem = __instance.method_8(targetItemContext); DisableMerge = targetItem == null; - bool showHighlights = targetItem == null; + bool isGridPlacement = targetItem == null; Stack operations = new(); foreach (ItemContextClass selectedItemContext in MultiSelect.SortedItemContexts(itemContext)) { - FindOrigin = GetTargetGridAddress(itemContext, selectedItemContext, hoveredAddress); - FindVerticalFirst = selectedItemContext.ItemRotation == ItemRotation.Vertical; - - if (targetItem is SortingTableClass) + if (Settings.GreedyStackMove.Value && !isGridPlacement && selectedItemContext.Item.StackObjectsCount > 1) { - operation = ___itemUiContext_0.QuickMoveToSortingTable(selectedItemContext.Item, false /* simulate */); - } - else - { - operation = targetItem != null ? - wrappedInstance.TraderController.ExecutePossibleAction(selectedItemContext, targetItem, false /* splitting */, false /* simulate */) : - wrappedInstance.TraderController.ExecutePossibleAction(selectedItemContext, __instance.SourceContext, hoveredAddress, false /* splitting */, false /* simulate */); - } - - FindOrigin = null; - FindVerticalFirst = false; - - if (__result = operation.Succeeded) - { - operations.Push(operation); - if (targetItem != null && showHighlights) // targetItem was originally null so this is the rest of the items + int stackCount = int.MaxValue; + bool failed = false; + while (selectedItemContext.Item.StackObjectsCount > 0) { - ShowPreview(__instance, selectedItemContext, operation); + if (selectedItemContext.Item.StackObjectsCount >= stackCount) + { + break; + } + + stackCount = selectedItemContext.Item.StackObjectsCount; + operation = wrappedInstance.TraderController.ExecutePossibleAction(selectedItemContext, targetItem, false /* splitting */, false /* simulate */); + if (__result = operation.Succeeded) + { + operations.Push(operation); + } + else + { + if (operation.Error is GClass3292 noRoomError) + { + // Wrap this error to display it + operation = new(new DisplayableErrorWrapper(noRoomError)); + } + + // Need to double-break + failed = true; + break; + } } - } - else if (operation.Error is InteractionsHandlerClass.GClass3329) - { - // Moving item to the same place, cool, not a problem - __result = true; - operation = default; - if (showHighlights && selectedItemContext.Item.Parent is GClass2769 gridAddress) + + if (failed) { - ShowPreview(__instance, selectedItemContext, gridAddress, R.GridView.ValidMoveColor); + break; } } else { - if (operation.Error is GClass3292 noRoomError) + if (isGridPlacement) { - // Wrap this error to display it - operation = new(new DisplayableErrorWrapper(noRoomError)); + FindOrigin = GetTargetGridAddress(itemContext, selectedItemContext, hoveredAddress); + FindVerticalFirst = selectedItemContext.ItemRotation == ItemRotation.Vertical; } - break; + if (targetItem is SortingTableClass) + { + operation = ___itemUiContext_0.QuickMoveToSortingTable(selectedItemContext.Item, false /* simulate */); + } + else + { + operation = targetItem != null ? + wrappedInstance.TraderController.ExecutePossibleAction(selectedItemContext, targetItem, false /* splitting */, false /* simulate */) : + wrappedInstance.TraderController.ExecutePossibleAction(selectedItemContext, __instance.SourceContext, hoveredAddress, false /* splitting */, false /* simulate */); + } + + FindOrigin = null; + FindVerticalFirst = false; + + if (__result = operation.Succeeded) + { + operations.Push(operation); + if (targetItem != null && isGridPlacement) // targetItem was originally null so this is the rest of the items + { + ShowPreview(__instance, selectedItemContext, operation); + } + } + else if (operation.Error is InteractionsHandlerClass.GClass3329) + { + // Moving item to the same place, cool, not a problem + __result = true; + operation = default; + if (isGridPlacement && selectedItemContext.Item.Parent is GClass2769 gridAddress) + { + ShowPreview(__instance, selectedItemContext, gridAddress, R.GridView.ValidMoveColor); + } + } + else + { + if (operation.Error is GClass3292 noRoomError) + { + // Wrap this error to display it + operation = new(new DisplayableErrorWrapper(noRoomError)); + } + + break; + } } // Set this after the first one @@ -603,7 +646,7 @@ namespace UIFixes targetItemContext = new GClass2817(__instance.Grid.ParentItem, EItemViewType.Empty); } - var serializer = __instance.GetOrAddComponent(); + var serializer = __instance.gameObject.AddComponent(); __result = serializer.Initialize(MultiSelect.SortedItemContexts(itemContext), ic => { FindOrigin = GetTargetGridAddress(itemContext, ic, hoveredAddress); @@ -665,7 +708,7 @@ namespace UIFixes // Multiselect always disables "Transfer", which is a partial merge // It leaves things behind and that's not intuitive when multi-selecting - order &= ~PartialMerge; + // order &= ~PartialMerge; if (DisableMerge) { @@ -774,19 +817,53 @@ namespace UIFixes Stack operations = new(); foreach (ItemContextClass itemContext in MultiSelect.SortedItemContexts()) { - __result = itemContext.CanAccept(__instance.Slot, __instance.ParentItemContext, ___InventoryController, out operation, false /* simulate */); - if (operation.Succeeded) + if (!Settings.GreedyStackMove.Value || itemContext.Item.StackObjectsCount <= 1) { - operations.Push(operation); - } - else if (operation.Error is InteractionsHandlerClass.GClass3329) - { - // Moving item to the same place, cool, not a problem - __result = true; + __result = itemContext.CanAccept(__instance.Slot, __instance.ParentItemContext, ___InventoryController, out operation, false /* simulate */); + if (operation.Succeeded) + { + operations.Push(operation); + } + else if (operation.Error is InteractionsHandlerClass.GClass3329) + { + // Moving item to the same place, cool, not a problem + __result = true; + } + else + { + break; + } } else { - break; + int stackCount = int.MaxValue; + bool failed = false; + while (itemContext.Item.StackObjectsCount > 0) + { + if (itemContext.Item.StackObjectsCount >= stackCount) + { + // The whole stack moved or nothing happened, it's done + break; + } + + stackCount = itemContext.Item.StackObjectsCount; + __result = itemContext.CanAccept(__instance.Slot, __instance.ParentItemContext, ___InventoryController, out operation, false /* simulate */); + if (operation.Succeeded) + { + operations.Push(operation); + } + else + { + // Need to double-break + failed = true; + break; + } + } + + if (failed) + { + break; + } } } @@ -818,7 +895,7 @@ namespace UIFixes InPatch = true; - var serializer = __instance.GetOrAddComponent(); + var serializer = __instance.gameObject.AddComponent(); __result = serializer.Initialize(MultiSelect.SortedItemContexts(), itemContext => __instance.AcceptItem(itemContext, targetItemContext)); __result.ContinueWith(_ => { InPatch = false; }); diff --git a/Patches/StackMoveGreedyPatches.cs b/Patches/StackMoveGreedyPatches.cs new file mode 100644 index 0000000..67339cb --- /dev/null +++ b/Patches/StackMoveGreedyPatches.cs @@ -0,0 +1,87 @@ +using Aki.Reflection.Patching; +using EFT.UI.DragAndDrop; +using HarmonyLib; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using UnityEngine; + +namespace UIFixes +{ + public static class StackMoveGreedyPatches + { + private static bool InPatch = false; + + public static void Enable() + { + new GridViewPatch().Enable(); + new SlotViewPatch().Enable(); + } + + public class GridViewPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(GridView), nameof(GridView.AcceptItem)); + } + + [PatchPrefix] + [HarmonyPriority(Priority.LowerThanNormal)] + public static bool Prefix(GridView __instance, ItemContextClass itemContext, ItemContextAbstractClass targetItemContext, ref Task __result) + { + return AcceptStackable(__instance, itemContext, targetItemContext, ref __result); + } + } + + public class SlotViewPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(SlotView), nameof(SlotView.AcceptItem)); + } + + [PatchPrefix] + [HarmonyPriority(Priority.LowerThanNormal)] + public static bool Prefix(SlotView __instance, ItemContextClass itemContext, ItemContextAbstractClass targetItemContext, ref Task __result) + { + return AcceptStackable(__instance, itemContext, targetItemContext, ref __result); + } + } + + private static bool AcceptStackable(T __instance, ItemContextClass itemContext, ItemContextAbstractClass targetItemContext, ref Task __result) where T : MonoBehaviour, IContainer + { + if (!Settings.GreedyStackMove.Value || InPatch || itemContext.Item.StackObjectsCount <= 1 || targetItemContext == null) + { + return true; + } + + InPatch = true; + + int stackCount = int.MaxValue; + var serializer = __instance.gameObject.AddComponent(); + __result = serializer.Initialize(itemContext.RepeatUntilEmpty(), ic => + { + if (ic.Item.StackObjectsCount >= stackCount) + { + // Nothing happened, bail out + return Task.FromCanceled(new CancellationToken(true)); + } + + stackCount = ic.Item.StackObjectsCount; + return __instance.AcceptItem(ic, targetItemContext); + }); + + // This won't block the first action from swapping, but will prevent follow up swaps + SwapPatches.BlockSwaps = true; + + __result.ContinueWith(_ => + { + InPatch = false; + SwapPatches.BlockSwaps = false; + }); + + return false; + } + } +} diff --git a/Patches/SwapPatches.cs b/Patches/SwapPatches.cs index d99a959..96aa0c2 100644 --- a/Patches/SwapPatches.cs +++ b/Patches/SwapPatches.cs @@ -30,6 +30,8 @@ namespace UIFixes private static readonly EOwnerType[] BannedOwnerTypes = [EOwnerType.Mail, EOwnerType.Trader]; + public static bool BlockSwaps = false; + public static void Enable() { new DetectSwapSourceContainerPatch().Enable(); @@ -57,6 +59,11 @@ namespace UIFixes return false; } + if (BlockSwaps) + { + return false; + } + var wrappedOperation = new R.GridViewCanAcceptOperation(operation); if (InHighlight || itemContext == null || targetItemContext == null || wrappedOperation.Succeeded) diff --git a/Plugin.cs b/Plugin.cs index cad51b4..505505f 100644 --- a/Plugin.cs +++ b/Plugin.cs @@ -51,6 +51,7 @@ namespace UIFixes LoadAmmoInRaidPatches.Enable(); MultiSelectPatches.Enable(); new FixUnloadLastBulletPatch().Enable(); + StackMoveGreedyPatches.Enable(); } public static bool InRaid() diff --git a/Settings.cs b/Settings.cs index 1883654..d1c9421 100644 --- a/Settings.cs +++ b/Settings.cs @@ -72,6 +72,7 @@ namespace UIFixes public static ConfigEntry SwapItems { get; set; } public static ConfigEntry SwapImpossibleContainers { get; set; } public static ConfigEntry SynchronizeStashScrolling { get; set; } + public static ConfigEntry GreedyStackMove { get; set; } public static ConfigEntry MergeFIRMoney { get; set; } public static ConfigEntry MergeFIRAmmo { get; set; } public static ConfigEntry MergeFIROther { get; set; } @@ -359,6 +360,15 @@ namespace UIFixes null, new ConfigurationManagerAttributes { }))); + configEntries.Add(GreedyStackMove = config.Bind( + InventorySection, + "Always Move Entire Stacks", + false, + new ConfigDescription( + "When moving into a container that contains a partial stack, this will top up that stack and try to move the remainder into an open spot (or another stack), instead of leaving it behind.", + null, + new ConfigurationManagerAttributes { }))); + configEntries.Add(MergeFIRMoney = config.Bind( InventorySection, "Autostack Money with FiR Money", diff --git a/TaskSerializer.cs b/TaskSerializer.cs index 0e7b07e..01476e9 100644 --- a/TaskSerializer.cs +++ b/TaskSerializer.cs @@ -9,13 +9,13 @@ namespace UIFixes public class TaskSerializer : MonoBehaviour { private Func func; - private Queue items; + private IEnumerator enumerator; private Task currentTask; private TaskCompletionSource totalTask; public Task Initialize(IEnumerable items, Func func) { - this.items = new(items); + this.enumerator = items.GetEnumerator(); this.func = func; currentTask = Task.CompletedTask; @@ -44,9 +44,9 @@ namespace UIFixes return; } - if (items.Any()) + if (enumerator.MoveNext()) { - currentTask = func(items.Dequeue()); + currentTask = func(enumerator.Current); } else {