573 lines
20 KiB
C#
573 lines
20 KiB
C#
using EFT.InventoryLogic;
|
|
using EFT.UI;
|
|
using EFT.UI.DragAndDrop;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
|
|
namespace UIFixes;
|
|
|
|
public class MultiSelect
|
|
{
|
|
private static GameObject SelectedMarkTemplate = null;
|
|
private static GameObject SelectedBackgroundTemplate = null;
|
|
|
|
private static readonly Dictionary<MultiSelectItemContext, GridItemView> SelectedItems = [];
|
|
private static readonly Dictionary<MultiSelectItemContext, GridItemView> SecondaryItems = [];
|
|
|
|
private static MultiSelectItemContextTaskSerializer LoadUnloadSerializer = null;
|
|
|
|
public static bool Enabled
|
|
{
|
|
get
|
|
{
|
|
return Settings.EnableMultiSelect.Value && (!Plugin.InRaid() || Settings.EnableMultiSelectInRaid.Value);
|
|
}
|
|
}
|
|
|
|
public static void Initialize()
|
|
{
|
|
// Grab the selection objects from ragfair as templates
|
|
RagfairNewOfferItemView ragfairNewOfferItemView = ItemViewFactory.CreateFromPool<RagfairNewOfferItemView>("ragfair_layout");
|
|
|
|
if (SelectedMarkTemplate == null)
|
|
{
|
|
SelectedMarkTemplate = UnityEngine.Object.Instantiate(ragfairNewOfferItemView.R().SelectedMark, null, false);
|
|
UnityEngine.Object.DontDestroyOnLoad(SelectedMarkTemplate);
|
|
}
|
|
|
|
if (SelectedBackgroundTemplate == null)
|
|
{
|
|
SelectedBackgroundTemplate = UnityEngine.Object.Instantiate(ragfairNewOfferItemView.R().SelectedBackground, null, false);
|
|
UnityEngine.Object.DontDestroyOnLoad(SelectedBackgroundTemplate);
|
|
}
|
|
|
|
ragfairNewOfferItemView.ReturnToPool();
|
|
}
|
|
|
|
public static void Toggle(GridItemView itemView, bool secondary = false)
|
|
{
|
|
var dictionary = secondary ? SecondaryItems : SelectedItems;
|
|
MultiSelectItemContext itemContext = dictionary.FirstOrDefault(x => x.Value == itemView).Key;
|
|
if (itemContext != null)
|
|
{
|
|
Deselect(itemContext, secondary);
|
|
}
|
|
else
|
|
{
|
|
Select(itemView, secondary);
|
|
}
|
|
}
|
|
|
|
public static void Clear()
|
|
{
|
|
// ToList() because modifying the collection
|
|
foreach (MultiSelectItemContext itemContext in SelectedItems.Keys.ToList())
|
|
{
|
|
Deselect(itemContext);
|
|
}
|
|
}
|
|
|
|
public static void Select(GridItemView itemView, bool secondary = false)
|
|
{
|
|
var dictionary = secondary ? SecondaryItems : SelectedItems;
|
|
|
|
if (itemView.IsSelectable() && !SelectedItems.Any(x => x.Key.Item == itemView.Item) && !SecondaryItems.Any(x => x.Key.Item == itemView.Item))
|
|
{
|
|
MultiSelectItemContext itemContext = new(itemView.ItemContext, itemView.ItemRotation);
|
|
|
|
// Subscribe to window closures to deselect
|
|
var windowContext = itemView.GetComponentInParent<GridWindow>()?.WindowContext ?? itemView.GetComponentInParent<InfoWindow>()?.WindowContext;
|
|
if (windowContext != null)
|
|
{
|
|
windowContext.OnClose += () => Deselect(itemContext);
|
|
}
|
|
|
|
// Thread unsafe way of ensuring we don't multiple subscribe. I'm sure it's fine.
|
|
itemContext.Item.Owner.AddItemEvent -= OnItemAdded;
|
|
itemContext.Item.Owner.AddItemEvent += OnItemAdded;
|
|
|
|
// Cache the gridview in case we need it
|
|
MultiGrid.Cache(itemView.Container as GridView);
|
|
|
|
dictionary.Add(itemContext, itemView);
|
|
ShowSelection(itemView);
|
|
}
|
|
}
|
|
|
|
public static void Deselect(MultiSelectItemContext itemContext, bool secondary = false)
|
|
{
|
|
var dictionary = secondary ? SecondaryItems : SelectedItems;
|
|
|
|
if (dictionary.TryGetValue(itemContext, out GridItemView itemView))
|
|
{
|
|
HideSelection(itemView);
|
|
}
|
|
|
|
dictionary.Remove(itemContext);
|
|
itemContext.Dispose();
|
|
}
|
|
|
|
public static void Deselect(GridItemView itemView, bool secondary = false)
|
|
{
|
|
var dictionary = secondary ? SecondaryItems : SelectedItems;
|
|
|
|
MultiSelectItemContext itemContext = dictionary.FirstOrDefault(x => x.Value == itemView).Key;
|
|
if (itemContext != null)
|
|
{
|
|
dictionary.Remove(itemContext);
|
|
itemContext.Dispose();
|
|
HideSelection(itemView);
|
|
}
|
|
}
|
|
|
|
public static void OnKillItemView(GridItemView itemView)
|
|
{
|
|
CombineSecondary();
|
|
|
|
MultiSelectItemContext itemContext = SelectedItems.FirstOrDefault(x => x.Value == itemView).Key;
|
|
if (itemContext != null)
|
|
{
|
|
SelectedItems[itemContext] = null;
|
|
HideSelection(itemView);
|
|
}
|
|
}
|
|
|
|
public static void OnNewItemView(GridItemView itemView)
|
|
{
|
|
if (!itemView.IsSelectable())
|
|
{
|
|
return;
|
|
}
|
|
|
|
CombineSecondary();
|
|
|
|
MultiSelectItemContext itemContext = SelectedItems.FirstOrDefault(x => x.Key.Item == itemView.Item).Key;
|
|
if (itemContext != null)
|
|
{
|
|
// Refresh the context. Note that the address might still be old
|
|
Deselect(itemContext);
|
|
Select(itemView);
|
|
}
|
|
}
|
|
|
|
// Occurs when an item is added somewhere. If it's from a move, and that item was multiselected,
|
|
// the context needs to be updated with the new address
|
|
private static void OnItemAdded(GEventArgs2 eventArgs)
|
|
{
|
|
if (eventArgs.Status != CommandStatus.Succeed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
CombineSecondary();
|
|
|
|
MultiSelectItemContext oldItemContext = SelectedItems.FirstOrDefault(x => x.Key.Item == eventArgs.Item).Key;
|
|
if (oldItemContext != null)
|
|
{
|
|
MultiSelectItemContext newContext = oldItemContext.Refresh();
|
|
if (newContext != null)
|
|
{
|
|
SelectedItems.Add(newContext, SelectedItems[oldItemContext]);
|
|
}
|
|
|
|
SelectedItems.Remove(oldItemContext);
|
|
oldItemContext.Dispose();
|
|
}
|
|
}
|
|
|
|
public static bool IsSelected(GridItemView itemView, bool secondary = false)
|
|
{
|
|
var dictionary = secondary ? SecondaryItems : SelectedItems;
|
|
return dictionary.Any(x => x.Key.Item == itemView.Item);
|
|
}
|
|
|
|
public static void Prune()
|
|
{
|
|
foreach (var entry in SelectedItems.ToList())
|
|
{
|
|
if (entry.Value == null)
|
|
{
|
|
Deselect(entry.Key);
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void CombineSecondary()
|
|
{
|
|
foreach (var entry in SecondaryItems)
|
|
{
|
|
SelectedItems.Add(entry.Key, entry.Value);
|
|
}
|
|
|
|
SecondaryItems.Clear();
|
|
}
|
|
|
|
public static IEnumerable<MultiSelectItemContext> ItemContexts
|
|
{
|
|
get { return SelectedItems.Keys; }
|
|
}
|
|
|
|
public static IEnumerable<MultiSelectItemContext> SecondaryContexts
|
|
{
|
|
get { return SecondaryItems.Keys; }
|
|
}
|
|
|
|
public static int Count
|
|
{
|
|
get { return SelectedItems.Count; }
|
|
}
|
|
|
|
public static int SecondaryCount
|
|
{
|
|
get { return SecondaryItems.Count; }
|
|
}
|
|
|
|
public static bool Active
|
|
{
|
|
get { return SelectedItems.Count > 0 || SecondaryItems.Count > 0; }
|
|
}
|
|
|
|
// Sort the items to prioritize the items that share a grid with the dragged item, prepend the dragContext as the first one
|
|
// Can pass no itemContext, and it just sorts items by their grid order
|
|
public static IEnumerable<MultiSelectItemContext> SortedItemContexts(DragItemContext first = null, bool prepend = true)
|
|
{
|
|
static int gridOrder(LocationInGrid loc) => 100 * loc.y + loc.x;
|
|
|
|
var result = SelectedItems.Keys
|
|
.Where(ic => first == null || ic.Item != first.Item)
|
|
.OrderByDescending(ic => ic.ItemAddress is GridItemAddress)
|
|
.ThenByDescending(ic => first != null && first.ItemAddress.Container.ParentItem == ic.ItemAddress.Container.ParentItem)
|
|
.ThenBy(ic => ic.ItemAddress is GridItemAddress selectedGridAddress ? gridOrder(MultiGrid.GetGridLocation(selectedGridAddress)) : 0);
|
|
|
|
if (first != null && prepend)
|
|
{
|
|
MultiSelectItemContext multiSelectItemContext = SelectedItems.Keys.FirstOrDefault(c => c.Item == first.Item);
|
|
if (multiSelectItemContext != null)
|
|
{
|
|
multiSelectItemContext.UpdateDragContext(first);
|
|
return result.Prepend(multiSelectItemContext);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public static void ShowDragCount(DraggedItemView draggedItemView)
|
|
{
|
|
if (draggedItemView != null && Count > 1)
|
|
{
|
|
GameObject textOverlay = new("MultiSelectText", [typeof(RectTransform), typeof(TextMeshProUGUI)]);
|
|
textOverlay.transform.SetParent(draggedItemView.transform, false);
|
|
textOverlay.transform.SetAsLastSibling();
|
|
textOverlay.SetActive(true);
|
|
|
|
RectTransform overlayRect = textOverlay.GetComponent<RectTransform>();
|
|
overlayRect.anchorMin = Vector2.zero;
|
|
overlayRect.anchorMax = Vector2.one;
|
|
overlayRect.anchoredPosition = new Vector2(0.5f, 0.5f);
|
|
|
|
TextMeshProUGUI text = textOverlay.GetComponent<TextMeshProUGUI>();
|
|
text.text = MultiSelect.Count.ToString();
|
|
text.fontSize = 36;
|
|
text.alignment = TextAlignmentOptions.Baseline;
|
|
}
|
|
}
|
|
|
|
public static int InteractionCount(EItemInfoButton interaction, ItemUiContext itemUiContext)
|
|
{
|
|
return ItemContexts.Count(ic => InteractionAvailable(ic, interaction, itemUiContext));
|
|
}
|
|
|
|
private static bool InteractionAvailable(DragItemContext itemContext, EItemInfoButton interaction, ItemUiContext itemUiContext)
|
|
{
|
|
// Since itemContext is for "drag", no context actions are allowed. Get the underlying "inventory" context
|
|
ItemContextAbstractClass innerContext = itemContext.ItemContextAbstractClass;
|
|
if (innerContext == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool createdContext = false;
|
|
if (innerContext.Item != itemContext.Item)
|
|
{
|
|
// Actual context went away and we're looking at inventory/stash context
|
|
innerContext = innerContext.CreateChild(itemContext.Item);
|
|
createdContext = true;
|
|
}
|
|
|
|
var contextInteractions = itemUiContext.GetItemContextInteractions(innerContext, null);
|
|
bool result = contextInteractions.IsInteractionAvailable(interaction);
|
|
|
|
if (createdContext)
|
|
{
|
|
innerContext.Dispose();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public static void EquipAll(ItemUiContext itemUiContext, bool allOrNothing)
|
|
{
|
|
if (!allOrNothing || InteractionCount(EItemInfoButton.Equip, itemUiContext) == Count)
|
|
{
|
|
var taskSerializer = itemUiContext.gameObject.AddComponent<MultiSelectItemContextTaskSerializer>();
|
|
taskSerializer.Initialize(
|
|
SortedItemContexts().Where(ic => InteractionAvailable(ic, EItemInfoButton.Equip, itemUiContext)),
|
|
itemContext => itemUiContext.QuickEquip(itemContext.Item));
|
|
|
|
itemUiContext.Tooltip?.Close();
|
|
}
|
|
}
|
|
|
|
public static void UnequipAll(ItemUiContext itemUiContext, bool allOrNothing)
|
|
{
|
|
if (!allOrNothing || InteractionCount(EItemInfoButton.Unequip, itemUiContext) == Count)
|
|
{
|
|
var taskSerializer = itemUiContext.gameObject.AddComponent<MultiSelectItemContextTaskSerializer>();
|
|
taskSerializer.Initialize(
|
|
SortedItemContexts().Where(ic => InteractionAvailable(ic, EItemInfoButton.Unequip, itemUiContext)),
|
|
itemContext => itemUiContext.Uninstall(itemContext.ItemContextAbstractClass));
|
|
|
|
itemUiContext.Tooltip?.Close();
|
|
}
|
|
}
|
|
|
|
public static Task LoadAmmoAll(ItemUiContext itemUiContext, string ammoTemplateId, bool allOrNothing)
|
|
{
|
|
StopLoading(true);
|
|
if (!allOrNothing || InteractionCount(EItemInfoButton.LoadAmmo, itemUiContext) == Count)
|
|
{
|
|
LoadUnloadSerializer = itemUiContext.gameObject.AddComponent<MultiSelectItemContextTaskSerializer>();
|
|
Task result = LoadUnloadSerializer.Initialize(
|
|
SortedItemContexts()
|
|
.Where(ic => ic.Item is MagazineClass && InteractionAvailable(ic, EItemInfoButton.LoadAmmo, itemUiContext))
|
|
.SelectMany(ic => ic.RepeatUntilFull()),
|
|
itemContext =>
|
|
{
|
|
IgnoreStopLoading = true;
|
|
return itemUiContext.LoadAmmoByType(itemContext.Item as MagazineClass, ammoTemplateId, itemContext.UpdateView);
|
|
});
|
|
|
|
itemUiContext.Tooltip?.Close();
|
|
|
|
return result.ContinueWith(t => LoadUnloadSerializer = null);
|
|
}
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public static void UnloadAmmoAll(ItemUiContext itemUiContext, bool allOrNothing)
|
|
{
|
|
StopLoading(true);
|
|
if (!allOrNothing || InteractionCount(EItemInfoButton.UnloadAmmo, itemUiContext) == Count)
|
|
{
|
|
LoadUnloadSerializer = itemUiContext.gameObject.AddComponent<MultiSelectItemContextTaskSerializer>();
|
|
LoadUnloadSerializer.Initialize(
|
|
SortedItemContexts().Where(ic => InteractionAvailable(ic, EItemInfoButton.UnloadAmmo, itemUiContext)),
|
|
itemContext =>
|
|
{
|
|
if (itemContext.Item is AmmoBox)
|
|
{
|
|
Deselect(itemContext);
|
|
}
|
|
|
|
IgnoreStopLoading = true;
|
|
return itemUiContext.UnloadAmmo(itemContext.Item);
|
|
}).ContinueWith(t => LoadUnloadSerializer = null);
|
|
|
|
itemUiContext.Tooltip?.Close();
|
|
}
|
|
}
|
|
|
|
private static bool IgnoreStopLoading = false;
|
|
|
|
public static void StopLoading(bool force = false)
|
|
{
|
|
if (LoadUnloadSerializer == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!IgnoreStopLoading || force)
|
|
{
|
|
LoadUnloadSerializer.Cancel();
|
|
LoadUnloadSerializer = null;
|
|
}
|
|
else
|
|
{
|
|
IgnoreStopLoading = false;
|
|
}
|
|
}
|
|
|
|
public static void UnpackAll(ItemUiContext itemUiContext, bool allOrNothing)
|
|
{
|
|
if (!allOrNothing || InteractionCount(EItemInfoButton.Unpack, itemUiContext) == Count)
|
|
{
|
|
var taskSerializer = itemUiContext.gameObject.AddComponent<MultiSelectItemContextTaskSerializer>();
|
|
taskSerializer.Initialize(
|
|
SortedItemContexts().Where(ic => InteractionAvailable(ic, EItemInfoButton.Unpack, itemUiContext)),
|
|
itemContext =>
|
|
{
|
|
Deselect(itemContext);
|
|
return itemUiContext.UnpackItem(itemContext.Item);
|
|
});
|
|
|
|
itemUiContext.Tooltip?.Close();
|
|
}
|
|
}
|
|
|
|
private static void ShowSelection(GridItemView itemView)
|
|
{
|
|
GameObject selectedMark = itemView.transform.Find("SelectedMark")?.gameObject;
|
|
if (selectedMark == null)
|
|
{
|
|
selectedMark = UnityEngine.Object.Instantiate(SelectedMarkTemplate, itemView.transform, false);
|
|
selectedMark.name = "SelectedMark";
|
|
}
|
|
|
|
selectedMark.SetActive(true);
|
|
|
|
GameObject selectedBackground = itemView.transform.Find("SelectedBackground")?.gameObject;
|
|
if (selectedBackground == null)
|
|
{
|
|
selectedBackground = UnityEngine.Object.Instantiate(SelectedBackgroundTemplate, itemView.transform, false);
|
|
selectedBackground.transform.SetAsFirstSibling();
|
|
selectedBackground.name = "SelectedBackground";
|
|
}
|
|
|
|
selectedBackground.SetActive(true);
|
|
}
|
|
|
|
private static void HideSelection(GridItemView itemView)
|
|
{
|
|
if (itemView == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
GameObject selectedMark = itemView.transform.Find("SelectedMark")?.gameObject;
|
|
GameObject selectedBackground = itemView.transform.Find("SelectedBackground")?.gameObject;
|
|
|
|
selectedMark?.SetActive(false);
|
|
selectedBackground?.SetActive(false);
|
|
}
|
|
}
|
|
|
|
public class MultiSelectItemContext : DragItemContext
|
|
{
|
|
public MultiSelectItemContext(ItemContextAbstractClass itemContext, ItemRotation rotation) : base(itemContext, rotation)
|
|
{
|
|
// Adjust event handlers
|
|
if (ItemContextAbstractClass != null)
|
|
{
|
|
// Listen for underlying context being disposed, it might mean the item is gone (merged, destroyed, etc)
|
|
ItemContextAbstractClass.OnDisposed += OnParentDispose;
|
|
// This serves no purpose and causes stack overflows
|
|
ItemContextAbstractClass.OnCloseWindow -= CloseDependentWindows;
|
|
}
|
|
}
|
|
|
|
public MultiSelectItemContext Refresh()
|
|
{
|
|
if (Item == ItemContextAbstractClass.Item)
|
|
{
|
|
return new MultiSelectItemContext(ItemContextAbstractClass, ItemRotation);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public void UpdateDragContext(DragItemContext itemContext)
|
|
{
|
|
SetPosition(itemContext.CursorPosition, itemContext.ItemPosition);
|
|
ItemRotation = itemContext.ItemRotation;
|
|
}
|
|
|
|
public override void Dispose()
|
|
{
|
|
base.Dispose();
|
|
if (ItemContextAbstractClass != null)
|
|
{
|
|
ItemContextAbstractClass.OnDisposed -= OnParentDispose;
|
|
}
|
|
}
|
|
|
|
private void OnParentDispose()
|
|
{
|
|
if (Item.CurrentAddress == null || Item.CurrentAddress.Container.ParentItem is MagazineClass)
|
|
{
|
|
// This item was entirely merged away, or went into a magazine
|
|
MultiSelect.Deselect(this);
|
|
}
|
|
}
|
|
|
|
// used by ItemUiContext.QuickFindAppropriatePlace, the one that picks a container, i.e. ctrl-click
|
|
// DragItemContext (drag) defaults to None, but we want what the underlying item allows
|
|
public override bool CanQuickMoveTo(ETargetContainer targetContainer)
|
|
{
|
|
if (ItemContextAbstractClass != null)
|
|
{
|
|
return ItemContextAbstractClass.CanQuickMoveTo(targetContainer);
|
|
}
|
|
|
|
return base.CanQuickMoveTo(targetContainer);
|
|
}
|
|
}
|
|
|
|
// Specific type of TaskSerializer because Unity can't understand generics
|
|
public class MultiSelectItemContextTaskSerializer : TaskSerializer<MultiSelectItemContext> { }
|
|
|
|
public static class MultiSelectExtensions
|
|
{
|
|
public static bool IsSelectable(this ItemView itemView)
|
|
{
|
|
// Common non-interactable stuff
|
|
if (!itemView.IsInteractable || !itemView.IsSearched || itemView.RemoveError.Value != null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Ironically, SelectableSlotItemView is not selectable. Those are for picking as a choice
|
|
if (itemView is SelectableSlotItemView)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// You can't multi-select trader's items or items being sold
|
|
if (itemView is TradingItemView tradingItemView)
|
|
{
|
|
if (itemView is not TradingPlayerItemView || tradingItemView.R().IsBeingSold)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public static IEnumerable<T> RepeatUntilEmpty<T>(this T itemContext) where T : ItemContextAbstractClass
|
|
{
|
|
while (itemContext.Item.StackObjectsCount > 0)
|
|
{
|
|
yield return itemContext;
|
|
}
|
|
}
|
|
|
|
public static IEnumerable<T> RepeatUntilFull<T>(this T itemContext) where T : ItemContextAbstractClass
|
|
{
|
|
if (itemContext.Item is MagazineClass magazine)
|
|
{
|
|
int ammoCount = -1;
|
|
while (magazine.Count > ammoCount && magazine.Count < magazine.MaxCount)
|
|
{
|
|
ammoCount = magazine.Count;
|
|
yield return itemContext;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|