diff --git a/Patches/InsureContextMenuPatches.cs b/Patches/InsureContextMenuPatches.cs new file mode 100644 index 0000000..64c5431 --- /dev/null +++ b/Patches/InsureContextMenuPatches.cs @@ -0,0 +1,300 @@ +using Aki.Reflection.Patching; +using Aki.Reflection.Utils; +using Comfort.Common; +using EFT.InventoryLogic; +using EFT.UI; +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace UIFixes +{ + public static class InsureContextMenuPatches + { + private static Type InventoryRootInteractionsType; + private static Type TradingRootInteractionsType; + private static FieldInfo TradingRootInteractionsItemField; + + private static int PlayerRubles; + + private static InsuranceInteractions CurrentInsuranceInteractions = null; + private static string CreatedContextMenuButtonTraderId = null; + + private static readonly HashSet TradingRootInteractions = + [ + EItemInfoButton.Inspect, + EItemInfoButton.Uninstall, + EItemInfoButton.Examine, + EItemInfoButton.Open, + EItemInfoButton.Insure, + EItemInfoButton.Repair, + EItemInfoButton.Modding, + EItemInfoButton.EditBuild, + EItemInfoButton.FilterSearch, + EItemInfoButton.LinkedSearch, + EItemInfoButton.NeededSearch, + EItemInfoButton.Tag, + EItemInfoButton.ResetTag, + EItemInfoButton.TurnOn, + EItemInfoButton.TurnOff, + EItemInfoButton.Fold, + EItemInfoButton.Unfold, + EItemInfoButton.Disassemble, + EItemInfoButton.Discard + ]; + + public static void Enable() + { + // The context menus in the inventory and the trading screen inventory are *completely different code* + InventoryRootInteractionsType = PatchConstants.EftTypes.First(t => t.GetField("HIDEOUT_WEAPON_MODIFICATION_REQUIRED") != null); // GClass3023 + + // GClass3032 - this is nuts to find, have to inspect a static enum array + TradingRootInteractionsType = PatchConstants.EftTypes.First(t => + { + var enumerableField = t.GetField("ienumerable_2", BindingFlags.NonPublic | BindingFlags.Static); + if (enumerableField != null) + { + var enumerable = (IEnumerable)enumerableField.GetValue(null); + return TradingRootInteractions.SetEquals(enumerable); + } + + return false; + }); + TradingRootInteractionsItemField = AccessTools.Field(TradingRootInteractionsType, "item_0"); + + new DeclareSubInteractionsInventoryPatch().Enable(); + new CreateSubInteractionsInventoryPatch().Enable(); + + new DeclareSubInteractionsTradingPatch().Enable(); + new CreateSubInteractionsTradingPatch().Enable(); + + new SniffInteractionButtonCreationPatch().Enable(); + new ChangeInteractionButtonCreationPatch().Enable(); + } + + public class DeclareSubInteractionsInventoryPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(InventoryRootInteractionsType, "get_SubInteractions"); + } + + [PatchPostfix] + public static void Postfix(ref IEnumerable __result) + { + __result = __result.Append(EItemInfoButton.Insure); + } + } + + public class CreateSubInteractionsInventoryPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(InventoryRootInteractionsType, "CreateSubInteractions"); + } + + [PatchPrefix] + public static bool Prefix(EItemInfoButton parentInteraction, ISubInteractions subInteractionsWrapper, Item ___item_0, ItemUiContext ___itemUiContext_1) + { + if (parentInteraction == EItemInfoButton.Insure) + { + Dictionary playerCurrencies = R.Money.GetMoneySums(___itemUiContext_1.R().InventoryController.Inventory.Stash.Grid.ContainedItems.Keys); + PlayerRubles = playerCurrencies[ECurrencyType.RUB]; + + CurrentInsuranceInteractions = new(___item_0, ___itemUiContext_1); + CurrentInsuranceInteractions.LoadAsync(() => subInteractionsWrapper.SetSubInteractions(CurrentInsuranceInteractions)); + + return false; + } + + return true; + } + } + + public class DeclareSubInteractionsTradingPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(TradingRootInteractionsType, "get_SubInteractions"); + } + + [PatchPostfix] + public static void Postfix(ref IEnumerable __result) + { + __result = __result.Append(EItemInfoButton.Insure); + } + } + + public class CreateSubInteractionsTradingPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(TradingRootInteractionsType, "CreateSubInteractions"); + } + + [PatchPrefix] + public static bool Prefix(object __instance, EItemInfoButton parentInteraction, ISubInteractions subInteractionsWrapper, ItemUiContext ___itemUiContext_0) + { + if (parentInteraction == EItemInfoButton.Insure) + { + Dictionary playerCurrencies = R.Money.GetMoneySums(___itemUiContext_0.R().InventoryController.Inventory.Stash.Grid.ContainedItems.Keys); + PlayerRubles = playerCurrencies[ECurrencyType.RUB]; + + // CreateSubInteractions is only on the base class here, which doesn't have an Item. But __instance is actually a GClass3032 + Item item = (Item)TradingRootInteractionsItemField.GetValue(__instance); + + CurrentInsuranceInteractions = new(item, ___itemUiContext_0); + CurrentInsuranceInteractions.LoadAsync(() => subInteractionsWrapper.SetSubInteractions(CurrentInsuranceInteractions)); + + return false; + } + + return true; + } + } + + public class SniffInteractionButtonCreationPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(InteractionButtonsContainer), nameof(InteractionButtonsContainer.method_3)); + } + + [PatchPrefix] + public static void Prefix(DynamicInteractionClass interaction) + { + if (interaction.IsInsuranceInteraction()) + { + CreatedContextMenuButtonTraderId = interaction.GetTraderId(); + } + } + + [PatchPostfix] + public static void Postfix() + { + CreatedContextMenuButtonTraderId = null; + } + } + + public class ChangeInteractionButtonCreationPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(InteractionButtonsContainer), nameof(InteractionButtonsContainer.method_5)); + } + + [PatchPrefix] + public static void Prefix(SimpleContextMenuButton button) + { + if (!String.IsNullOrEmpty(CreatedContextMenuButtonTraderId) && CurrentInsuranceInteractions != null) + { + button.SetButtonInteraction(CurrentInsuranceInteractions.GetButtonInteraction(CreatedContextMenuButtonTraderId)); + } + } + } + + public class CleanUpInteractionsPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(SimpleContextMenu), nameof(SimpleContextMenu.Close)); + } + + [PatchPostfix] + public static void Postfix() + { + CurrentInsuranceInteractions = null; + } + } + + public class InsuranceInteractions(Item item, ItemUiContext uiContext) : ItemInfoInteractionsAbstractClass(uiContext) + { + private readonly InsuranceCompanyClass insurance = uiContext.Session.InsuranceCompany; + private readonly Item item = item; + private List items; + private readonly Dictionary prices = []; + + public void LoadAsync(Action callback) + { + ItemClass itemClass = ItemClass.FindOrCreate(item); + items = insurance.GetItemChildren(itemClass).Flatten(insurance.GetItemChildren).Concat([itemClass]) + .Where(i => insurance.ItemTypeAvailableForInsurance(i) && !insurance.InsuredItems.Contains(i)) + .ToList(); + + insurance.GetInsurePriceAsync(items, _ => + { + foreach (var insurer in insurance.Insurers) + { + int price = this.items.Select(i => insurance.InsureSummary[insurer.Id][i]).Where(s => s.Loaded).Sum(s => s.Amount); + prices[insurer.Id] = price; + + string priceColor = price > PlayerRubles ? "#FF0000" : "#ADB8BC"; + + string text = string.Format("{0} ({2} ₽)", insurer.LocalizedName, priceColor, price); + + base.method_2(MakeInteractionId(insurer.Id), text, () => this.Insure(insurer.Id)); + } + + callback(); + }); + } + + public void Insure(string insurerId) + { + insurance.SelectedInsurerId = insurerId; + insurance.InsureItems(this.items, result => { }); + } + + public IResult GetButtonInteraction(string traderId) + { + if (prices[traderId] > PlayerRubles) + { + return new FailedResult("ragfair/Not enough money", 0); + } + + return SuccessfulResult.New; + } + + public override void ExecuteInteractionInternal(EInsurers interaction) + { + } + + public override bool IsActive(EInsurers button) + { + return button == EInsurers.None && !this.insurance.Insurers.Any(); + } + + public override IResult IsInteractive(EInsurers button) + { + return new FailedResult("No insurers??", 0); + } + + public override bool HasIcons + { + get { return false; } + } + + public enum EInsurers + { + None + } + } + + private static string MakeInteractionId(string traderId) + { + return "UIFixesInsurerId:" + traderId; + } + + private static bool IsInsuranceInteraction(this DynamicInteractionClass interaction) + { + return interaction.Id.StartsWith("UIFixesInsurerId:"); + } + + private static string GetTraderId(this DynamicInteractionClass interaction) + { + return interaction.Id.Split(':')[1]; + } + } +} diff --git a/Patches/TradingAutoSwitchPatches.cs b/Patches/TradingAutoSwitchPatches.cs new file mode 100644 index 0000000..be4d435 --- /dev/null +++ b/Patches/TradingAutoSwitchPatches.cs @@ -0,0 +1,101 @@ +using Aki.Reflection.Patching; +using Comfort.Common; +using EFT.UI; +using EFT.UI.DragAndDrop; +using HarmonyLib; +using System.Reflection; +using UnityEngine; +using UnityEngine.EventSystems; + +namespace UIFixes +{ + public static class TradingAutoSwitchPatches + { + private static Tab BuyTab; + private static Tab SellTab; + + public static void Enable() + { + new GetTraderScreensGroupPatch().Enable(); + new SwitchOnClickPatch().Enable(); + } + + public class GetTraderScreensGroupPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(TraderScreensGroup), nameof(TraderScreensGroup.Show)); + } + + [PatchPostfix] + public static void Postfix(TraderScreensGroup __instance) + { + var wrappedInstance = __instance.R(); + + BuyTab = wrappedInstance.BuyTab; + SellTab = wrappedInstance.SellTab; + + wrappedInstance.AddDisposable(() => + { + BuyTab = null; + SellTab = null; + }); + } + } + + public class SwitchOnClickPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(TradingItemView), nameof(TradingItemView.OnClick)); + } + + // Basically reimplementing this method for the two cases I want to handle + // Key difference being NOT to check the current trading mode, and to call switch at the end + // Have to call switch *after*, because it completely rebuilds the entire player-side grid + [PatchPrefix] + public static bool Prefix( + TradingItemView __instance, + PointerEventData.InputButton button, + bool doubleClick, + ETradingItemViewType ___etradingItemViewType_0, bool ___bool_8) + { + if (!Settings.AutoSwitchTrading.Value) + { + return true; + } + + var tradingItemView = __instance.R(); + if (button != PointerEventData.InputButton.Left || ___etradingItemViewType_0 == ETradingItemViewType.TradingTable) + { + return true; + } + + bool ctrlPressed = Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl); + + if (!ctrlPressed && doubleClick) + { + return true; + } + + if (!___bool_8 && ctrlPressed && tradingItemView.TraderAssortmentControler.QuickFindTradingAppropriatePlace(__instance.Item, null)) + { + __instance.ItemContext.CloseDependentWindows(); + __instance.HideTooltip(); + Singleton.Instance.PlayItemSound(__instance.Item.ItemSound, EInventorySoundType.pickup, false); + SellTab.OnPointerClick(null); + return false; + } + + if (___bool_8) + { + tradingItemView.TraderAssortmentControler.SelectItem(__instance.Item); + BuyTab.OnPointerClick(null); + return false; + } + + return true; + } + } + } +} diff --git a/Plugin.cs b/Plugin.cs index 8bff0b8..549e9db 100644 --- a/Plugin.cs +++ b/Plugin.cs @@ -37,6 +37,8 @@ namespace UIFixes AddOfferClickablePricesPatches.Enable(); new AssortUnlocksPatch().Enable(); new AutofillQuestItemsPatch().Enable(); + InsureContextMenuPatches.Enable(); + TradingAutoSwitchPatches.Enable(); } public static bool InRaid() diff --git a/R.cs b/R.cs index 2660807..5d48d2b 100644 --- a/R.cs +++ b/R.cs @@ -45,6 +45,10 @@ namespace UIFixes QuestCache.InitTypes(); ItemMarketPricesPanel.InitTypes(); AddOfferWindow.InitTypes(); + ItemUiContext.InitTypes(); + Money.InitTypes(); + TraderScreensGroup.InitTypes(); + TradingItemView.InitTypes(); } public abstract class Wrapper(object value) @@ -447,6 +451,71 @@ namespace UIFixes public RagFairClass Ragfair { get { return (RagFairClass)RagfairField.GetValue(Value); } } } + + public class ItemUiContext(object value) : Wrapper(value) + { + public static Type Type { get; private set; } + private static FieldInfo InventoryControllerField; + + public static void InitTypes() + { + Type = typeof(EFT.UI.ItemUiContext); + InventoryControllerField = AccessTools.GetDeclaredFields(Type).First(t => t.FieldType == typeof(InventoryControllerClass)); + } + + public InventoryControllerClass InventoryController { get { return (InventoryControllerClass)InventoryControllerField.GetValue(Value); } } + } + + public static class Money + { + public static Type Type { get; private set; } + private static MethodInfo GetMoneySumsMethod; + + public static void InitTypes() + { + Type = PatchConstants.EftTypes.First(t => t.GetMethod("GetMoneySums", BindingFlags.Public | BindingFlags.Static) != null); + GetMoneySumsMethod = AccessTools.Method(Type, "GetMoneySums"); + } + + public static Dictionary GetMoneySums(IEnumerable items) => (Dictionary)GetMoneySumsMethod.Invoke(null, [items]); + } + + public class TraderScreensGroup(object value) : Wrapper(value) + { + public static Type Type { get; private set; } + private static FieldInfo UIField; + private static MethodInfo UIAddDisposableMethod; + private static FieldInfo BuyTabField; + private static FieldInfo SellTabField; + + + public static void InitTypes() + { + Type = typeof(EFT.UI.TraderScreensGroup); + UIField = AccessTools.Field(Type, "UI"); + UIAddDisposableMethod = AccessTools.Method(UIField.FieldType, "AddDisposable", [typeof(Action)]); + BuyTabField = AccessTools.Field(Type, "_buyTab"); + SellTabField = AccessTools.Field(Type, "_sellTab"); + } + + public object UI { get { return UIField.GetValue(Value); } } + public void AddDisposable(Action action) => UIAddDisposableMethod.Invoke(UI, [action]); + public Tab BuyTab { get { return (Tab)BuyTabField.GetValue(Value); } } + public Tab SellTab { get { return (Tab)SellTabField.GetValue(Value); } } + } + + public class TradingItemView(object value) : Wrapper(value) + { + public static Type Type { get; private set; } + private static FieldInfo TraderAssortmentControllerField; + public static void InitTypes() + { + Type = typeof(EFT.UI.DragAndDrop.TradingItemView); + TraderAssortmentControllerField = AccessTools.GetDeclaredFields(Type).First(t => t.FieldType == typeof(TraderAssortmentControllerClass)); + } + + public TraderAssortmentControllerClass TraderAssortmentControler { get { return (TraderAssortmentControllerClass)TraderAssortmentControllerField.GetValue(Value); } } + } } public static class RExtentensions @@ -464,5 +533,8 @@ namespace UIFixes public static R.FiltersPanel R(this FiltersPanel value) => new(value); public static R.ItemMarketPricesPanel R(this ItemMarketPricesPanel value) => new(value); public static R.AddOfferWindow R(this AddOfferWindow value) => new(value); + public static R.ItemUiContext R(this ItemUiContext value) => new(value); + public static R.TraderScreensGroup R(this TraderScreensGroup value) => new(value); + public static R.TradingItemView R(this TradingItemView value) => new(value); } } diff --git a/Settings.cs b/Settings.cs index be05e48..f858d8e 100644 --- a/Settings.cs +++ b/Settings.cs @@ -32,6 +32,7 @@ namespace UIFixes public static ConfigEntry ShowPresetConfirmations { get; set; } public static ConfigEntry ShowTransferConfirmations { get; set; } public static ConfigEntry AutofillQuestTurnIns { get; set; } + public static ConfigEntry AutoSwitchTrading { get; set; } // Input public static ConfigEntry UseHomeEnd { get; set; } @@ -97,6 +98,15 @@ namespace UIFixes null, new ConfigurationManagerAttributes { }))); + configEntries.Add(AutoSwitchTrading = config.Bind( + GeneralSection, + "Autoswitch Buy/Sell when Trading", + true, + new ConfigDescription( + "Click a trader's item, switch to buy mode. Control-click your item, switch to sell mode.", + null, + new ConfigurationManagerAttributes { }))); + // Input configEntries.Add(UseHomeEnd = config.Bind( InputSection,