From 0bf508df1f324d736353e2a5c848e6f4b44fc007 Mon Sep 17 00:00:00 2001 From: Tyfon <29051038+tyfon7@users.noreply.github.com> Date: Fri, 14 Jun 2024 14:08:08 -0700 Subject: [PATCH] Initial multiselect checkpoint, still long way to go --- DrawMultiSelect.cs | 87 ++++++++++ MultiSelect.cs | 172 +++++++++++++++++++ Patches/MultiSelectPatches.cs | 313 ++++++++++++++++++++++++++++++++++ Plugin.cs | 1 + R.cs | 19 +++ UIFixes.csproj | 3 + 6 files changed, 595 insertions(+) create mode 100644 DrawMultiSelect.cs create mode 100644 MultiSelect.cs create mode 100644 Patches/MultiSelectPatches.cs diff --git a/DrawMultiSelect.cs b/DrawMultiSelect.cs new file mode 100644 index 0000000..5ebe8d5 --- /dev/null +++ b/DrawMultiSelect.cs @@ -0,0 +1,87 @@ +using EFT.UI; +using EFT.UI.DragAndDrop; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace UIFixes +{ + public class DrawMultiSelect : MonoBehaviour + { + Texture2D selectTexture; + + Vector3 selectOrigin; + Vector3 selectEnd; + + bool drawing; + + public void Start() + { + selectTexture = new Texture2D(1, 1); + selectTexture.SetPixel(0, 0, new Color(1f, 1f, 1f, 0.8f)); + selectTexture.Apply(); + } + + public void Update() + { + if (Input.GetKeyDown(KeyCode.Mouse0) && ItemUiContext.Instance.R().ItemContext == null) + { + selectOrigin = Input.mousePosition; + drawing = true; + } + + if (drawing) + { + selectEnd = Input.mousePosition; + + Rect selectRect = new(selectOrigin.x, selectOrigin.y, selectEnd.x - selectOrigin.x, selectEnd.y - selectOrigin.y); + foreach (GridItemView gridItemView in GetComponentsInChildren()) + { + RectTransform itemTransform = gridItemView.GetComponent(); + Rect screenRect = new((Vector2)itemTransform.position + itemTransform.rect.position, itemTransform.rect.size); + + if (selectRect.Overlaps(screenRect, true)) + { + MultiSelect.Select(gridItemView); + } + else + { + MultiSelect.Deselect(gridItemView); + } + } + } + + if (drawing && !Input.GetKey(KeyCode.Mouse0)) + { + drawing = false; + } + } + + public void OnGUI() + { + if (drawing) + { + // Invert Y because GUI has upper-left origin + Rect area = new(selectOrigin.x, Screen.height - selectOrigin.y, selectEnd.x - selectOrigin.x, selectOrigin.y - selectEnd.y); + + Rect lineArea = area; + lineArea.height = 1; // Top + GUI.DrawTexture(lineArea, selectTexture); + + lineArea.y = area.yMax - 1; // Bottom + GUI.DrawTexture(lineArea, selectTexture); + + lineArea = area; + lineArea.width = 1; // Left + GUI.DrawTexture(lineArea, selectTexture); + + lineArea.x = area.xMax - 1; // Right + GUI.DrawTexture(lineArea, selectTexture); + } + } + + } +} diff --git a/MultiSelect.cs b/MultiSelect.cs new file mode 100644 index 0000000..b85b490 --- /dev/null +++ b/MultiSelect.cs @@ -0,0 +1,172 @@ +using EFT.InventoryLogic; +using EFT.UI.DragAndDrop; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TMPro; +using UnityEngine; + +namespace UIFixes +{ + public class MultiSelect + { + private static GameObject SelectedMarkTemplate; + private static GameObject SelectedBackgroundTemplate; + + private static readonly HashSet SelectedItemViews = []; + private static readonly List SelectedItemContexts = []; + + public static void Initialize() + { + // Grab the selection objects from ragfair as templates + RagfairNewOfferItemView ragfairNewOfferItemView = ItemViewFactory.CreateFromPool("ragfair_layout"); + SelectedMarkTemplate = UnityEngine.Object.Instantiate(ragfairNewOfferItemView.R().SelectedMark, null, false); + SelectedBackgroundTemplate = UnityEngine.Object.Instantiate(ragfairNewOfferItemView.R().SelectedBackground, null, false); + ragfairNewOfferItemView.ReturnToPool(); + } + + public static void Toggle(GridItemView itemView) + { + if (!itemView.IsInteractable) + { + return; + } + + if (SelectedItemViews.Contains(itemView)) + { + Deselect(itemView); + } + else + { + Select(itemView); + } + } + + public static void Clear() + { + // ToList() because we'll be modifying the collection + foreach (GridItemView itemView in SelectedItemViews.ToList()) + { + Deselect(itemView); + } + } + + public static void Select(GridItemView itemView) + { + if (itemView.IsInteractable && SelectedItemViews.Add(itemView)) + { + ShowSelection(itemView); + } + } + + public static void Deselect(GridItemView itemView) + { + if (SelectedItemViews.Remove(itemView)) + { + HideSelection(itemView); + } + } + + public static IEnumerable ItemViews + { + get { return SelectedItemViews; } + } + + public static IEnumerable ItemContexts + { + get { return SelectedItemContexts; } + } + + public static int Count + { + get { return SelectedItemViews.Count; } + } + + public static bool Contains(GridItemView itemView) + { + return SelectedItemViews.Contains(itemView); + } + + public static bool Active + { + get { return SelectedItemViews.Any(); } + } + + public static bool IsSelected(GridItemView itemView) + { + return SelectedItemViews.Contains(itemView); + } + + public static void BeginDrag() + { + foreach (ItemView itemView in SelectedItemViews) + { + SelectedItemContexts.Add(new ItemContextClass(itemView.ItemContext, itemView.ItemRotation)); + } + } + + public static void EndDrag() + { + foreach(ItemContextClass itemContext in SelectedItemContexts) + { + itemContext.Dispose(); + } + + SelectedItemContexts.Clear(); + } + + public static void ShowDragCount(DraggedItemView draggedItemView) + { + if (Count > 1) + { + GameObject textOverlay = new("MultiSelectText", [typeof(RectTransform), typeof(TextMeshProUGUI)]); + textOverlay.transform.parent = draggedItemView.transform; + textOverlay.transform.SetAsLastSibling(); + textOverlay.SetActive(true); + + RectTransform overlayRect = textOverlay.GetComponent(); + overlayRect.anchorMin = Vector2.zero; + overlayRect.anchorMax = Vector2.one; + overlayRect.anchoredPosition = new Vector2(0.5f, 0.5f); + + TextMeshProUGUI text = textOverlay.GetComponent(); + text.text = MultiSelect.Count.ToString(); + text.fontSize = 36; + text.alignment = TextAlignmentOptions.Baseline; + } + } + + 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) + { + GameObject selectedMark = itemView.transform.Find("SelectedMark")?.gameObject; + GameObject selectedBackground = itemView.transform.Find("SelectedBackground")?.gameObject; + + selectedMark?.SetActive(false); + selectedBackground?.SetActive(false); + } + } +} diff --git a/Patches/MultiSelectPatches.cs b/Patches/MultiSelectPatches.cs new file mode 100644 index 0000000..b230d82 --- /dev/null +++ b/Patches/MultiSelectPatches.cs @@ -0,0 +1,313 @@ +using Aki.Reflection.Patching; +using Comfort.Common; +using EFT.UI; +using EFT.UI.DragAndDrop; +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using TMPro; +using UnityEngine; +using UnityEngine.EventSystems; + +namespace UIFixes +{ + public static class MultiSelectPatches + { + // Used to prevent infinite recursion of CanAccept/AcceptItem + private static bool InPatch = false; + + public static void Enable() + { + new InitializePatch().Enable(); + new SelectPatch().Enable(); + new DeselectOnOtherMouseDown().Enable(); + new DeselectOnMovePatch().Enable(); + new BeginDragPatch().Enable(); + new EndDragPatch().Enable(); + + new GridViewCanAcceptPatch().Enable(); + new SlotViewCanAcceptPatch().Enable(); + new SlotViewAcceptItemPatch().Enable(); + } + + public class InitializePatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(CommonUI), nameof(CommonUI.Awake)); + } + + [PatchPostfix] + public static void Postfix(CommonUI __instance) + { + MultiSelect.Initialize(); + + __instance.InventoryScreen.GetOrAddComponent(); + //__instance.TransferItemsInRaidScreen.GetOrAddComponent(); + //__instance.TransferItemsScreen.GetOrAddComponent(); + } + } + + public class SelectPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(GridItemView), nameof(GridItemView.OnClick)); + } + + [PatchPostfix] + public static void Postfix(GridItemView __instance, PointerEventData.InputButton button) + { + if (button == PointerEventData.InputButton.Left && (Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift))) + { + MultiSelect.Toggle(__instance); + return; + } + + if (button == PointerEventData.InputButton.Left)// && !MultiSelect.IsSelected(__instance)) + { + MultiSelect.Clear(); + } + } + } + + public class DeselectOnOtherMouseDown : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(ItemView), nameof(ItemView.OnPointerDown)); + } + + [PatchPostfix] + public static void Postfix(ItemView __instance, PointerEventData eventData) + { + if (eventData.button == PointerEventData.InputButton.Left && (Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift))) + { + // This will be shift-click, let it cook + return; + } + + if (__instance is not GridItemView gridItemView || !MultiSelect.IsSelected(gridItemView)) + { + MultiSelect.Clear(); + } + } + } + + public class DeselectOnMovePatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(ItemView), nameof(ItemView.Kill)); + } + + [PatchPostfix] + public static void Postfix(ItemView __instance) + { + if (__instance is GridItemView gridItemView) + { + MultiSelect.Deselect(gridItemView); + } + } + } + + public class BeginDragPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(ItemView), nameof(ItemView.OnBeginDrag)); + } + + [PatchPrefix] + public static void Prefix() + { + MultiSelect.BeginDrag(); + } + + [PatchPostfix] + public static void Postfix(ItemView __instance) + { + MultiSelect.ShowDragCount(__instance.DraggedItemView); + } + } + + public class EndDragPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(ItemView), nameof(ItemView.OnEndDrag)); + } + + [PatchPostfix] + public static void Postfix() + { + MultiSelect.EndDrag(); + } + } + + public class GridViewCanAcceptPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(GridView), nameof(GridView.CanAccept)); + } + + [PatchPrefix] + public static bool Prefix(GridView __instance, ItemContextAbstractClass targetItemContext, ref GStruct413 operation, ref bool __result) + { + if (InPatch || !MultiSelect.Active) + { + return true; + } + + operation = default; + __result = false; + return false; + + /* InPatch = true; + foreach (ItemContextClass itemContext in MultiSelect.ItemContexts) + { + __result = __instance.CanAccept(itemContext, targetItemContext, out operation); + if (!__result) + { + break; + } + } + + InPatch = false; + return false;*/ + } + } + + public class SlotViewCanAcceptPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(SlotView), nameof(SlotView.CanAccept)); + } + + [PatchPrefix] + public static bool Prefix(SlotView __instance, ItemContextAbstractClass targetItemContext, ref GStruct413 operation, ref bool __result) + { + if (InPatch || !MultiSelect.Active) + { + return true; + } + + operation = default; + __result = false; + + InPatch = true; + foreach (ItemContextClass itemContext in MultiSelect.ItemContexts) + { + __result = __instance.CanAccept(itemContext, targetItemContext, out operation); + if (!__result) + { + break; + } + } + + InPatch = false; + return false; + } + } + + public class SlotViewAcceptItemPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(SlotView), nameof(SlotView.AcceptItem)); + } + + [PatchPrefix] + public static bool Prefix(SlotView __instance, ItemContextAbstractClass targetItemContext, ref Task __result) + { + if (InPatch || !MultiSelect.Active) + { + return true; + } + + InPatch = true; + /* __result = Task.CompletedTask; + foreach (ItemContextClass itemContext in MultiSelect.ItemContexts.ToList()) + { + __result = __result.ContinueWith(_ => __instance.AcceptItem(itemContext, targetItemContext)); + }*/ + + var serializer = __instance.GetOrAddComponent(); + __result = serializer.Initialize(MultiSelect.ItemContexts, itemContext => __instance.AcceptItem(itemContext, targetItemContext)); + + __result.ContinueWith(_ => { InPatch = false; }); + return false; + } + } + + public class TaskSerializer : MonoBehaviour + { + private Func func; + private Queue itemContexts; + private Task currentTask; + private TaskCompletionSource totalTask; + + public Task Initialize(IEnumerable itemContexts, Func func) + { + this.itemContexts = new(itemContexts); + this.func = func; + + currentTask = Task.CompletedTask; + Update(); + + totalTask = new TaskCompletionSource(); + return totalTask.Task; + } + + public void Update() + { + if (!currentTask.IsCompleted) + { + return; + } + + if (itemContexts.Any()) + { + currentTask = func(itemContexts.Dequeue()); + } + else + { + totalTask.Complete(); + func = null; + Destroy(this); + } + } + } + + /*public class GridViewAcceptItemPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(GridView), nameof(GridView.AcceptItem)); + } + + [PatchPrefix] + public static async bool Prefix(GridView __instance, ItemContextAbstractClass targetItemContext, ref Task __result) + { + if (InPatch || !MultiSelectContext.Instance.Any()) + { + return true; + } + + InPatch = true; + foreach (ItemContextClass itemContext in MultiSelectContext.Instance.SelectedDragContexts) + { + await __instance.AcceptItem(itemContext, targetItemContext); + } + + InPatch = false; + return false; + } + }*/ + } +} diff --git a/Plugin.cs b/Plugin.cs index b45f0c5..341010c 100644 --- a/Plugin.cs +++ b/Plugin.cs @@ -49,6 +49,7 @@ namespace UIFixes ContextMenuShortcutPatches.Enable(); new OpenSortingTablePatch().Enable(); LoadAmmoInRaidPatches.Enable(); + MultiSelectPatches.Enable(); } public static bool InRaid() diff --git a/R.cs b/R.cs index 10a5d08..2ea179a 100644 --- a/R.cs +++ b/R.cs @@ -58,6 +58,7 @@ namespace UIFixes GridSortPanel.InitTypes(); RepairStrategy.InitTypes(); ContextMenuHelper.InitTypes(); + RagfairNewOfferItemView.InitTypes(); } public abstract class Wrapper(object value) @@ -674,6 +675,23 @@ namespace UIFixes public InsuranceCompanyClass InsuranceCompany { get { return (InsuranceCompanyClass)InsuranceCompanyField.GetValue(Value); } } } + + public class RagfairNewOfferItemView(object value) : Wrapper(value) + { + public static Type Type { get; private set; } + private static FieldInfo SelectedMarkField; + private static FieldInfo SelectedBackgroundField; + + public static void InitTypes() + { + Type = typeof(EFT.UI.DragAndDrop.RagfairNewOfferItemView); + SelectedMarkField = AccessTools.Field(Type, "_selectedMark"); + SelectedBackgroundField = AccessTools.Field(Type, "_selectedBackground"); + } + + public GameObject SelectedMark { get { return (GameObject)SelectedMarkField.GetValue(Value); } } + public GameObject SelectedBackground { get { return (GameObject)SelectedBackgroundField.GetValue(Value); } } + } } public static class RExtentensions @@ -700,5 +718,6 @@ namespace UIFixes public static R.GridSortPanel R(this GridSortPanel value) => new(value); public static R.RepairerParametersPanel R(this RepairerParametersPanel value) => new(value); public static R.MessageWindow R(this MessageWindow value) => new(value); + public static R.RagfairNewOfferItemView R(this RagfairNewOfferItemView value) => new(value); } } diff --git a/UIFixes.csproj b/UIFixes.csproj index 2a67f52..cdc2bc0 100644 --- a/UIFixes.csproj +++ b/UIFixes.csproj @@ -57,6 +57,9 @@ $(PathToSPT)\EscapeFromTarkov_Data\Managed\UnityEngine.CoreModule.dll + + $(PathToSPT)\EscapeFromTarkov_Data\Managed\UnityEngine.IMGUIModule.dll + $(PathToSPT)\EscapeFromTarkov_Data\Managed\UnityEngine.InputLegacyModule.dll