diff --git a/ExtraProperties.cs b/ExtraProperties.cs index ce4cdf4..9c58ec5 100644 --- a/ExtraProperties.cs +++ b/ExtraProperties.cs @@ -1,6 +1,7 @@ using EFT.InventoryLogic; using EFT.UI.DragAndDrop; using System.Runtime.CompilerServices; +using UnityEngine; namespace UIFixes; @@ -10,7 +11,7 @@ public static class ExtraItemProperties private class Properties { - public bool Reordered; + public bool Reordered = false; } public static bool GetReordered(this Item item) => properties.GetOrCreateValue(item).Reordered; @@ -23,7 +24,7 @@ public static class ExtraTemplatedGridsViewProperties private class Properties { - public bool Reordered; + public bool Reordered = false; } public static bool GetReordered(this TemplatedGridsView gridsView) => properties.GetOrCreateValue(gridsView).Reordered; @@ -42,3 +43,47 @@ public static class ExtraTradingGridProperties public static bool GetShowOutOfStock(this TradingGridView gridView) => properties.GetOrCreateValue(gridView).ShowOutOfStock; public static void SetShowOutOfStock(this TradingGridView gridView, bool value) => properties.GetOrCreateValue(gridView).ShowOutOfStock = value; } + +public static class ExtraRagfairOfferItemViewProperties +{ + private static readonly ConditionalWeakTable properties = new(); + + private class Properties + { + public Vector2? SizeOverride = null; + public bool ShowCaption = false; + public string Inscription = null; + public string Count = null; + public string Tooltip = null; + } + + public static Vector2? GetSizeOverride(this RagfairOfferItemView itemView) => properties.GetOrCreateValue(itemView).SizeOverride; + public static void SetSizeOverride(this RagfairOfferItemView itemView, Vector2 value) => properties.GetOrCreateValue(itemView).SizeOverride = value; + + public static bool GetShowCaption(this RagfairOfferItemView itemView) => properties.GetOrCreateValue(itemView).ShowCaption; + public static void SetShowCaption(this RagfairOfferItemView itemView, bool value) => properties.GetOrCreateValue(itemView).ShowCaption = value; + + public static string GetInscription(this RagfairOfferItemView itemView) => properties.GetOrCreateValue(itemView).Inscription; + public static void SetInscription(this RagfairOfferItemView itemView, string value) => properties.GetOrCreateValue(itemView).Inscription = value; + + public static string GetCount(this RagfairOfferItemView itemView) => properties.GetOrCreateValue(itemView).Count; + public static void SetCount(this RagfairOfferItemView itemView, string value) => properties.GetOrCreateValue(itemView).Count = value; + + public static string GetTooltip(this RagfairOfferItemView itemView) => properties.GetOrCreateValue(itemView).Tooltip; + public static void SetTooltip(this RagfairOfferItemView itemView, string value) => properties.GetOrCreateValue(itemView).Tooltip = value; + +} + +public static class ExtraItemViewStatsProperties +{ + private static readonly ConditionalWeakTable properties = new(); + + private class Properties + { + public bool HideMods = false; + } + + public static bool GetHideMods(this ItemViewStats itemViewStats) => properties.GetOrCreateValue(itemViewStats).HideMods; + public static void SetHideMods(this ItemViewStats itemViewStats, bool value) => properties.GetOrCreateValue(itemViewStats).HideMods = value; +} + diff --git a/GlobalUsings.cs b/GlobalUsings.cs index 324da0f..19651d1 100644 --- a/GlobalUsings.cs +++ b/GlobalUsings.cs @@ -18,6 +18,7 @@ global using DiscardResult = GClass2799; global using ItemSorter = GClass2772; global using ItemWithLocation = GClass2521; global using SearchableGrid = GClass2516; +global using CursorManager = GClass3034; // State machine states global using FirearmReadyState = EFT.Player.FirearmController.GClass1619; diff --git a/Multiselect/DrawMultiSelect.cs b/Multiselect/DrawMultiSelect.cs index 2f765e2..bf3e469 100644 --- a/Multiselect/DrawMultiSelect.cs +++ b/Multiselect/DrawMultiSelect.cs @@ -1,4 +1,5 @@ -using Comfort.Common; +using BepInEx.Configuration; +using Comfort.Common; using EFT.UI; using EFT.UI.DragAndDrop; using System; @@ -24,6 +25,8 @@ public class DrawMultiSelect : MonoBehaviour private bool drawing; private bool secondary; + private static Vector2 Deadzone = new(5f, 5f); + public void Start() { selectTexture = new Texture2D(1, 1); @@ -56,37 +59,34 @@ public class DrawMultiSelect : MonoBehaviour return; } - // checking ItemUiContext is a quick and easy way to know the mouse is over an item - if (Input.GetKeyDown(Settings.SelectionBoxKey.Value.MainKey) && ItemUiContext.Instance.R().ItemContext == null) + if (Settings.SelectionBoxKey.Value.IsDownIgnoreOthers()) { - PointerEventData eventData = new(EventSystem.current) + bool shiftDown = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift); + + // Only need to check we aren't over draggables/clickables if the multiselect key is left mouse + if (Settings.SelectionBoxKey.Value.MainKey == KeyCode.Mouse0 && !shiftDown && !MouseIsOverClickable()) { - position = Input.mousePosition - }; - - List results = []; - localRaycaster.Raycast(eventData, results); - preloaderRaycaster.Raycast(eventData, results); - - foreach (GameObject gameObject in results.Select(r => r.gameObject)) - { - var draggables = gameObject.GetComponents() - .Where(c => c is IDragHandler || c is IBeginDragHandler || c is TextMeshProUGUI) // tmp_inputfield is draggable, but textmesh isn't so explicitly include - .Where(c => c is not ScrollRectNoDrag) // this disables scrolling, it doesn't add it - .Where(c => c.name != "Inner"); // there's a random DragTrigger sitting in ItemInfoWindows - - var clickables = gameObject.GetComponents() - .Where(c => c is IPointerClickHandler || c is IPointerDownHandler || c is IPointerUpHandler); - - if (draggables.Any() || clickables.Any()) - { - return; - } + return; } selectOrigin = Input.mousePosition; drawing = true; - secondary = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift); + secondary = shiftDown; + + if (!secondary) + { + MultiSelect.Clear(); + } + } + + if (drawing && !Settings.SelectionBoxKey.Value.IsPressedIgnoreOthers()) + { + drawing = false; + if (secondary) + { + MultiSelect.CombineSecondary(); + secondary = false; + } } if (drawing) @@ -94,6 +94,10 @@ public class DrawMultiSelect : MonoBehaviour selectEnd = Input.mousePosition; Rect selectRect = new(selectOrigin, selectEnd - selectOrigin); + if (Mathf.Abs(selectRect.size.x) < Deadzone.x && Mathf.Abs(selectRect.size.y) < Deadzone.y) + { + return; + } // If not secondary, then we can kick out any non-rendered items, plus they won't be covered by the foreach below if (!secondary) @@ -136,16 +140,6 @@ public class DrawMultiSelect : MonoBehaviour MultiSelect.Deselect(gridItemView, secondary); } } - - if (drawing && !Input.GetKey(Settings.SelectionBoxKey.Value.MainKey)) - { - drawing = false; - if (secondary) - { - MultiSelect.CombineSecondary(); - secondary = false; - } - } } public void OnGUI() @@ -171,6 +165,42 @@ public class DrawMultiSelect : MonoBehaviour } } + private bool MouseIsOverClickable() + { + // checking ItemUiContext is a quick and easy way to know the mouse is over an item + if (ItemUiContext.Instance.R().ItemContext != null) + { + return false; + } + + PointerEventData eventData = new(EventSystem.current) + { + position = Input.mousePosition + }; + + List results = []; + localRaycaster.Raycast(eventData, results); + preloaderRaycaster.Raycast(eventData, results); + + foreach (GameObject gameObject in results.Select(r => r.gameObject)) + { + var draggables = gameObject.GetComponents() + .Where(c => c is IDragHandler || c is IBeginDragHandler || c is TextMeshProUGUI) // tmp_inputfield is draggable, but textmesh isn't so explicitly include + .Where(c => c is not ScrollRectNoDrag) // this disables scrolling, it doesn't add it + .Where(c => c.name != "Inner"); // there's a random DragTrigger sitting in ItemInfoWindows + + var clickables = gameObject.GetComponents() + .Where(c => c is IPointerClickHandler || c is IPointerDownHandler || c is IPointerUpHandler); + + if (draggables.Any() || clickables.Any()) + { + return false; + } + } + + return true; + } + private bool IsOnTop(Rect itemRect, Transform itemTransform, GraphicRaycaster raycaster) { // Otherwise, ensure it's not overlapped by window UI diff --git a/Multiselect/MultiSelect.cs b/Multiselect/MultiSelect.cs index e3d07fb..5859374 100644 --- a/Multiselect/MultiSelect.cs +++ b/Multiselect/MultiSelect.cs @@ -125,6 +125,8 @@ public class MultiSelect public static void OnKillItemView(GridItemView itemView) { + CombineSecondary(); + MultiSelectItemContext itemContext = SelectedItems.FirstOrDefault(x => x.Value == itemView).Key; if (itemContext != null) { @@ -140,6 +142,8 @@ public class MultiSelect return; } + CombineSecondary(); + MultiSelectItemContext itemContext = SelectedItems.FirstOrDefault(x => x.Key.Item == itemView.Item).Key; if (itemContext != null) { @@ -158,6 +162,8 @@ public class MultiSelect return; } + CombineSecondary(); + MultiSelectItemContext oldItemContext = SelectedItems.FirstOrDefault(x => x.Key.Item == eventArgs.Item).Key; if (oldItemContext != null) { @@ -221,7 +227,7 @@ public class MultiSelect public static bool Active { - get { return SelectedItems.Count > 0; } + 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 diff --git a/Multiselect/MultiSelectDebug.cs b/Multiselect/MultiSelectDebug.cs index 9e78ddd..ff14a48 100644 --- a/Multiselect/MultiSelectDebug.cs +++ b/Multiselect/MultiSelectDebug.cs @@ -69,6 +69,6 @@ public class MultiSelectDebug : MonoBehaviour LocationInGrid location = address is GridItemAddress gridAddress ? gridAddress.LocationInGrid : null; string locationString = location != null ? $"({location.x}, {location.y})" : "(slot)"; - return $"x{itemContext.Item.StackObjectsCount} {address.Container.ID} {locationString} {itemContext.Item.Name.Localized()}"; + return $"x{itemContext.Item.StackObjectsCount} {(address != null ? address.Container.ID : "")} {locationString} {itemContext.Item.Name.Localized()}"; } } diff --git a/Patches/BarterOfferPatches.cs b/Patches/BarterOfferPatches.cs new file mode 100644 index 0000000..9a63221 --- /dev/null +++ b/Patches/BarterOfferPatches.cs @@ -0,0 +1,334 @@ +using EFT; +using EFT.InventoryLogic; +using EFT.UI; +using EFT.UI.DragAndDrop; +using EFT.UI.Ragfair; +using HarmonyLib; +using SPT.Reflection.Patching; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using TMPro; +using UnityEngine; +using UnityEngine.UI; + +namespace UIFixes; + +public static class BarterOfferPatches +{ + public static void Enable() + { + new IconsPatch().Enable(); + new ItemViewScalePatch().Enable(); + new ItemUpdateInfoPatch().Enable(); + new HideItemViewStatsPatch().Enable(); + new OverrideGridItemViewTooltipPatch().Enable(); + + new NoPointerEnterPatch().Enable(); + new NoPointerExitPatch().Enable(); + new NoPointerClickPatch().Enable(); + } + + public class IconsPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.DeclaredMethod(typeof(OfferItemPriceBarter), nameof(OfferItemPriceBarter.Show)); + } + + [PatchPostfix] + public static void Postfix( + OfferItemPriceBarter __instance, + IExchangeRequirement requirement, + ItemTooltip tooltip, + Offer offer, + InventoryControllerClass inventoryController, + ItemUiContext itemUiContext, + InsuranceCompanyClass insuranceCompany, + int index, + bool expanded, + GameObject ____barterIcon, + TextMeshProUGUI ____requirementName, + GameObject ____separator) + { + if (!Settings.ShowBarterIcons.Value) + { + return; + } + + if (requirement is not HandoverRequirement handoverRequirement) + { + return; + } + + bool isDogtag = requirement.Item.GetItemComponent() != null; + + HorizontalOrVerticalLayoutGroup layoutGroup = __instance.transform.parent.GetComponent(); + if (layoutGroup != null) + { + layoutGroup.spacing = 1f; + } + + Vector2 smallSizeDelta = ____barterIcon.RectTransform().sizeDelta; + + RagfairOfferItemView itemView = ItemViewFactory.CreateFromPool("ragfair_offer_layout"); + itemView.transform.SetParent(__instance.transform, false); + if (!expanded) + { + itemView.SetSizeOverride(smallSizeDelta); + + ItemViewStats itemViewStats = itemView.GetComponent(); + itemViewStats.SetHideMods(true); + } + else + { + if (isDogtag) + { + if (handoverRequirement.Side != EDogtagExchangeSide.Any) + { + itemView.SetShowCaption(true); + } + + itemView.SetInscription("LVLKILLLIST".Localized() + " " + handoverRequirement.Level); + } + + int ownedCount = GetOwnedCount(requirement, inventoryController); + itemView.SetCount(string.Format("{0}/{1}", ownedCount.FormatSeparate(" "), requirement.IntCount.FormatSeparate(" "), "C5C3B2")); + } + + if (isDogtag) + { + itemView.SetTooltip(string.Concat( + [ + "Dogtag".Localized(), + " ≥ ", + handoverRequirement.Level, + " ", + "LVLKILLLIST".Localized(), + (handoverRequirement.Side != EDogtagExchangeSide.Any ? ", " + handoverRequirement.Side : "").ToUpper() + ])); + } + + Vector2 sizeDelta = expanded ? new Vector2(64f, 64f) : smallSizeDelta; + LayoutElement layoutElement = itemView.GetComponent(); + layoutElement.preferredWidth = layoutElement.minWidth = sizeDelta.x; + layoutElement.preferredHeight = layoutElement.minHeight = sizeDelta.y; + + itemView.Show(null, requirement.Item, ItemRotation.Horizontal, false, inventoryController, requirement.Item.Owner, itemUiContext, null); + + ItemViewManager itemViewManager = __instance.GetOrAddComponent(); + itemViewManager.Init(itemView); + + ____barterIcon.SetActive(false); + ____separator?.SetActive(false); + + if (expanded) + { + ____requirementName.transform.parent.gameObject.SetActive(false); // The name and the ? icon + } + else + { + ____requirementName.gameObject.SetActive(false); + } + } + + private static int GetOwnedCount(IExchangeRequirement requirement, InventoryControllerClass inventoryController) + { + List allItems = []; + inventoryController.Inventory.Stash.GetAllAssembledItemsNonAlloc(allItems); + inventoryController.Inventory.QuestStashItems.GetAllAssembledItemsNonAlloc(allItems); + inventoryController.Inventory.QuestRaidItems.GetAllAssembledItemsNonAlloc(allItems); + + if (requirement is not HandoverRequirement handoverRequirement) + { + return 0; + } + + if (requirement.Item.GetItemComponent() != null) + { + return allItems.Where(item => RagFairClass.CanUseForBarterExchange(item, out string error)) + .Select(item => item.GetItemComponent()) + .Where(dogtag => dogtag != null) + .Where(dogtag => dogtag.Level >= handoverRequirement.Level) + .Where(dogtag => handoverRequirement.Side == EDogtagExchangeSide.Any || dogtag.Side.ToString() == handoverRequirement.Side.ToString()) + .Count(); + } + + return allItems.Where(item => RagFairClass.CanUseForBarterExchange(item, out string error)) + .Where(item => item.TemplateId == requirement.Item.TemplateId) + .Where(item => !requirement.OnlyFunctional || item is not LootItemClass lootItem || !lootItem.MissingVitalParts.Any()) + .Where(item => item is not GInterface325 encodable || requirement.Item is not GInterface325 || encodable.IsEncoded() == requirement.IsEncoded) + .Sum(item => item.StackObjectsCount); + } + } + + public class ItemViewScalePatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.DeclaredMethod(typeof(RagfairOfferItemView), nameof(RagfairOfferItemView.UpdateScale)); + } + + [PatchPostfix] + public static void Postfix(RagfairOfferItemView __instance, Image ___MainImage) + { + Vector2? sizeOverride = __instance.GetSizeOverride(); + if (sizeOverride.HasValue) + { + Vector2 sizeDelta = ___MainImage.rectTransform.sizeDelta; + float x = sizeDelta.x; + float y = sizeDelta.y; + + // Calculate scale and multiply to preserve aspect ratio + float scale = Mathf.Min((float)sizeOverride.Value.x / x, (float)sizeOverride.Value.y / y); + ___MainImage.rectTransform.sizeDelta = new Vector2(x * scale, y * scale); + } + } + } + + public class ItemUpdateInfoPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.DeclaredMethod(typeof(RagfairOfferItemView), nameof(RagfairOfferItemView.UpdateInfo)); + } + + [PatchPostfix] + public static void Postfix(RagfairOfferItemView __instance, TextMeshProUGUI ___Caption, TextMeshProUGUI ___ItemInscription, TextMeshProUGUI ___ItemValue) + { + if (__instance.GetShowCaption()) + { + ___Caption.gameObject.SetActive(true); + } + + string inscription = __instance.GetInscription(); + if (!string.IsNullOrEmpty(inscription)) + { + ___ItemInscription.text = inscription; + ___ItemInscription.gameObject.SetActive(true); + } + + string value = __instance.GetCount(); + if (!string.IsNullOrEmpty(value)) + { + ___ItemValue.text = value; + ___ItemValue.fontSize = 16f; + ___ItemValue.alignment = TextAlignmentOptions.Left; + + RectTransform rectTransform = ___ItemValue.RectTransform(); + rectTransform.pivot = new Vector2(0f, 0.5f); + rectTransform.anchorMin = rectTransform.anchorMax = new Vector2(1f, 0.5f); + rectTransform.anchoredPosition = new Vector2(5f, 0f); + ___ItemValue.gameObject.SetActive(true); + } + } + } + + public class OverrideGridItemViewTooltipPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.DeclaredMethod(typeof(GridItemView), nameof(GridItemView.ShowTooltip)); + } + + [PatchPrefix] + public static bool Prefix(GridItemView __instance, ItemUiContext ___ItemUiContext) + { + if (__instance is not RagfairOfferItemView ragfairOfferItemView) + { + return true; + } + + string tooltip = ragfairOfferItemView.GetTooltip(); + if (!string.IsNullOrEmpty(tooltip)) + { + ___ItemUiContext.Tooltip.Show(tooltip, null, 0.5f); + return false; + } + + return true; + } + } + + public class HideItemViewStatsPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(ItemViewStats), nameof(ItemViewStats.SetStaticInfo)); + } + + [PatchPrefix] + public static bool Prefix(ItemViewStats __instance, Image ____modIcon, Image ____modTypeIcon, Image ____specialIcon, Image ____armorClassIcon) + { + if (!__instance.GetHideMods()) + { + return true; + } + + ____modIcon.gameObject.SetActive(false); + ____modTypeIcon.gameObject.SetActive(false); + ____specialIcon.gameObject.SetActive(false); + ____armorClassIcon?.gameObject.SetActive(false); + + return false; + } + } + + public class NoPointerEnterPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(OfferItemPriceBarter), nameof(OfferItemPriceBarter.OnPointerEnter)); + } + + [PatchPrefix] + public static bool Prefix() + { + return !Settings.ShowBarterIcons.Value; + } + } + + public class NoPointerExitPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(OfferItemPriceBarter), nameof(OfferItemPriceBarter.OnPointerExit)); + } + + [PatchPrefix] + public static bool Prefix() + { + return !Settings.ShowBarterIcons.Value; + } + } + + public class NoPointerClickPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(OfferItemPriceBarter), nameof(OfferItemPriceBarter.OnPointerClick)); + } + + [PatchPrefix] + public static bool Prefix() + { + return !Settings.ShowBarterIcons.Value; + } + } + + public class ItemViewManager : MonoBehaviour + { + RagfairOfferItemView itemView; + + public void Init(RagfairOfferItemView itemView) + { + this.itemView = itemView; + } + + public void OnDestroy() + { + itemView.IsStub = true; + itemView.Kill(); + } + } +} \ No newline at end of file diff --git a/Patches/ContextMenuPatches.cs b/Patches/ContextMenuPatches.cs index e5d5532..2347dfc 100644 --- a/Patches/ContextMenuPatches.cs +++ b/Patches/ContextMenuPatches.cs @@ -192,13 +192,20 @@ public static class ContextMenuPatches { protected override MethodBase GetTargetMethod() { - return AccessTools.Method(R.TradingInteractions.Type, "get_SubInteractions"); + return AccessTools.PropertyGetter( + typeof(ItemInfoInteractionsAbstractClass), + nameof(ItemInfoInteractionsAbstractClass.SubInteractions)); } [PatchPostfix] - public static void Postfix(ref IEnumerable __result) + public static void Postfix( + ItemInfoInteractionsAbstractClass __instance, + ref IEnumerable __result) { - __result = __result.Append(EItemInfoButton.Repair).Append(EItemInfoButton.Insure); + if (R.TradingInteractions.Type.IsInstanceOfType(__instance)) + { + __result = __result.Append(EItemInfoButton.Repair).Append(EItemInfoButton.Insure); + } } } @@ -208,12 +215,23 @@ public static class ContextMenuPatches protected override MethodBase GetTargetMethod() { - return AccessTools.Method(R.TradingInteractions.Type, "CreateSubInteractions"); + return AccessTools.Method( + typeof(ItemInfoInteractionsAbstractClass), + nameof(ItemInfoInteractionsAbstractClass.CreateSubInteractions)); } [PatchPrefix] - public static bool Prefix(object __instance, EItemInfoButton parentInteraction, ISubInteractions subInteractionsWrapper, ItemUiContext ___itemUiContext_0) + public static bool Prefix( + ItemInfoInteractionsAbstractClass __instance, + EItemInfoButton parentInteraction, + ISubInteractions subInteractionsWrapper, + ItemUiContext ___itemUiContext_0) { + if (!R.TradingInteractions.Type.IsInstanceOfType(__instance)) + { + return true; + } + // Clear this, since something else should be active (even a different mouseover of the insurance button) LoadingInsuranceActions = false; diff --git a/Patches/ContextMenuShortcutPatches.cs b/Patches/ContextMenuShortcutPatches.cs index edcf444..e2de8f9 100644 --- a/Patches/ContextMenuShortcutPatches.cs +++ b/Patches/ContextMenuShortcutPatches.cs @@ -1,11 +1,11 @@ -using EFT.InventoryLogic; +using Comfort.Common; +using EFT.InventoryLogic; using EFT.UI; using EFT.UI.DragAndDrop; using HarmonyLib; using SPT.Reflection.Patching; using System.Reflection; using TMPro; -using UnityEngine; using UnityEngine.EventSystems; namespace UIFixes; @@ -19,7 +19,6 @@ public static class ContextMenuShortcutPatches new ItemUiContextPatch().Enable(); new HideoutItemViewRegisterContextPatch().Enable(); - new HideoutItemViewUnegisterContextPatch().Enable(); new TradingPanelRegisterContextPatch().Enable(); new TradingPanelUnregisterContextPatch().Enable(); @@ -76,12 +75,12 @@ public static class ContextMenuShortcutPatches if (Settings.UseAllKeyBind.Value.IsDown()) { - TryInteraction(__instance, itemContext, EItemInfoButton.UseAll, EItemInfoButton.Use); + TryInteraction(__instance, itemContext, EItemInfoButton.UseAll, [EItemInfoButton.Use]); } if (Settings.UnloadKeyBind.Value.IsDown()) { - TryInteraction(__instance, itemContext, EItemInfoButton.Unload, EItemInfoButton.UnloadAmmo); + TryInteraction(__instance, itemContext, EItemInfoButton.Unload, [EItemInfoButton.UnloadAmmo]); } if (Settings.UnpackKeyBind.Value.IsDown()) @@ -99,15 +98,62 @@ public static class ContextMenuShortcutPatches TryInteraction(__instance, itemContext, EItemInfoButton.LinkedSearch); } + if (Settings.SortingTableKeyBind.Value.IsDown()) + { + MoveToFromSortingTable(itemContext, __instance); + } + + if (Settings.ExamineKeyBind.Value.IsDown()) + { + TryInteraction(__instance, itemContext, EItemInfoButton.Examine, + [EItemInfoButton.Fold, EItemInfoButton.Unfold, EItemInfoButton.TurnOn, EItemInfoButton.TurnOff, EItemInfoButton.CheckMagazine]); + } + Interactions = null; } - private static void TryInteraction(ItemUiContext itemUiContext, ItemContextAbstractClass itemContext, EItemInfoButton interaction, EItemInfoButton? fallbackInteraction = null) + private static void TryInteraction(ItemUiContext itemUiContext, ItemContextAbstractClass itemContext, EItemInfoButton interaction, EItemInfoButton[] fallbackInteractions = null) { Interactions ??= itemUiContext.GetItemContextInteractions(itemContext, null); - if (!Interactions.ExecuteInteraction(interaction) && fallbackInteraction.HasValue) + if (!Interactions.ExecuteInteraction(interaction) && fallbackInteractions != null) { - Interactions.ExecuteInteraction(fallbackInteraction.Value); + foreach (var fallbackInteraction in fallbackInteractions) + { + if (Interactions.ExecuteInteraction(fallbackInteraction)) + { + return; + } + } + } + } + + private static void MoveToFromSortingTable(ItemContextAbstractClass itemContext, ItemUiContext itemUiContext) + { + Item item = itemContext.Item; + if (item.Owner is not InventoryControllerClass controller) + { + return; + } + + SortingTableClass sortingTable = controller.Inventory.SortingTable; + bool isInSortingTable = sortingTable != null && item.Parent.Container.ParentItem == sortingTable; + + var operation = isInSortingTable ? itemUiContext.QuickFindAppropriatePlace(itemContext, controller, false, true, true) : itemUiContext.QuickMoveToSortingTable(item, true); + if (operation.Succeeded && controller.CanExecute(operation.Value)) + { + if (operation.Value is IDestroyResult destroyResult && destroyResult.ItemsDestroyRequired) + { + NotificationManagerClass.DisplayWarningNotification(new DestroyError(item, destroyResult.ItemsToDestroy).GetLocalizedDescription()); + return; + } + + controller.RunNetworkTransaction(operation.Value, null); + if (itemUiContext.Tooltip != null) + { + itemUiContext.Tooltip.Close(); + } + + Singleton.Instance.PlayItemSound(item.ItemSound, EInventorySoundType.pickup, false); } } } @@ -127,20 +173,6 @@ public static class ContextMenuShortcutPatches } } - public class HideoutItemViewUnegisterContextPatch : ModulePatch - { - protected override MethodBase GetTargetMethod() - { - return AccessTools.Method(typeof(HideoutItemView), nameof(HideoutItemView.OnPointerExit)); - } - - [PatchPostfix] - public static void Postfix(HideoutItemView __instance, ItemUiContext ___ItemUiContext) - { - ___ItemUiContext.UnregisterCurrentItemContext(__instance.ItemContext); - } - } - public class TradingPanelRegisterContextPatch : ModulePatch { protected override MethodBase GetTargetMethod() diff --git a/Patches/FixFleaPatches.cs b/Patches/FixFleaPatches.cs index 93bfd48..1590249 100644 --- a/Patches/FixFleaPatches.cs +++ b/Patches/FixFleaPatches.cs @@ -1,6 +1,8 @@ -using EFT.UI.Ragfair; +using EFT.UI; +using EFT.UI.Ragfair; using HarmonyLib; using SPT.Reflection.Patching; +using System.Linq; using System.Reflection; using TMPro; using UnityEngine; @@ -12,12 +14,15 @@ public static class FixFleaPatches { public static void Enable() { - // These two are anal AF + // These are anal AF new DoNotToggleOnMouseOverPatch().Enable(); new ToggleOnOpenPatch().Enable(); + new DropdownHeightPatch().Enable(); new OfferItemFixMaskPatch().Enable(); new OfferViewTweaksPatch().Enable(); + + new SearchPatch().Enable(); } public class DoNotToggleOnMouseOverPatch : ModulePatch @@ -95,4 +100,57 @@ public static class FixFleaPatches timeLeft.childControlWidth = false; } } + + public class SearchPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(RagfairCategoriesPanel), nameof(RagfairCategoriesPanel.method_9)); + } + + [PatchPrefix] + public static bool Prefix(RagfairCategoriesPanel __instance, string arg) + { + if (!Settings.ClearFiltersOnSearch.Value) + { + return true; + } + + if (arg.StartsWith("#") || __instance.Ragfair == null || __instance.EViewListType_0 != EViewListType.AllOffers) + { + return true; + } + + if (__instance.FilteredNodes.Values.Sum(node => node.Count) > 0) + { + return true; + } + + __instance.Ragfair.CancellableFilters.Clear(); + + FilterRule filterRule = __instance.Ragfair.method_3(EViewListType.AllOffers); + filterRule.HandbookId = string.Empty; + + __instance.Ragfair.AddSearchesInRule(filterRule, true); + + return false; + } + } + + public class DropdownHeightPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.DeclaredMethod(typeof(DropDownBox), nameof(DropDownBox.Init)); + } + + [PatchPostfix] + public static void Postfix(ref float ____maxVisibleHeight) + { + if (____maxVisibleHeight == 120f) + { + ____maxVisibleHeight = 240f; + } + } + } } diff --git a/Patches/MultiSelectPatches.cs b/Patches/MultiSelectPatches.cs index 9603996..eeaf7f1 100644 --- a/Patches/MultiSelectPatches.cs +++ b/Patches/MultiSelectPatches.cs @@ -26,7 +26,7 @@ public static class MultiSelectPatches // Used to prevent infinite recursion of CanAccept/AcceptItem private static bool InPatch = false; - // If the CanAccept method should render highlights + // Keep track of preview images when dragging private static readonly List Previews = []; // Point that various QuickFindPlace overrides should start at @@ -150,7 +150,7 @@ public static class MultiSelectPatches } [PatchPostfix] - public static void Postfix(ItemView __instance, PointerEventData eventData) + public static void Postfix(ItemView __instance, PointerEventData eventData, TraderControllerClass ___ItemController) { if (!MultiSelect.Enabled || __instance is RagfairNewOfferItemView || __instance is InsuranceItemView) { @@ -161,12 +161,36 @@ public static class MultiSelectPatches bool shiftDown = Input.GetKey(KeyCode.LeftShift) && !Input.GetKey(KeyCode.RightShift); bool altDown = Input.GetKey(KeyCode.LeftAlt) && !Input.GetKey(KeyCode.RightAlt); - if (Settings.EnableMultiClick.Value && __instance is GridItemView gridItemView && eventData.button == PointerEventData.InputButton.Left && shiftDown && !ctrlDown && !altDown) + // If sorting table is open and default shift-click behavior is enabled, don't multiselect + bool couldBeSortingTableMove = false; + if (Settings.DefaultSortingTableBind.Value && + shiftDown && + eventData.button == PointerEventData.InputButton.Left && + ___ItemController is InventoryControllerClass inventoryController) + { + SortingTableClass sortingTable = inventoryController.Inventory.SortingTable; + if (sortingTable != null && sortingTable.IsVisible) + { + couldBeSortingTableMove = true; + } + } + + if (Settings.EnableMultiClick.Value && + !couldBeSortingTableMove && + __instance is GridItemView gridItemView && + eventData.button == PointerEventData.InputButton.Left && + shiftDown && !ctrlDown && !altDown) { MultiSelect.Toggle(gridItemView); return; } + // Mainly this tests for when selection box is rebound to another mouse button, to enable secondary selection + if (!couldBeSortingTableMove && shiftDown && Settings.SelectionBoxKey.Value.IsDownIgnoreOthers()) + { + return; + } + if (__instance is not GridItemView gridItemView2 || !MultiSelect.IsSelected(gridItemView2)) { MultiSelect.Clear(); @@ -211,9 +235,14 @@ public static class MultiSelectPatches return false; } - if (shiftDown) + if (shiftDown && !ctrlDown && !altDown) { - // Nothing to do, mousedown handled it. + if (Settings.DefaultSortingTableBind.Value) + { + QuickMove(__instance, ___ItemUiContext, ___ItemController, true); + return false; + } + return true; } @@ -222,15 +251,17 @@ public static class MultiSelectPatches return true; } - private static void QuickMove(GridItemView gridItemView, ItemUiContext itemUiContext, TraderControllerClass itemController) + private static void QuickMove(GridItemView gridItemView, ItemUiContext itemUiContext, TraderControllerClass itemController, bool moveToSortingTable = false) { bool succeeded = true; DisableMerge = true; IgnoreItemParent = true; Stack operations = new(); - foreach (DragItemContext selectedItemContext in MultiSelect.SortedItemContexts()) + foreach (var selectedItemContext in MultiSelect.SortedItemContexts()) { - ItemOperation operation = itemUiContext.QuickFindAppropriatePlace(selectedItemContext, itemController, false /*forceStash*/, false /*showWarnings*/, false /*simulate*/); + ItemOperation operation = moveToSortingTable ? + itemUiContext.QuickMoveToSortingTable(selectedItemContext.Item, false /*simulate*/) : + itemUiContext.QuickFindAppropriatePlace(selectedItemContext, itemController, false /*forceStash*/, false /*showWarnings*/, false /*simulate*/); if (operation.Succeeded && itemController.CanExecute(operation.Value)) { operations.Push(operation); @@ -434,6 +465,13 @@ public static class MultiSelectPatches return AccessTools.Method(typeof(ItemView), nameof(ItemView.OnBeginDrag)); } + [PatchPrefix] + public static bool Prefix() + { + // Disable drag if shift is down + return !Input.GetKey(KeyCode.LeftShift) && !Input.GetKey(KeyCode.RightShift); + } + [PatchPostfix] public static void Postfix(ItemView __instance) { diff --git a/Patches/OpenSortingTablePatch.cs b/Patches/OpenSortingTablePatch.cs deleted file mode 100644 index d887cc1..0000000 --- a/Patches/OpenSortingTablePatch.cs +++ /dev/null @@ -1,59 +0,0 @@ -using Comfort.Common; -using EFT.UI; -using HarmonyLib; -using SPT.Reflection.Patching; -using System.Linq; -using System.Reflection; -using UnityEngine; - -namespace UIFixes; - -public class OpenSortingTablePatch : ModulePatch -{ - private static readonly EItemUiContextType[] AllowedScreens = [EItemUiContextType.InventoryScreen, EItemUiContextType.ScavengerInventoryScreen]; - - - protected override MethodBase GetTargetMethod() - { - return AccessTools.Method(typeof(ItemUiContext), nameof(ItemUiContext.QuickMoveToSortingTable)); - } - - [PatchPrefix] - public static bool Prefix(ItemUiContext __instance, ref ItemOperation __result) - { - // BSG checks visibility, not in-raid. There's a bug where somehow that visibility can be true in raid - if (Plugin.InRaid()) - { - __result = new GClass3370("SortingTable/VisibilityError"); - return false; - } - - if (!Settings.AutoOpenSortingTable.Value || !AllowedScreens.Contains(__instance.ContextType)) - { - return true; - } - - // Temporary work-around for LootValue bug - bail out if the ALT key is down - if (Input.GetKey(KeyCode.LeftAlt)) - { - return true; - } - - SortingTableClass sortingTable = __instance.R().InventoryController.Inventory.SortingTable; - if (sortingTable != null && !sortingTable.IsVisible) - { - if (__instance.ContextType == EItemUiContextType.InventoryScreen) - { - Singleton.Instance.InventoryScreen.method_6(); - Singleton.Instance.InventoryScreen.R().SimpleStashPanel.ChangeSortingTableTabState(true); - } - else if (__instance.ContextType == EItemUiContextType.ScavengerInventoryScreen) - { - Singleton.Instance.ScavengerInventoryScreen.method_7(); - Singleton.Instance.ScavengerInventoryScreen.R().SimpleStashPanel.ChangeSortingTableTabState(true); - } - } - - return true; - } -} diff --git a/Patches/OpenSortingTablePatches.cs b/Patches/OpenSortingTablePatches.cs new file mode 100644 index 0000000..58d07b7 --- /dev/null +++ b/Patches/OpenSortingTablePatches.cs @@ -0,0 +1,98 @@ +using Comfort.Common; +using EFT.UI; +using EFT.UI.DragAndDrop; +using HarmonyLib; +using SPT.Reflection.Patching; +using System.Linq; +using System.Reflection; +using UnityEngine; +using UnityEngine.EventSystems; + +namespace UIFixes; + +public static class OpenSortingTablePatches +{ + public static void Enable() + { + new AutoOpenPatch().Enable(); + new DefaultBindPatch().Enable(); + } + + public class AutoOpenPatch : ModulePatch + { + private static readonly EItemUiContextType[] AllowedScreens = [EItemUiContextType.InventoryScreen, EItemUiContextType.ScavengerInventoryScreen]; + + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(ItemUiContext), nameof(ItemUiContext.QuickMoveToSortingTable)); + } + + [PatchPrefix] + public static bool Prefix(ItemUiContext __instance, ref ItemOperation __result) + { + // BSG checks visibility, not in-raid. There's a bug where somehow that visibility can be true in raid + if (Plugin.InRaid()) + { + __result = new GClass3370("SortingTable/VisibilityError"); + return false; + } + + // Allowed screens only, and auto-open is enabled or the custom bind is active + if (!AllowedScreens.Contains(__instance.ContextType) || (!Settings.AutoOpenSortingTable.Value && !Settings.SortingTableKeyBind.Value.IsDown())) + { + return true; + } + + // Temporary work-around for LootValue bug - bail out if the ALT key is down + if (Input.GetKey(KeyCode.LeftAlt)) + { + return true; + } + + SortingTableClass sortingTable = __instance.R().InventoryController.Inventory.SortingTable; + if (sortingTable != null && !sortingTable.IsVisible) + { + if (__instance.ContextType == EItemUiContextType.InventoryScreen) + { + Singleton.Instance.InventoryScreen.method_6(); + Singleton.Instance.InventoryScreen.R().SimpleStashPanel.ChangeSortingTableTabState(true); + } + else if (__instance.ContextType == EItemUiContextType.ScavengerInventoryScreen) + { + Singleton.Instance.ScavengerInventoryScreen.method_7(); + Singleton.Instance.ScavengerInventoryScreen.R().SimpleStashPanel.ChangeSortingTableTabState(true); + } + } + + return true; + } + } + + public class DefaultBindPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(ItemView), nameof(ItemView.OnClick)); + } + + [PatchPrefix] + public static bool Prefix(PointerEventData.InputButton button, bool doubleClick) + { + if (Settings.DefaultSortingTableBind.Value) + { + return true; + } + + bool ctrlDown = Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl); + bool altDown = Input.GetKey(KeyCode.LeftAlt) || Input.GetKey(KeyCode.RightAlt); + bool shiftDown = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift); + + if (button == PointerEventData.InputButton.Left && !doubleClick && !ctrlDown && !altDown && shiftDown) + { + return false; + } + + return true; + } + } +} diff --git a/Patches/ReloadInPlacePatches.cs b/Patches/ReloadInPlacePatches.cs new file mode 100644 index 0000000..1d6dbce --- /dev/null +++ b/Patches/ReloadInPlacePatches.cs @@ -0,0 +1,156 @@ +using EFT; +using EFT.UI; +using HarmonyLib; +using SPT.Reflection.Patching; +using System; +using System.Linq; +using System.Reflection; + +namespace UIFixes; + +public static class ReloadInPlacePatches +{ + private static bool IsReloading = false; + private static MagazineClass FoundMagazine = null; + + public static void Enable() + { + // These patch ItemUiContext.ReloadWeapon, which is called from the context menu Reload + new ReloadInPlacePatch().Enable(); + new ReloadInPlaceFindMagPatch().Enable(); + new ReloadInPlaceFindSpotPatch().Enable(); + + // This patches the firearmsController code when you hit R in raid with an external magazine class + new SwapIfNoSpacePatch().Enable(); + } + + public class ReloadInPlacePatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(ItemUiContext), nameof(ItemUiContext.ReloadWeapon)); + } + + [PatchPrefix] + public static void Prefix() + { + IsReloading = Settings.SwapMags.Value; + } + + [PatchPostfix] + public static void Postfix() + { + IsReloading = false; + FoundMagazine = null; + } + } + + public class ReloadInPlaceFindMagPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(ItemUiContext), nameof(ItemUiContext.method_5)); + } + + [PatchPostfix] + public static void Postfix(MagazineClass __result) + { + if (IsReloading) + { + FoundMagazine = __result; + } + } + } + + public class ReloadInPlaceFindSpotPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + Type type = typeof(ItemUiContext).GetNestedTypes().Single(t => t.GetField("currentMagazine") != null); + return AccessTools.Method(type, "method_0"); + } + + [PatchPrefix] + public static void Prefix(StashGridClass grid, ref GStruct414 __state) + { + if (!Settings.SwapMags.Value) + { + return; + } + + if (grid.Contains(FoundMagazine)) + { + __state = InteractionsHandlerClass.Remove(FoundMagazine, grid.ParentItem.Owner as TraderControllerClass, false, false); + } + } + + [PatchPostfix] + public static void Postfix(GStruct414 __state) + { + if (!Settings.SwapMags.Value || __state.Value == null) + { + return; + } + + if (__state.Succeeded) + { + __state.Value.RollBack(); + } + } + } + + public class SwapIfNoSpacePatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(Player.FirearmController), nameof(Player.FirearmController.ReloadMag)); + } + + // By default this method will do a series of removes and adds, but not swap, to reload + // This tied to a different animation state machine sequence than Swap(), and is faster than Swap. + // So only use Swap if *needed*, otherwise its penalizing all reload speeds + [PatchPrefix] + public static bool Prefix(Player.FirearmController __instance, MagazineClass magazine, ItemAddressClass gridItemAddress) + { + // If gridItemAddress isn't null, it already found a place for the current mag, so let it run (unless always swap is enabled) + if (!Settings.SwapMags.Value || (gridItemAddress != null && !Settings.AlwaysSwapMags.Value)) + { + return true; + } + + // Weapon doesn't currently have a magazine, let the default run (will load one) + MagazineClass currentMagazine = __instance.Weapon.GetCurrentMagazine(); + if (currentMagazine == null) + { + return true; + } + + InventoryControllerClass controller = __instance.Weapon.Owner as InventoryControllerClass; + + // Null address means it couldn't find a spot. Try to remove magazine (temporarily) and try again + var operation = InteractionsHandlerClass.Remove(magazine, controller, false, false); + if (operation.Failed) + { + return true; + } + + gridItemAddress = controller.Inventory.Equipment.GetPrioritizedGridsForUnloadedObject(false) + .Select(grid => grid.FindLocationForItem(currentMagazine)) + .Where(address => address != null) + .OrderBy(address => address.Grid.GridWidth.Value * address.Grid.GridHeight.Value) + .FirstOrDefault(); // BSG's version checks null again, but there's no nulls already. If there's no matches, the enumerable is empty + + // Put the magazine back + operation.Value.RollBack(); + + if (gridItemAddress == null) + { + // Didn't work, nowhere to put magazine. Let it run (will drop mag on ground) + return true; + } + + controller.TryRunNetworkTransaction(InteractionsHandlerClass.Swap(currentMagazine, gridItemAddress, magazine, new GClass2783(__instance.Weapon.GetMagazineSlot()), controller, true), null); + return false; + } + } +} \ No newline at end of file diff --git a/Patches/ScrollPatches.cs b/Patches/ScrollPatches.cs index e523b35..9ea6d43 100644 --- a/Patches/ScrollPatches.cs +++ b/Patches/ScrollPatches.cs @@ -23,7 +23,10 @@ public static class ScrollPatches new EnchanceTraderStashScrollingPatch().Enable(); new EnhanceFleaScrollingPatch().Enable(); new EnhanceMailScrollingPatch().Enable(); + new MouseScrollingSpeedPatch().Enable(); + new LightScrollerSpeedPatch().Enable(); + new EnhanceHideoutScrollingPatch().Enable(); new EnhanceTaskListScrollingPatch().Enable(); new OpenLastTaskPatch().Enable(); @@ -305,6 +308,21 @@ public static class ScrollPatches } } + public class LightScrollerSpeedPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(LightScroller), nameof(LightScroller.method_1)); + } + + [PatchPrefix] + public static void Prefix(ref float deltaPixels) + { + int multi = Settings.UseRaidMouseScrollMulti.Value && Plugin.InRaid() ? Settings.MouseScrollMultiInRaid.Value : Settings.MouseScrollMulti.Value; + deltaPixels *= multi; + } + } + public class EnhanceTaskListScrollingPatch : ModulePatch { protected override MethodBase GetTargetMethod() diff --git a/Patches/SortPatches.cs b/Patches/SortPatches.cs index 9c5a638..c6264af 100644 --- a/Patches/SortPatches.cs +++ b/Patches/SortPatches.cs @@ -119,7 +119,6 @@ public static class SortPatches { Error error = null; var mergeableItems = lootItem.Grids.SelectMany(g => g.Items) - .OfType() .Where(i => i.StackObjectsCount < i.StackMaxSize) .ToArray(); @@ -131,7 +130,7 @@ public static class SortPatches continue; } - if (InteractionsHandlerClass.smethod_0(lootItem.Grids, item, out Stackable targetItem, 1)) + if (Sorter.FindStackForMerge(lootItem.Grids, item, out Item targetItem, 1)) { var operation = InteractionsHandlerClass.TransferOrMerge(item, targetItem, inventoryController, true); if (operation.Succeeded) diff --git a/Patches/StackFirItemsPatches.cs b/Patches/StackFirItemsPatches.cs index 00dbdbf..fec2674 100644 --- a/Patches/StackFirItemsPatches.cs +++ b/Patches/StackFirItemsPatches.cs @@ -27,44 +27,10 @@ public static class StackFirItemsPatches return method; } - // Reimplementing this entire method to ignore SpawnedInSession for certain types [PatchPrefix] - public static bool Prefix(IEnumerable containersToPut, Item itemToMerge, ref object mergeableItem, int overrideCount, ref bool __result) + public static bool Prefix(IEnumerable containersToPut, Item itemToMerge, ref Item mergeableItem, int overrideCount, ref bool __result) { - if (!MergeableItemType.IsInstanceOfType(itemToMerge)) - { - mergeableItem = null; - __result = false; - } - - if (overrideCount <= 0) - { - overrideCount = itemToMerge.StackObjectsCount; - } - - bool ignoreSpawnedInSession; - if (itemToMerge.Template is MoneyClass) - { - ignoreSpawnedInSession = Settings.MergeFIRMoney.Value; - } - else if (itemToMerge.Template is AmmoTemplate) - { - ignoreSpawnedInSession = Settings.MergeFIRAmmo.Value; - } - else - { - ignoreSpawnedInSession = Settings.MergeFIROther.Value; - } - - mergeableItem = containersToPut.SelectMany(x => x.Items) - .Where(MergeableItemType.IsInstanceOfType) - .Where(x => x != itemToMerge) - .Where(x => x.TemplateId == itemToMerge.TemplateId) - .Where(x => ignoreSpawnedInSession || x.SpawnedInSession == itemToMerge.SpawnedInSession) - .Where(x => x.StackObjectsCount < x.StackMaxSize) - .FirstOrDefault(x => overrideCount <= x.StackMaxSize - x.StackObjectsCount); - - __result = mergeableItem != null; + __result = Sorter.FindStackForMerge(containersToPut, itemToMerge, out mergeableItem, overrideCount); return false; } } @@ -79,19 +45,12 @@ public static class StackFirItemsPatches [PatchPrefix] public static bool Prefix(Item __instance, Item other, ref bool __result) { - bool ignoreSpawnedInSession; - if (__instance.Template is MoneyClass) + bool ignoreSpawnedInSession = __instance.Template switch { - ignoreSpawnedInSession = Settings.MergeFIRMoney.Value; - } - else if (__instance.Template is AmmoTemplate) - { - ignoreSpawnedInSession = Settings.MergeFIRAmmo.Value; - } - else - { - ignoreSpawnedInSession = Settings.MergeFIROther.Value; - } + MoneyClass _ => Settings.MergeFIRMoney.Value, + AmmoTemplate _ => Settings.MergeFIRMoney.Value, + _ => Settings.MergeFIROther.Value, + }; __result = __instance.TemplateId == other.TemplateId && __instance.Id != other.Id && (ignoreSpawnedInSession || __instance.SpawnedInSession == other.SpawnedInSession); return false; diff --git a/Patches/UnloadAmmoPatches.cs b/Patches/UnloadAmmoPatches.cs index f64aa64..118b798 100644 --- a/Patches/UnloadAmmoPatches.cs +++ b/Patches/UnloadAmmoPatches.cs @@ -3,6 +3,7 @@ using EFT.InventoryLogic; using EFT.UI; using HarmonyLib; using SPT.Reflection.Patching; +using SPT.Reflection.Utils; using System; using System.Collections.Generic; using System.Linq; @@ -13,12 +14,17 @@ namespace UIFixes; public static class UnloadAmmoPatches { + private static UnloadAmmoBoxState UnloadState = null; + public static void Enable() { new TradingPlayerPatch().Enable(); new TransferPlayerPatch().Enable(); new UnloadScavTransferPatch().Enable(); new NoScavStashPatch().Enable(); + + new UnloadAmmoBoxPatch().Enable(); + new QuickFindUnloadAmmoBoxPatch().Enable(); } public class TradingPlayerPatch : ModulePatch @@ -98,4 +104,97 @@ public static class UnloadAmmoPatches scavController.Inventory.Stash = null; } } + + public class UnloadAmmoBoxPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(ItemUiContext), nameof(ItemUiContext.UnloadAmmo)); + } + + [PatchPrefix] + public static void Prefix(Item item) + { + if (item is AmmoBox) + { + UnloadState = new(); + } + } + + [PatchPostfix] + public static void Postfix() + { + UnloadState = null; + } + } + + public class QuickFindUnloadAmmoBoxPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(InteractionsHandlerClass), nameof(InteractionsHandlerClass.QuickFindAppropriatePlace)); + } + + [PatchPrefix] + public static void Prefix(Item item, TraderControllerClass controller, ref IEnumerable targets, ref InteractionsHandlerClass.EMoveItemOrder order) + { + if (UnloadState == null) + { + return; + } + + AmmoBox box = item.Parent.Container.ParentItem as AmmoBox; + if (box == null) + { + return; + } + + // Ammo boxes with multiple stacks will loop through this code, so we only want to move the box once + if (UnloadState.initialized) + { + order = UnloadState.order; + targets = UnloadState.targets; + } + else + { + // Have to do this for them, since the calls to get parent will be wrong once we move the box + if (!order.HasFlag(InteractionsHandlerClass.EMoveItemOrder.IgnoreItemParent)) + { + LootItemClass parent = (item.GetNotMergedParent() as LootItemClass) ?? (item.GetRootMergedItem() as EquipmentClass); + if (parent != null) + { + UnloadState.targets = targets = order.HasFlag(InteractionsHandlerClass.EMoveItemOrder.PrioritizeParent) ? + parent.ToEnumerable().Concat(targets).Distinct() : + targets.Concat(parent.ToEnumerable()).Distinct(); + } + + UnloadState.order = order |= InteractionsHandlerClass.EMoveItemOrder.IgnoreItemParent; + } + + var operation = InteractionsHandlerClass.Move(box, UnloadState.fakeStash.Grid.FindLocationForItem(box), controller, false); + operation.Value.RaiseEvents(controller, CommandStatus.Begin); + operation.Value.RaiseEvents(controller, CommandStatus.Succeed); + + UnloadState.initialized = true; + } + } + } + + public class UnloadAmmoBoxState + { + public StashClass fakeStash; + public TraderControllerClass fakeController; + + public bool initialized; + public InteractionsHandlerClass.EMoveItemOrder order; + public IEnumerable targets; + + public UnloadAmmoBoxState() + { + fakeStash = (StashClass)Singleton.Instance.CreateItem("FakeStash", "566abbc34bdc2d92178b4576", null); + + var profile = PatchConstants.BackEndSession.Profile; + fakeController = new(fakeStash, profile.ProfileId, profile.Nickname); + } + } } diff --git a/Patches/UnlockCursorPatch.cs b/Patches/UnlockCursorPatch.cs new file mode 100644 index 0000000..fc67210 --- /dev/null +++ b/Patches/UnlockCursorPatch.cs @@ -0,0 +1,33 @@ +using HarmonyLib; +using SPT.Reflection.Patching; +using System; +using System.Linq; +using System.Reflection; +using UnityEngine; + +namespace UIFixes; + +public class UnlockCursorPatch : ModulePatch +{ + private static readonly FullScreenMode[] WindowedModes = [FullScreenMode.Windowed, FullScreenMode.MaximizedWindow, FullScreenMode.FullScreenWindow]; + + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(CursorManager), nameof(CursorManager.SetCursorLockMode)); + } + + [PatchPrefix] + public static bool Prefix(bool cursorVisible, FullScreenMode fullscreenMode, Action ___action_0) + { + Cursor.lockState = cursorVisible ? + Settings.UnlockCursor.Value && WindowedModes.Contains(fullscreenMode) ? CursorLockMode.None : CursorLockMode.Confined : + CursorLockMode.Locked; + + if (___action_0 != null) + { + ___action_0(); + } + + return false; + } +} \ No newline at end of file diff --git a/Plugin.cs b/Plugin.cs index 01d0270..5072daa 100644 --- a/Plugin.cs +++ b/Plugin.cs @@ -47,7 +47,7 @@ public class Plugin : BaseUnityPlugin new LoadMagPresetsPatch().Enable(); KeepWindowsOnScreenPatches.Enable(); ContextMenuShortcutPatches.Enable(); - new OpenSortingTablePatch().Enable(); + OpenSortingTablePatches.Enable(); LoadAmmoInRaidPatches.Enable(); MultiSelectPatches.Enable(); new FixUnloadLastBulletPatch().Enable(); @@ -65,6 +65,9 @@ public class Plugin : BaseUnityPlugin MoveSortingTablePatches.Enable(); FilterOutOfStockPatches.Enable(); SortPatches.Enable(); + ReloadInPlacePatches.Enable(); + BarterOfferPatches.Enable(); + new UnlockCursorPatch().Enable(); } public static bool InRaid() diff --git a/README.md b/README.md index 29ba7d5..db4c2ad 100644 --- a/README.md +++ b/README.md @@ -28,12 +28,16 @@ Existing SPT features made better - Rebind Home/End, PageUp/PageDown to work like you would expect - Customizable mouse scrolling speed +- Moving stacks into containers always moves entire stack +- ✨ Items made stackable by other mods follow normal stacking behavior - Allow found in raid money and ammo automatically stack with non-found-in-raid items - Synchronize stash scroll position everywhere your stash is visible - Insure and repair items directly from the context menu - Load ammo via context menu _in raid_ - Load ammo preset will pull ammo from inventory, not just stash - ✨ Multi-grid vest and backpack grids reordered to be left to right, top to bottom. +- ✨ Sorting will stack and combine stacks of items +- ✨ Shift-clicking sort will only sort loose items, leaving containers in place #### Inspect windows @@ -57,6 +61,8 @@ Existing SPT features made better - Option to keep the Add Offer window open after placing your offer - Set prices in the Add Offer window by clicking the min/avg/max market prices (multiplies for bulk orders) - Autoselect Similar checkbox is remembered across sessions and application restarts +- ✨ Replace barter offers icons with actual item images, plus owned/required counts on expansion +- ✨ Clears filters for you when you type in search bar and there's no match #### Weapon modding/presets @@ -70,6 +76,7 @@ Existing SPT features made better #### In raid +- ✨ Reloading will swap magazines in-place, instead of dropping them on the ground when there's no room. - ✨ Grenade quickbinds will transfer to the next grenade of the same type after throwing. - ✨ Option to change the behavior of the grenade key from selecting a random grenade to a deterministic one diff --git a/Settings.cs b/Settings.cs index f5ebf05..7d1cc59 100644 --- a/Settings.cs +++ b/Settings.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Linq; using UnityEngine; namespace UIFixes; @@ -47,6 +48,7 @@ internal class Settings private const string FleaMarketSection = "6. Flea Market"; // General + public static ConfigEntry UnlockCursor { get; set; } public static ConfigEntry ShowPresetConfirmations { get; set; } public static ConfigEntry ShowTransferConfirmations { get; set; } public static ConfigEntry KeepMessagesOpen { get; set; } @@ -62,6 +64,7 @@ internal class Settings public static ConfigEntry MouseScrollMulti { get; set; } public static ConfigEntry InspectKeyBind { get; set; } public static ConfigEntry OpenKeyBind { get; set; } + public static ConfigEntry ExamineKeyBind { get; set; } public static ConfigEntry TopUpKeyBind { get; set; } public static ConfigEntry UseKeyBind { get; set; } public static ConfigEntry UseAllKeyBind { get; set; } @@ -69,6 +72,7 @@ internal class Settings public static ConfigEntry UnpackKeyBind { get; set; } public static ConfigEntry FilterByKeyBind { get; set; } public static ConfigEntry LinkedSearchKeyBind { get; set; } + public static ConfigEntry SortingTableKeyBind { get; set; } public static ConfigEntry UseRaidMouseScrollMulti { get; set; } // Advanced public static ConfigEntry MouseScrollMultiInRaid { get; set; } // Advanced public static ConfigEntry ItemContextBlocksTextInputs { get; set; } // Advanced @@ -81,6 +85,8 @@ internal class Settings public static ConfigEntry MultiSelectStrat { get; set; } public static ConfigEntry ShowMultiSelectDebug { get; set; } // Advanced public static ConfigEntry SwapItems { get; set; } + public static ConfigEntry SwapMags { get; set; } + public static ConfigEntry AlwaysSwapMags { get; set; } public static ConfigEntry SwapImpossibleContainers { get; set; } public static ConfigEntry ReorderGrids { get; set; } public static ConfigEntry SynchronizeStashScrolling { get; set; } @@ -90,6 +96,7 @@ internal class Settings public static ConfigEntry MergeFIRAmmo { get; set; } public static ConfigEntry MergeFIROther { get; set; } public static ConfigEntry AutoOpenSortingTable { get; set; } + public static ConfigEntry DefaultSortingTableBind { get; set; } // Advanced public static ConfigEntry ContextMenuOnRight { get; set; } public static ConfigEntry ShowGPCurrency { get; set; } public static ConfigEntry ShowOutOfStockCheckbox { get; set; } @@ -113,9 +120,11 @@ internal class Settings // Flea Market public static ConfigEntry EnableFleaHistory { get; set; } + public static ConfigEntry ShowBarterIcons { get; set; } public static ConfigEntry EnableSlotSearch { get; set; } public static ConfigEntry ShowRequiredQuest { get; set; } public static ConfigEntry AutoExpandCategories { get; set; } + public static ConfigEntry ClearFiltersOnSearch { get; set; } public static ConfigEntry KeepAddOfferOpen { get; set; } public static ConfigEntry PurchaseAllKeybind { get; set; } public static ConfigEntry KeepAddOfferOpenIgnoreMaxOffers { get; set; } // Advanced @@ -126,6 +135,15 @@ internal class Settings var configEntries = new List(); // General + configEntries.Add(UnlockCursor = config.Bind( + GeneralSection, + "Unlock Cursor", + true, + new ConfigDescription( + "Unlock cursor in Windowed, Maximized Windowed, and FullScreen Windowed modes. Note that you must alt-tab out of the game and back in for this to take affect.", + null, + new ConfigurationManagerAttributes { }))); + configEntries.Add(ShowPresetConfirmations = config.Bind( GeneralSection, "Show Weapon Preset Confirmation Dialog", @@ -262,6 +280,15 @@ internal class Settings null, new ConfigurationManagerAttributes { }))); + configEntries.Add(ExamineKeyBind = config.Bind( + InputSection, + "Examine/Interact Shortcut", + new KeyboardShortcut(KeyCode.None), + new ConfigDescription( + "Keybind to examine an item, fold it, unfold it, turn it on, turn it off, or check a magazine", + null, + new ConfigurationManagerAttributes { }))); + configEntries.Add(TopUpKeyBind = config.Bind( InputSection, "Top Up Ammo Shortcut", @@ -325,6 +352,15 @@ internal class Settings null, new ConfigurationManagerAttributes { }))); + configEntries.Add(SortingTableKeyBind = config.Bind( + InputSection, + "Transfer to/from Sorting Table", + new KeyboardShortcut(KeyCode.None), + new ConfigDescription( + "Keybind to transfer items to and from the sorting table. Will auto-open sorting table if necessary.", + null, + new ConfigurationManagerAttributes { }))); + configEntries.Add(ItemContextBlocksTextInputs = config.Bind( InputSection, "Block Text Inputs on Item Mouseover", @@ -398,6 +434,24 @@ internal class Settings null, new ConfigurationManagerAttributes { }))); + configEntries.Add(SwapMags = config.Bind( + InventorySection, + "Reload Magazines In-Place", + true, + new ConfigDescription( + "When reloading a weapon with a magazine, swap locations with the new magazine if necessary (and possible)", + null, + new ConfigurationManagerAttributes { }))); + + configEntries.Add(AlwaysSwapMags = config.Bind( + InventorySection, + "Always Reload Magazines In-Place", + false, + new ConfigDescription( + "Always reload magazines in-place, even if there's space not to. Note that in-place reloads are slower.", + null, + new ConfigurationManagerAttributes { }))); + configEntries.Add(SwapImpossibleContainers = config.Bind( InventorySection, "Swap with Incompatible Containers", @@ -479,6 +533,15 @@ internal class Settings null, new ConfigurationManagerAttributes { }))); + configEntries.Add(DefaultSortingTableBind = config.Bind( + InventorySection, + "Shift-Click to Sorting Table", + true, + new ConfigDescription( + "This setting lets you enable/disable the default Tarkov behavior of shift-clicking items to transfer them to the sorting table.", + null, + new ConfigurationManagerAttributes { IsAdvanced = true }))); + configEntries.Add(ContextMenuOnRight = config.Bind( InventorySection, "Context Menu Flyout on Right", @@ -635,6 +698,15 @@ internal class Settings null, new ConfigurationManagerAttributes { }))); + configEntries.Add(ShowBarterIcons = config.Bind( + FleaMarketSection, + "Show Barter Icons", + true, + new ConfigDescription( + "Show item icons for barters instead of the generic barter icon", + null, + new ConfigurationManagerAttributes { }))); + configEntries.Add(EnableSlotSearch = config.Bind( FleaMarketSection, "Enable Linked Slot Search", @@ -653,6 +725,15 @@ internal class Settings null, new ConfigurationManagerAttributes { }))); + configEntries.Add(ClearFiltersOnSearch = config.Bind( + FleaMarketSection, + "Clear Filters on Search", + true, + new ConfigDescription( + "Pressing Enter after typing in the flea search bar will clear non-default filters", + null, + new ConfigurationManagerAttributes { }))); + configEntries.Add(ShowRequiredQuest = config.Bind( FleaMarketSection, "Show Required Quest for Locked Offers", @@ -793,10 +874,25 @@ public static class SettingExtensions configEntry.SettingChanged += (_, _) => onChange(configEntry.Value); } - public static void Bind(this ConfigEntry configEntry, Action onChange) { configEntry.Subscribe(onChange); onChange(configEntry.Value); } + + // KeyboardShortcut methods return false if any other key is down + public static bool IsDownIgnoreOthers(this KeyboardShortcut shortcut) + { + return Input.GetKeyDown(shortcut.MainKey) && shortcut.Modifiers.All(Input.GetKey); + } + + public static bool IsPressedIgnoreOthers(this KeyboardShortcut shortcut) + { + return Input.GetKey(shortcut.MainKey) && shortcut.Modifiers.All(Input.GetKey); + } + + public static bool IsUpIgnoreOthers(this KeyboardShortcut shortcut) + { + return Input.GetKeyUp(shortcut.MainKey) && shortcut.Modifiers.All(Input.GetKey); + } } diff --git a/Sorter.cs b/Sorter.cs index 5a56179..9d4f872 100644 --- a/Sorter.cs +++ b/Sorter.cs @@ -106,4 +106,30 @@ public static class Sorter return operation; } + + // Recreation of InteractionsHandlerClass.smethod_0, but without the out type being Stackable. + // minimumStackSpace of 0 means complete merge only, i.e. mininumStackSpace = itemToMerge.StackObjectCount + public static bool FindStackForMerge(IEnumerable containers, Item itemToMerge, out Item mergeableItem, int minimumStackSpace = 0) + { + if (minimumStackSpace <= 0) + { + minimumStackSpace = itemToMerge.StackObjectsCount; + } + + bool ignoreSpawnedInSession = itemToMerge.Template switch + { + MoneyClass _ => Settings.MergeFIRMoney.Value, + AmmoTemplate _ => Settings.MergeFIRMoney.Value, + _ => Settings.MergeFIROther.Value, + }; + + mergeableItem = containers.SelectMany(x => x.Items) + .Where(x => x != itemToMerge) + .Where(x => x.TemplateId == itemToMerge.TemplateId) + .Where(x => ignoreSpawnedInSession || x.SpawnedInSession == itemToMerge.SpawnedInSession) + .Where(x => x.StackObjectsCount < x.StackMaxSize) + .FirstOrDefault(x => minimumStackSpace <= x.StackMaxSize - x.StackObjectsCount); + + return mergeableItem != null; + } } \ No newline at end of file diff --git a/UIFixes.csproj b/UIFixes.csproj index dd139ca..e179f13 100644 --- a/UIFixes.csproj +++ b/UIFixes.csproj @@ -4,7 +4,7 @@ net471 Tyfon.UIFixes SPT UI Fixes - 2.2.2 + 2.3.1 true latest Debug;Release @@ -104,8 +104,7 @@ xcopy /F /Y "$(TargetPath)" "$(ProjectDir)dist\BepInEx\plugins\" xcopy /F /Y /S "$(ProjectDir)dist\BepInEx" "$(ProjectDir)$(PathToSPT)\BepInEx" xcopy /F /Y /S "$(ProjectDir)dist\user" "$(ProjectDir)$(PathToSPT)\user" - 7z a -t7z $(TargetName.Replace(".", "-"))-$(Version).7z $(ProjectDir)dist\BepInEx $(ProjectDir)dist\user - move /Y $(TargetName.Replace(".", "-"))-$(Version).7z dist\ + 7z a -tzip dist\$(TargetName.Replace(".", "-"))-$(Version).zip $(ProjectDir)dist\BepInEx $(ProjectDir)dist\user )' /> diff --git a/server/build.mjs b/server/build.mjs index b4b5a58..123127b 100644 --- a/server/build.mjs +++ b/server/build.mjs @@ -38,7 +38,7 @@ import ignore from "ignore"; import archiver from "archiver"; import winston from "winston"; -const sptPath = "/SPT/3.9.2-debug"; +const sptPath = "/SPT/3.9.3-debug"; // Get the command line arguments to determine whether to use verbose logging. const args = process.argv.slice(2); diff --git a/server/package.json b/server/package.json index 7c1964d..c5a5469 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "uifixes", - "version": "2.2.2", + "version": "2.3.1", "main": "src/mod.js", "license": "MIT", "author": "Tyfon", diff --git a/server/src/mod.ts b/server/src/mod.ts index c46acfb..ce41278 100644 --- a/server/src/mod.ts +++ b/server/src/mod.ts @@ -13,6 +13,8 @@ import type { ICloner } from "@spt/utils/cloners/ICloner"; import { RagfairLinkedSlotItemService } from "./RagfairLinkedSlotItemService"; import config from "../config/config.json"; +import { RagfairOfferGenerator } from "@spt/generators/RagfairOfferGenerator"; +import { IRagfairOffer } from "@spt/models/eft/ragfair/IRagfairOffer"; class UIFixes implements IPreSptLoadMod { private databaseService: DatabaseService; @@ -50,6 +52,36 @@ class UIFixes implements IPreSptLoadMod { { frequency: "Always" } ); + // Trader offers with dogtag barter - fixed in next SPT release *after* 3.9.3 + container.afterResolution( + "RagfairOfferGenerator", + (_, ragfairOfferGenerator: RagfairOfferGenerator) => { + const original = ragfairOfferGenerator["createOffer"]; // By name because protected + + ragfairOfferGenerator["createOffer"] = (userID, time, items, barterScheme, loyalLevel, isPackOffer) => { + const offer: IRagfairOffer = original.call( + ragfairOfferGenerator, + userID, + time, + items, + barterScheme, + loyalLevel, + isPackOffer + ); + + for (let i = 0; i < offer.requirements.length; i++) { + if (barterScheme[i]["level"] !== undefined) { + offer.requirements[i]["level"] = barterScheme[i]["level"]; + offer.requirements[i]["side"] = barterScheme[i]["side"]; + } + } + + return offer; + }; + }, + { frequency: "Always" } + ); + // Better tool return - starting production if (config.putToolsBack) { container.afterResolution( @@ -197,4 +229,4 @@ class UIFixes implements IPreSptLoadMod { } } -module.exports = { mod: new UIFixes() }; +export const mod = new UIFixes(); diff --git a/server/tsconfig.json b/server/tsconfig.json index 9ac1a11..16b6a3a 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -1,9 +1,9 @@ { "compilerOptions": { "allowJs": true, - "module": "CommonJS", + "module": "NodeNext", "target": "ES2022", - "moduleResolution": "Node10", + "moduleResolution": "NodeNext", "esModuleInterop": true, "downlevelIteration": true, "experimentalDecorators": true,