diff --git a/Patches/AddOfferClickablePricesPatches.cs b/Patches/AddOfferClickablePricesPatches.cs deleted file mode 100644 index f4ab190..0000000 --- a/Patches/AddOfferClickablePricesPatches.cs +++ /dev/null @@ -1,97 +0,0 @@ -using EFT.UI.Ragfair; -using HarmonyLib; -using JetBrains.Annotations; -using SPT.Reflection.Patching; -using System.Linq; -using System.Reflection; -using TMPro; -using UnityEngine; -using UnityEngine.EventSystems; -using UnityEngine.UI; - -namespace UIFixes; - -public static class AddOfferClickablePricesPatches -{ - public static void Enable() - { - new AddButtonPatch().Enable(); - } - - public class AddButtonPatch : ModulePatch - { - protected override MethodBase GetTargetMethod() - { - return AccessTools.Method(typeof(AddOfferWindow), nameof(AddOfferWindow.Show)); - } - - [PatchPostfix] - public static void Postfix(AddOfferWindow __instance, ItemMarketPricesPanel ____pricesPanel, RequirementView[] ____requirementViews) - { - var panel = ____pricesPanel.R(); - - var rublesRequirement = ____requirementViews.First(rv => rv.name == "Requirement (RUB)"); - - Button lowestButton = panel.LowestLabel.GetOrAddComponent(); - lowestButton.onClick.AddListener(() => SetRequirement(__instance, rublesRequirement, ____pricesPanel.Minimum)); - ____pricesPanel.AddDisposable(lowestButton.onClick.RemoveAllListeners); - - Button averageButton = panel.AverageLabel.GetOrAddComponent(); - averageButton.onClick.AddListener(() => SetRequirement(__instance, rublesRequirement, ____pricesPanel.Average)); - ____pricesPanel.AddDisposable(averageButton.onClick.RemoveAllListeners); - - Button maximumButton = panel.MaximumLabel.GetOrAddComponent(); - maximumButton.onClick.AddListener(() => SetRequirement(__instance, rublesRequirement, ____pricesPanel.Maximum)); - ____pricesPanel.AddDisposable(maximumButton.onClick.RemoveAllListeners); - } - } - - private static void SetRequirement(AddOfferWindow window, RequirementView requirement, float price) - { - if (window.R().BulkOffer) - { - price *= window.Int32_0; // offer item count - } - - requirement.method_0(price.ToString("F0")); - } - - public class HighlightButton : Button - { - private Color originalColor; - bool originalOverrideColorTags; - - private TextMeshProUGUI _text; - private TextMeshProUGUI Text - { - get - { - if (_text == null) - { - _text = GetComponent(); - } - - return _text; - } - } - - public override void OnPointerEnter([NotNull] PointerEventData eventData) - { - base.OnPointerEnter(eventData); - - originalColor = Text.color; - originalOverrideColorTags = Text.overrideColorTags; - - Text.overrideColorTags = true; - Text.color = Color.white; - } - - public override void OnPointerExit([NotNull] PointerEventData eventData) - { - base.OnPointerExit(eventData); - - Text.overrideColorTags = originalOverrideColorTags; - Text.color = originalColor; - } - } -} diff --git a/Patches/AimToggleHoldPatches.cs b/Patches/AimToggleHoldPatches.cs deleted file mode 100644 index e8d3210..0000000 --- a/Patches/AimToggleHoldPatches.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Comfort.Common; -using EFT.InputSystem; -using HarmonyLib; -using SPT.Reflection.Patching; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; - -namespace UIFixes; - -public static class AimToggleHoldPatches -{ - public static void Enable() - { - new AddStatesPatch().Enable(); - new UpdateInputPatch().Enable(); - - Settings.ToggleOrHoldAim.SettingChanged += (_, _) => - { - // Will "save" control settings, running GClass1911.UpdateInput, which will set (or unset) toggle/hold behavior - Singleton.Instance.Control.Controller.method_3(); - }; - } - - public class AddStatesPatch : ModulePatch - { - private static FieldInfo StateMachineArray; - - protected override MethodBase GetTargetMethod() - { - StateMachineArray = AccessTools.Field(typeof(KeyCombination), "keyCombinationState_1"); - return AccessTools.GetDeclaredConstructors(typeof(ToggleKeyCombination)).Single(); - } - - [PatchPostfix] - public static void Postfix(ToggleKeyCombination __instance, EGameKey gameKey, ECommand disableCommand, KeyCombination.KeyCombinationState[] ___keyCombinationState_1) - { - if (!Settings.ToggleOrHoldAim.Value || gameKey != EGameKey.Aim) - { - return; - } - - List states = new(___keyCombinationState_1) - { - new ToggleHoldIdleState(__instance), - new ToggleHoldClickOrHoldState(__instance), - new ToggleHoldHoldState(__instance, disableCommand) - }; - - StateMachineArray.SetValue(__instance, states.ToArray()); - } - } - - public class UpdateInputPatch : ModulePatch - { - protected override MethodBase GetTargetMethod() - { - return AccessTools.Method(typeof(KeyCombination), nameof(KeyCombination.UpdateInput)); - } - - [PatchPostfix] - public static void Postfix(KeyCombination __instance) - { - if (!Settings.ToggleOrHoldAim.Value || __instance.GameKey != EGameKey.Aim) - { - return; - } - - __instance.method_0((KeyCombination.EKeyState)ToggleHoldState.Idle); - } - } -} diff --git a/Patches/UnloadAmmoPatches.cs b/Patches/UnloadAmmoPatches.cs deleted file mode 100644 index 118b798..0000000 --- a/Patches/UnloadAmmoPatches.cs +++ /dev/null @@ -1,200 +0,0 @@ -using Comfort.Common; -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; -using System.Reflection; -using System.Threading.Tasks; - -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 - { - protected override MethodBase GetTargetMethod() - { - return AccessTools.DeclaredProperty(R.TradingInteractions.Type, "AvailableInteractions").GetMethod; - } - - [PatchPostfix] - public static void Postfix(ref IEnumerable __result) - { - var list = __result.ToList(); - list.Insert(list.IndexOf(EItemInfoButton.Repair), EItemInfoButton.UnloadAmmo); - __result = list; - } - } - - public class TransferPlayerPatch : ModulePatch - { - protected override MethodBase GetTargetMethod() - { - return AccessTools.DeclaredProperty(R.TransferInteractions.Type, "AvailableInteractions").GetMethod; - } - - [PatchPostfix] - public static void Postfix(ref IEnumerable __result) - { - var list = __result.ToList(); - list.Insert(list.IndexOf(EItemInfoButton.Fold), EItemInfoButton.UnloadAmmo); - __result = list; - } - } - - // The scav inventory screen has two inventory controllers, the player's and the scav's. Unload always uses the player's, which causes issues - // because the bullets are never marked as "known" by the scav, so if you click back/next they show up as unsearched, with no way to search - // This patch forces unload to use the controller of whoever owns the magazine. - public class UnloadScavTransferPatch : ModulePatch - { - protected override MethodBase GetTargetMethod() - { - return AccessTools.DeclaredMethod(typeof(InventoryControllerClass), nameof(InventoryControllerClass.UnloadMagazine)); - } - - [PatchPrefix] - public static bool Prefix(InventoryControllerClass __instance, MagazineClass magazine, ref Task __result) - { - if (ItemUiContext.Instance.ContextType != EItemUiContextType.ScavengerInventoryScreen) - { - return true; - } - - if (magazine.Owner == __instance || magazine.Owner is not InventoryControllerClass ownerInventoryController) - { - return true; - } - - __result = ownerInventoryController.UnloadMagazine(magazine); - return false; - } - } - - // Because of the above patch, unload uses the scav's inventory controller, which provides locations to unload ammo: equipment and stash. Why do scavs have a stash? - // If the equipment is full, the bullets would go to the scav stash, aka a black hole, and are never seen again. - // Remove the scav's stash - public class NoScavStashPatch : ModulePatch - { - protected override MethodBase GetTargetMethod() - { - Type type = typeof(ScavengerInventoryScreen).GetNestedTypes().Single(t => t.GetField("ScavController") != null); // ScavengerInventoryScreen.GClass3156 - return AccessTools.GetDeclaredConstructors(type).Single(); - } - - [PatchPrefix] - public static void Prefix(InventoryContainerClass scavController) - { - 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/README.md b/README.md index db4c2ad..410b73a 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,12 @@ New UI features enabled by this mod - Ctrl-click and Alt-click to quick move or equip them all. Compatible with Quick Move to Containers! - Context menu to insure all, equip all, unequip all, unload ammo from all - Swap items in place - drag one item over another to swap their locations! +- ✨ Add offer to the flea market from an item's context menu - Flea market history - press the new back button to go back to the previous search -- ✨ Linked flea search from empty slots - find mods that fit that specific slot +- Linked flea search from empty slots - find mods that fit that specific slot - Keybinds for most context menu actions -- ✨ Toggle/Hold Aiming - tap to toggle ADS, or hold to ADS and stop when you release +- Toggle/Hold input - tap a key for "Press" mechanics, hold the key for "Continuous" mechanics + - Can be set for aiming, sprinting, tactical devices, headlights, and goggles/faceshields ## Improved features @@ -26,25 +28,28 @@ Existing SPT features made better #### Inventory +- ✨ Modify equipped weapons - 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 +- 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 +- 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 +- ✨ Open->All context flyout that will recursively open nested containers to get at that innermost bag +- ✨ Add/Remove from wishlist everywhere #### Inspect windows - Show the total stats (including sub-mods) when inspecting mods (optional, toggleable _in_ the inspect pane with a new button) - See stats change as you add/remove mods, with color-coded deltas - Remember last window size when you change it, with restore button to resize to default -- Move left and move right buttons + keybinds to quickly snap inspect windows to the left or right half of the screen, for easy comparisons. +- Move left and move right buttons + keybinds to quickly snap inspect windows to the left or right half of the screen, for easy comparisons - Auto-expand descriptions when possible (great for showing extra text from mods like Item Info) - Quickbinds will not be removed from items you keep when you die @@ -61,11 +66,12 @@ 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 +- 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 +- ✨ Weapons can grow left or up, not just right and down - Enable zooming with mousewheel - Skip needless unsaved changes warnings when not actually closing the screen @@ -76,9 +82,11 @@ 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 +- ✨ Quickbind tactical devices to control them individually +- ✨ Option to make unequipped weapons moddable in raid, optionally with multitool +- 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 #### Mail @@ -110,3 +118,28 @@ Fixing bugs that BSG won't or can't - Skips "You can return to this later" warnings when not transferring all items - "Receive All" button no longer shows up when there is nothing to receive + +## Interop + +UI Fixes offers interop with other mods that want to use the multi-select functionality, _without_ taking a hard dependency on `Tyfon.UIFixes.dll`. + +To do this, simply download and add [MultiSelectInterop.cs](src/Multiselect/MultiSelectInterop.cs) to your client project. It will take care of testing if UI Fixes is present and, using reflection, interoping with the mod. + +MultiSelectInterop exposes a small static surface to give you access to the multi-selection. + +```cs +public static class MultiSelect +{ + // Returns the number of items in the current selection + public static int Count { get; } + + // Returns the items in the current selection + public static IEnumerable Items { get; } + + // Executes an operation on each item in the selection, sequentially + // Passing an ItemUiContext is optional as it will use ItemUiContext.Instance if needed + // The second overload takes an async operation and returns a task representing the aggregate. + public static void Apply(Action action, ItemUiContext context = null); + public static Task Apply(Func func, ItemUiContext context = null); +} +``` diff --git a/UIFixes.csproj b/UIFixes.csproj index 91bcde1..dee5462 100644 --- a/UIFixes.csproj +++ b/UIFixes.csproj @@ -4,7 +4,7 @@ net471 Tyfon.UIFixes SPT UI Fixes - 2.3.1 + 2.5.1 true latest Debug;Release @@ -79,7 +79,8 @@ - + diff --git a/server/package.json b/server/package.json index c5a5469..749ff16 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "uifixes", - "version": "2.3.1", + "version": "2.5.1", "main": "src/mod.js", "license": "MIT", "author": "Tyfon", diff --git a/server/src/mod.ts b/server/src/mod.ts index ce41278..8378b37 100644 --- a/server/src/mod.ts +++ b/server/src/mod.ts @@ -1,7 +1,7 @@ import type { DependencyContainer } from "tsyringe"; +import type { InraidController } from "@spt/controllers/InraidController"; import type { HideoutHelper } from "@spt/helpers/HideoutHelper"; -import type { InRaidHelper } from "@spt/helpers/InRaidHelper"; import type { InventoryHelper } from "@spt/helpers/InventoryHelper"; import type { ItemHelper } from "@spt/helpers/ItemHelper"; import type { IHideoutSingleProductionStartRequestData } from "@spt/models/eft/hideout/IHideoutSingleProductionStartRequestData"; @@ -13,8 +13,6 @@ 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; @@ -30,53 +28,29 @@ class UIFixes implements IPreSptLoadMod { // Keep quickbinds for items that aren't actually lost on death container.afterResolution( - "InRaidHelper", - (_, inRaidHelper: InRaidHelper) => { - const original = inRaidHelper.deleteInventory; + "InraidController", + (_, inRaidController: InraidController) => { + const original = inRaidController["performPostRaidActionsWhenDead"]; // protected, can only access by name - inRaidHelper.deleteInventory = (pmcData, sessionId) => { + inRaidController["performPostRaidActionsWhenDead"] = (postRaidSaveRequest, pmcData, sessionId) => { // Copy the existing quickbinds const fastPanel = cloner.clone(pmcData.Inventory.fastPanel); // Nukes the inventory and the fastpanel - original.call(inRaidHelper, pmcData, sessionId); + const result = original.call(inRaidController, postRaidSaveRequest, pmcData, sessionId); // Restore the quickbinds for items that still exist - for (const index in fastPanel) { - if (pmcData.Inventory.items.find(i => i._id == fastPanel[index])) { - pmcData.Inventory.fastPanel[index] = fastPanel[index]; - } - } - }; - }, - { 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"]; + try { + for (const index in fastPanel) { + if (pmcData.Inventory.items.find(i => i._id == fastPanel[index])) { + pmcData.Inventory.fastPanel[index] = fastPanel[index]; + } } + } catch (error) { + this.logger.error(`UIFixes: Failed to restore quickbinds\n ${error}`); } - return offer; + return result; }; }, { frequency: "Always" } @@ -93,14 +67,29 @@ class UIFixes implements IPreSptLoadMod { const result = original.call(hideoutHelper, pmcData, body, sessionID); // The items haven't been deleted yet, augment the list with their parentId - const bodyAsSingle = body as IHideoutSingleProductionStartRequestData; - if (bodyAsSingle && bodyAsSingle.tools?.length > 0) { - const requestTools = bodyAsSingle.tools; - const tools = pmcData.Hideout.Production[body.recipeId].sptRequiredTools; - for (let i = 0; i < tools.length; i++) { - const originalTool = pmcData.Inventory.items.find(x => x._id === requestTools[i].id); - tools[i]["uifixes.returnTo"] = [originalTool.parentId, originalTool.slotId]; + try { + const bodyAsSingle = body as IHideoutSingleProductionStartRequestData; + if (bodyAsSingle && bodyAsSingle.tools?.length > 0) { + const requestTools = bodyAsSingle.tools; + const tools = pmcData.Hideout.Production[body.recipeId].sptRequiredTools; + for (let i = 0; i < tools.length; i++) { + const originalTool = pmcData.Inventory.items.find( + x => x._id === requestTools[i].id + ); + + // If the tool is in the stash itself, skip it. Same check as InventoryHelper.isItemInStash + if ( + originalTool.parentId === pmcData.Inventory.stash && + originalTool.slotId === "hideout" + ) { + continue; + } + + tools[i]["uifixes.returnTo"] = [originalTool.parentId, originalTool.slotId]; + } } + } catch (error) { + this.logger.error(`UIFixes: Failed to save tool origin\n ${error}`); } return result; @@ -121,52 +110,62 @@ class UIFixes implements IPreSptLoadMod { // If a tool marked with uifixes is there, try to return it to its original container const tool = itemWithModsToAddClone[0]; if (tool["uifixes.returnTo"]) { - const [containerId, slotId] = tool["uifixes.returnTo"]; + try { + const [containerId, slotId] = tool["uifixes.returnTo"]; - const container = pmcData.Inventory.items.find(x => x._id === containerId); - if (container) { - const containerTemplate = itemHelper.getItem(container._tpl)[1]; - const containerFS2D = inventoryHelper.getContainerMap( - containerTemplate._props.Grids[0]._props.cellsH, - containerTemplate._props.Grids[0]._props.cellsV, - pmcData.Inventory.items, - containerId - ); + const container = pmcData.Inventory.items.find(x => x._id === containerId); + if (container) { + const [foundTemplate, containerTemplate] = itemHelper.getItem(container._tpl); + if (foundTemplate && containerTemplate) { + const containerFS2D = inventoryHelper.getContainerMap( + containerTemplate._props.Grids[0]._props.cellsH, + containerTemplate._props.Grids[0]._props.cellsV, + pmcData.Inventory.items, + containerId + ); - // will change the array so clone it - if ( - inventoryHelper.canPlaceItemInContainer( - cloner.clone(containerFS2D), - itemWithModsToAddClone - ) - ) { - // At this point everything should succeed - inventoryHelper.placeItemInContainer( - containerFS2D, - itemWithModsToAddClone, - containerId, - slotId - ); + // will change the array so clone it + if ( + inventoryHelper.canPlaceItemInContainer( + cloner.clone(containerFS2D), + itemWithModsToAddClone + ) + ) { + // At this point everything should succeed + inventoryHelper.placeItemInContainer( + containerFS2D, + itemWithModsToAddClone, + containerId, + slotId + ); - // protected function, bypass typescript - inventoryHelper["setFindInRaidStatusForItem"]( - itemWithModsToAddClone, - request.foundInRaid - ); + // protected function, bypass typescript + inventoryHelper["setFindInRaidStatusForItem"]( + itemWithModsToAddClone, + request.foundInRaid + ); - // Add item + mods to output and profile inventory - output.profileChanges[sessionId].items.new.push(...itemWithModsToAddClone); - pmcData.Inventory.items.push(...itemWithModsToAddClone); + // Add item + mods to output and profile inventory + output.profileChanges[sessionId].items.new.push(...itemWithModsToAddClone); + pmcData.Inventory.items.push(...itemWithModsToAddClone); - this.logger.debug( - `Added ${itemWithModsToAddClone[0].upd?.StackObjectsCount ?? 1} item: ${ - itemWithModsToAddClone[0]._tpl - } with: ${itemWithModsToAddClone.length - 1} mods to ${containerId}` - ); + this.logger.debug( + `Added ${itemWithModsToAddClone[0].upd?.StackObjectsCount ?? 1} item: ${ + itemWithModsToAddClone[0]._tpl + } with: ${itemWithModsToAddClone.length - 1} mods to ${containerId}` + ); - return; + return; + } + } } + } catch (error) { + this.logger.error(`UIFixes: Encounted an error trying to put tool back.\n ${error}`); } + + this.logger.info( + "UIFixes: Unable to put tool back in its original container, returning it to stash." + ); } return original.call(inventoryHelper, sessionId, request, pmcData, output); @@ -214,7 +213,7 @@ class UIFixes implements IPreSptLoadMod { if (!quests[questId]) { this.logger.error( - `Trader ${traderId} questassort references unknown quest ${JSON.stringify(questId)}!` + `UIFixes: Trader ${traderId} questassort references unknown quest ${JSON.stringify(questId)}!` ); continue; } diff --git a/ConfigurationManagerAttributes.cs b/src/ConfigurationManagerAttributes.cs similarity index 100% rename from ConfigurationManagerAttributes.cs rename to src/ConfigurationManagerAttributes.cs diff --git a/ContextMenus/EmptySlotMenu.cs b/src/ContextMenus/EmptySlotMenu.cs similarity index 100% rename from ContextMenus/EmptySlotMenu.cs rename to src/ContextMenus/EmptySlotMenu.cs diff --git a/ContextMenus/EmptySlotMenuTrigger.cs b/src/ContextMenus/EmptySlotMenuTrigger.cs similarity index 95% rename from ContextMenus/EmptySlotMenuTrigger.cs rename to src/ContextMenus/EmptySlotMenuTrigger.cs index 25db1bd..00920fa 100644 --- a/ContextMenus/EmptySlotMenuTrigger.cs +++ b/src/ContextMenus/EmptySlotMenuTrigger.cs @@ -32,6 +32,9 @@ public class EmptySlotMenuTrigger : MonoBehaviour, IPointerClickHandler, IPointe using EmptySlotContext context = new(slot, parentContext, itemUiContext); var interactions = itemUiContext.GetItemContextInteractions(context, null); interactions.ExecuteInteraction(EItemInfoButton.LinkedSearch); + + // Call this explicitly since screen transition prevents it from firing normally + OnPointerExit(null); } } diff --git a/ContextMenus/InsuranceInteractions.cs b/src/ContextMenus/InsuranceInteractions.cs similarity index 100% rename from ContextMenus/InsuranceInteractions.cs rename to src/ContextMenus/InsuranceInteractions.cs diff --git a/src/ContextMenus/OpenInteractions.cs b/src/ContextMenus/OpenInteractions.cs new file mode 100644 index 0000000..d572a5e --- /dev/null +++ b/src/ContextMenus/OpenInteractions.cs @@ -0,0 +1,83 @@ +using Comfort.Common; +using EFT.UI; +using EFT.UI.DragAndDrop; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace UIFixes; + +public class OpenInteractions(ItemContextAbstractClass itemContext, ItemUiContext itemUiContext) : ItemInfoInteractionsAbstractClass(itemUiContext) +{ + private readonly ItemContextAbstractClass itemContext = itemContext; + + public override void ExecuteInteractionInternal(Options interaction) + { + if (itemContext == null || itemContext.Item is not LootItemClass compoundItem) + { + return; + } + + var taskSerializer = itemUiContext_0.gameObject.AddComponent(); + taskSerializer.Initialize(GetNestedContainers(itemContext), containerContext => + { + if (containerContext != null) + { + itemUiContext_0.OpenItem(containerContext.Item as LootItemClass, containerContext, true); + } + + return Task.CompletedTask; + }); + } + + public override bool IsActive(Options button) + { + return true; + } + + public override IResult IsInteractive(Options button) + { + return SuccessfulResult.New; + } + + public override bool HasIcons + { + get { return false; } + } + + public enum Options + { + All + } + + private IEnumerable GetNestedContainers(ItemContextAbstractClass first) + { + var windowRoot = Singleton.Instance; + LootItemClass parent = first.Item as LootItemClass; + + yield return first; + + while (true) + { + var innerContainers = parent.GetFirstLevelItems() + .Where(i => i != parent) + .Where(i => i is LootItemClass innerContainer && innerContainer.Grids.Any()); + if (innerContainers.Count() != 1) + { + yield break; + } + + var targetId = innerContainers.First().Id; + var targetItemView = windowRoot.GetComponentsInChildren().FirstOrDefault(itemView => itemView.Item.Id == targetId); + if (targetItemView == null) + { + yield return null; // Keeps returning null until the window is open + } + + parent = targetItemView.Item as LootItemClass; + yield return targetItemView.ItemContext; + } + } +} + +public class NestedContainerTaskSerializer : TaskSerializer { } \ No newline at end of file diff --git a/ContextMenus/RepairInteractions.cs b/src/ContextMenus/RepairInteractions.cs similarity index 100% rename from ContextMenus/RepairInteractions.cs rename to src/ContextMenus/RepairInteractions.cs diff --git a/src/Extensions.cs b/src/Extensions.cs new file mode 100644 index 0000000..fb844eb --- /dev/null +++ b/src/Extensions.cs @@ -0,0 +1,22 @@ +using System.Linq; +using EFT.InventoryLogic; + +namespace UIFixes; + +public static class Extensions +{ + public static Item GetRootItemNotEquipment(this Item item) + { + return item.GetAllParentItemsAndSelf(true).LastOrDefault(i => i is not EquipmentClass) ?? item; + } + + public static Item GetRootItemNotEquipment(this ItemAddress itemAddress) + { + if (itemAddress.Container == null || itemAddress.Container.ParentItem == null) + { + return null; + } + + return itemAddress.Container.ParentItem.GetRootItemNotEquipment(); + } +} \ No newline at end of file diff --git a/ExtraProperties.cs b/src/ExtraProperties.cs similarity index 77% rename from ExtraProperties.cs rename to src/ExtraProperties.cs index 9c58ec5..0c53ffd 100644 --- a/ExtraProperties.cs +++ b/src/ExtraProperties.cs @@ -1,5 +1,7 @@ using EFT.InventoryLogic; using EFT.UI.DragAndDrop; +using EFT.UI.Ragfair; +using System; using System.Runtime.CompilerServices; using UnityEngine; @@ -87,3 +89,29 @@ public static class ExtraItemViewStatsProperties public static void SetHideMods(this ItemViewStats itemViewStats, bool value) => properties.GetOrCreateValue(itemViewStats).HideMods = value; } +public static class ExtraItemMarketPricesPanelProperties +{ + private static readonly ConditionalWeakTable properties = new(); + + private class Properties + { + public Action OnMarketPricesCallback = null; + } + + public static Action GetOnMarketPricesCallback(this ItemMarketPricesPanel panel) => properties.GetOrCreateValue(panel).OnMarketPricesCallback; + public static void SetOnMarketPricesCallback(this ItemMarketPricesPanel panel, Action handler) => properties.GetOrCreateValue(panel).OnMarketPricesCallback = handler; +} + +public static class ExtraEventResultProperties +{ + private static readonly ConditionalWeakTable properties = new(); + + private class Properties + { + public MoveOperation MoveOperation; + } + + public static MoveOperation GetMoveOperation(this GClass2803 result) => properties.GetOrCreateValue(result).MoveOperation; + public static void SetMoveOperation(this GClass2803 result, MoveOperation operation) => properties.GetOrCreateValue(result).MoveOperation = operation; +} + diff --git a/GlobalUsings.cs b/src/GlobalUsings.cs similarity index 82% rename from GlobalUsings.cs rename to src/GlobalUsings.cs index 19651d1..b80732c 100644 --- a/GlobalUsings.cs +++ b/src/GlobalUsings.cs @@ -1,4 +1,4 @@ -// These shouln't change (unless they do) +// These shouldn't change (unless they do) global using GridItemAddress = ItemAddressClass; global using DragItemContext = ItemContextClass; global using InsuranceItem = ItemClass; @@ -19,6 +19,7 @@ global using ItemSorter = GClass2772; global using ItemWithLocation = GClass2521; global using SearchableGrid = GClass2516; global using CursorManager = GClass3034; +global using Helmet = GClass2651; // State machine states global using FirearmReadyState = EFT.Player.FirearmController.GClass1619; @@ -38,10 +39,17 @@ global using NoPossibleActionsError = GClass3317; global using CannotSortError = GClass3325; global using FailedToSortError = GClass3326; global using MoveSameSpaceError = InteractionsHandlerClass.GClass3353; +global using NotModdableInRaidError = GClass3321; +global using MultitoolNeededError = GClass3322; +global using ModVitalPartInRaidError = GClass3323; +global using SlotNotEmptyError = EFT.InventoryLogic.Slot.GClass3339; // Operations global using ItemOperation = GStruct413; global using MoveOperation = GClass2802; +global using AddOperation = GClass2798; +global using ResizeOperation = GClass2803; +global using FoldOperation = GClass2815; global using NoOpMove = GClass2795; global using BindOperation = GClass2818; global using SortOperation = GClass2824; diff --git a/Multiselect/DrawMultiSelect.cs b/src/Multiselect/DrawMultiSelect.cs similarity index 77% rename from Multiselect/DrawMultiSelect.cs rename to src/Multiselect/DrawMultiSelect.cs index bf3e469..bb3852e 100644 --- a/Multiselect/DrawMultiSelect.cs +++ b/src/Multiselect/DrawMultiSelect.cs @@ -63,8 +63,8 @@ public class DrawMultiSelect : MonoBehaviour { 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()) + // Special case: if selection key is mouse0 (left), don't start selection if over a clickable + if (Settings.SelectionBoxKey.Value.MainKey == KeyCode.Mouse0 && !shiftDown && MouseIsOverClickable()) { return; } @@ -75,7 +75,11 @@ public class DrawMultiSelect : MonoBehaviour if (!secondary) { - MultiSelect.Clear(); + // Special case: if selection key is any mouse key (center,right), don't clear selection on mouse down if over item + if (Settings.SelectionBoxKey.Value.MainKey != KeyCode.Mouse1 && Settings.SelectionBoxKey.Value.MainKey != KeyCode.Mouse2 || !MouseIsOverItem()) + { + MultiSelect.Clear(); + } } } @@ -165,12 +169,17 @@ public class DrawMultiSelect : MonoBehaviour } } - private bool MouseIsOverClickable() + private bool MouseIsOverItem() { // checking ItemUiContext is a quick and easy way to know the mouse is over an item - if (ItemUiContext.Instance.R().ItemContext != null) + return ItemUiContext.Instance.R().ItemContext != null; + } + + private bool MouseIsOverClickable() + { + if (MouseIsOverItem()) { - return false; + return true; } PointerEventData eventData = new(EventSystem.current) @@ -179,26 +188,42 @@ public class DrawMultiSelect : MonoBehaviour }; List results = []; + preloaderRaycaster.Raycast(eventData, results); // preload objects are on top, so check that first localRaycaster.Raycast(eventData, results); - preloaderRaycaster.Raycast(eventData, results); - foreach (GameObject gameObject in results.Select(r => r.gameObject)) + GameObject gameObject = results.FirstOrDefault().gameObject; + if (gameObject == null) { - 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 false; } - return true; + var draggables = gameObject.GetComponentsInParent() + .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.GetComponentsInParent() + .Where(c => c is IPointerClickHandler || c is IPointerDownHandler || c is IPointerUpHandler) + .Where(c => c is not EmptySlotMenuTrigger); // ignore empty slots that are right-clickable due to UIFixes + + // Windows are clickable to focus them, but that shouldn't block selection + var windows = clickables + .Where(c => c is UIInputNode) // Windows<>'s parent, cheap check + .Where(c => + { + // Most window types implement IPointerClickHandler and inherit directly from Window<> + Type baseType = c.GetType().BaseType; + return baseType != null && baseType.IsGenericType && baseType.GetGenericTypeDefinition() == typeof(Window<>); + }); + + clickables = clickables.Except(windows); + + if (draggables.Any() || clickables.Any()) + { + return true; + } + + return false; } private bool IsOnTop(Rect itemRect, Transform itemTransform, GraphicRaycaster raycaster) diff --git a/Multiselect/MultiGrid.cs b/src/Multiselect/MultiGrid.cs similarity index 100% rename from Multiselect/MultiGrid.cs rename to src/Multiselect/MultiGrid.cs diff --git a/Multiselect/MultiSelect.cs b/src/Multiselect/MultiSelect.cs similarity index 93% rename from Multiselect/MultiSelect.cs rename to src/Multiselect/MultiSelect.cs index 5859374..8637f61 100644 --- a/Multiselect/MultiSelect.cs +++ b/src/Multiselect/MultiSelect.cs @@ -419,6 +419,38 @@ public class MultiSelect } } + public static void WishlistAll(ItemUiContext itemUiContext, BaseItemInfoInteractions interactions, bool add, bool allOrNothing) + { + EItemInfoButton interaction = add ? EItemInfoButton.AddToWishlist : EItemInfoButton.RemoveFromWishlist; + if (!allOrNothing || InteractionCount(interaction, itemUiContext) == Count) + { + var taskSerializer = itemUiContext.gameObject.AddComponent(); + taskSerializer.Initialize(ItemContexts.Where(ic => InteractionAvailable(ic, interaction, itemUiContext)), + itemContext => + { + TaskCompletionSource taskSource = new(); + void callback() + { + interactions.RequestRedrawForItem(); + taskSource.Complete(); + } + + if (add) + { + itemUiContext.AddToWishList(itemContext.Item, callback); + } + else + { + itemUiContext.RemoveFromWishList(itemContext.Item, callback); + } + + return taskSource.Task; + }); + + itemUiContext.Tooltip?.Close(); + } + } + private static void ShowSelection(GridItemView itemView) { GameObject selectedMark = itemView.transform.Find("SelectedMark")?.gameObject; diff --git a/src/Multiselect/MultiSelectController.cs b/src/Multiselect/MultiSelectController.cs new file mode 100644 index 0000000..c1b05fc --- /dev/null +++ b/src/Multiselect/MultiSelectController.cs @@ -0,0 +1,31 @@ +using EFT.InventoryLogic; +using EFT.UI; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace UIFixes; + +// This class exists to create a layer between MultiSelectInterop and the MultiSelect implementation. +public static class MultiSelectController +{ + public static int GetCount() + { + return MultiSelect.Count; + } + + public static IEnumerable GetItems() + { + return MultiSelect.SortedItemContexts().Select(ic => ic.Item); + } + + public static Task Apply(Func func, ItemUiContext itemUiContext = null) + { + itemUiContext ??= ItemUiContext.Instance; + var taskSerializer = itemUiContext.gameObject.AddComponent(); + return taskSerializer.Initialize(GetItems(), func); + } +} + +public class ItemTaskSerializer : TaskSerializer { } diff --git a/Multiselect/MultiSelectDebug.cs b/src/Multiselect/MultiSelectDebug.cs similarity index 100% rename from Multiselect/MultiSelectDebug.cs rename to src/Multiselect/MultiSelectDebug.cs diff --git a/src/Multiselect/MultiSelectInterop.cs b/src/Multiselect/MultiSelectInterop.cs new file mode 100644 index 0000000..43d1b6c --- /dev/null +++ b/src/Multiselect/MultiSelectInterop.cs @@ -0,0 +1,139 @@ +using BepInEx; +using BepInEx.Bootstrap; +using EFT.InventoryLogic; +using EFT.UI; +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading.Tasks; + +/* +UI Fixes Multi-Select InterOp + +First, add the following attribute to your plugin class: + +[BepInDependency("Tyfon.UIFixes", BepInDependency.DependencyFlags.SoftDependency)] + +This will ensure UI Fixes is loaded already when your code is run. It will fail gracefully if UI Fixes is missing. + +Second, add this file to your project. Use the below UIFixesInterop.MultiSelect static methods, no explicit initialization required. + +Some things to keep in mind: +- While you can use MultiSelect.Items to get the items, this should only be used for reading purproses. If you need to +execute an operation on the items, I strongly suggest using the provided MultiSelect.Apply() method. + +- Apply() will execute the provided operation on each item, sequentially (sorted by grid order), maximum of one operation per frame. +It does this because strange bugs manifest if you try to do more than one thing in a single frame. + +- If the operation you are passing to Apply() does anything async, use the overload that takes a Func. It will wait +until each operation is over before doing the next. This is especially important if an operation could be affected by the preceding one, +for example in a quick-move where the avaiable space changes. It's also required if you are doing anything in-raid that will trigger +an animation, as starting the next one before it is complete will likely cancel the first. +*/ +namespace UIFixesInterop +{ + /// + /// Provides access to UI Fixes' multiselect functionality. + /// + internal static class MultiSelect + { + private static readonly Version RequiredVersion = new Version(2, 5); + + private static bool? UIFixesLoaded; + + private static Type MultiSelectType; + private static MethodInfo GetCountMethod; + private static MethodInfo GetItemsMethod; + private static MethodInfo ApplyMethod; + + /// Count represents the number of items in the current selection, 0 if UI Fixes is not present. + public static int Count + { + get + { + if (!Loaded()) + { + return 0; + } + + return (int)GetCountMethod.Invoke(null, new object[] { }); + } + } + + /// Items is an enumerable list of items in the current selection, empty if UI Fixes is not present. + public static IEnumerable Items + { + get + { + if (!Loaded()) + { + return new Item[] { }; + } + + return (IEnumerable)GetItemsMethod.Invoke(null, new object[] { }); + } + } + + /// + /// This method takes an Action and calls it *sequentially* on each item in the current selection. + /// Will no-op if UI Fixes is not present. + /// + /// The action to call on each item. + /// Optional ItemUiContext; will use ItemUiContext.Instance if not provided. + public static void Apply(Action action, ItemUiContext itemUiContext = null) + { + if (!Loaded()) + { + return; + } + + Func func = item => + { + action(item); + return Task.CompletedTask; + }; + + ApplyMethod.Invoke(null, new object[] { func, itemUiContext }); + } + + /// + /// This method takes an Func that returns a Task and calls it *sequentially* on each item in the current selection. + /// Will return a completed task immediately if UI Fixes is not present. + /// + /// The function to call on each item + /// Optional ItemUiContext; will use ItemUiContext.Instance if not provided. + /// A Task that will complete when all the function calls are complete. + public static Task Apply(Func func, ItemUiContext itemUiContext = null) + { + if (!Loaded()) + { + return Task.CompletedTask; + } + + return (Task)ApplyMethod.Invoke(null, new object[] { func, itemUiContext }); + } + + private static bool Loaded() + { + if (!UIFixesLoaded.HasValue) + { + bool present = Chainloader.PluginInfos.TryGetValue("Tyfon.UIFixes", out PluginInfo pluginInfo); + UIFixesLoaded = present && pluginInfo.Metadata.Version >= RequiredVersion; + + if (UIFixesLoaded.Value) + { + MultiSelectType = Type.GetType("UIFixes.MultiSelectController, Tyfon.UIFixes"); + if (MultiSelectType != null) + { + GetCountMethod = AccessTools.Method(MultiSelectType, "GetCount"); + GetItemsMethod = AccessTools.Method(MultiSelectType, "GetItems"); + ApplyMethod = AccessTools.Method(MultiSelectType, "Apply"); + } + } + } + + return UIFixesLoaded.Value; + } + } +} \ No newline at end of file diff --git a/src/Patches/AddOfferClickablePricesPatches.cs b/src/Patches/AddOfferClickablePricesPatches.cs new file mode 100644 index 0000000..40271e6 --- /dev/null +++ b/src/Patches/AddOfferClickablePricesPatches.cs @@ -0,0 +1,211 @@ +using EFT.InventoryLogic; +using EFT.UI.Ragfair; +using HarmonyLib; +using JetBrains.Annotations; +using SPT.Reflection.Patching; +using System; +using System.Linq; +using System.Reflection; +using TMPro; +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.UI; + +namespace UIFixes; + +public static class AddOfferClickablePricesPatches +{ + public static void Enable() + { + new AddButtonPatch().Enable(); + new MarketPriceUpdatePatch().Enable(); + new BulkTogglePatch().Enable(); + new MultipleStacksPatch().Enable(); + } + + public class AddButtonPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(AddOfferWindow), nameof(AddOfferWindow.Show)); + } + + [PatchPostfix] + public static void Postfix(AddOfferWindow __instance, ItemMarketPricesPanel ____pricesPanel, RequirementView[] ____requirementViews) + { + var panel = ____pricesPanel.R(); + + var rublesRequirement = ____requirementViews.First(rv => rv.name == "Requirement (RUB)"); + + Button lowestButton = panel.LowestLabel.GetOrAddComponent(); + lowestButton.onClick.AddListener(() => SetRequirement(__instance, rublesRequirement, ____pricesPanel.Minimum)); + ____pricesPanel.AddDisposable(lowestButton.onClick.RemoveAllListeners); + + Button averageButton = panel.AverageLabel.GetOrAddComponent(); + averageButton.onClick.AddListener(() => SetRequirement(__instance, rublesRequirement, ____pricesPanel.Average)); + ____pricesPanel.AddDisposable(averageButton.onClick.RemoveAllListeners); + + Button maximumButton = panel.MaximumLabel.GetOrAddComponent(); + maximumButton.onClick.AddListener(() => SetRequirement(__instance, rublesRequirement, ____pricesPanel.Maximum)); + ____pricesPanel.AddDisposable(maximumButton.onClick.RemoveAllListeners); + + ____pricesPanel.SetOnMarketPricesCallback(() => PopulateOfferPrice(__instance, ____pricesPanel, rublesRequirement)); + } + } + + public class MarketPriceUpdatePatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(ItemMarketPricesPanel), nameof(ItemMarketPricesPanel.method_1)); + } + + [PatchPostfix] + public static void Postfix(ItemMarketPricesPanel __instance) + { + var action = __instance.GetOnMarketPricesCallback(); + action?.Invoke(); + } + } + + public class BulkTogglePatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(AddOfferWindow), nameof(AddOfferWindow.method_12)); + } + + [PatchPostfix] + public static void Postfix(AddOfferWindow __instance, bool arg, ItemMarketPricesPanel ____pricesPanel, RequirementView[] ____requirementViews) + { + if (!Settings.UpdatePriceOnBulk.Value) + { + return; + } + + RequirementView rublesRequirement = ____requirementViews.First(rv => rv.name == "Requirement (RUB)"); + double currentPrice = rublesRequirement.Requirement.PreciseCount; + if (currentPrice <= 0) + { + return; + } + + // SetRequirement will multiply (or not), so just need the individual price + double individualPrice = arg ? currentPrice : Math.Ceiling(currentPrice / __instance.Int32_0); + SetRequirement(__instance, rublesRequirement, individualPrice); + } + } + + // Called when item selection changes. Handles updating price if bulk is (or was) checked + public class MultipleStacksPatch : ModulePatch + { + private static bool WasBulk; + + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(AddOfferWindow), nameof(AddOfferWindow.method_9)); + } + + [PatchPrefix] + public static void Prefix(AddOfferWindow __instance) + { + WasBulk = __instance.R().BulkOffer; + } + + [PatchPostfix] + public static void Postfix(AddOfferWindow __instance, Item item, bool selected, ItemMarketPricesPanel ____pricesPanel, RequirementView[] ____requirementViews) + { + if (!Settings.UpdatePriceOnBulk.Value || __instance.Int32_0 < 1) + { + return; + } + + // Bulk can autochange when selecting/deselecting, so only bail if it wasn't and still isn't bulk + if (!WasBulk && !__instance.R().BulkOffer) + { + return; + } + + var rublesRequirement = ____requirementViews.First(rv => rv.name == "Requirement (RUB)"); + double currentPrice = rublesRequirement.Requirement.PreciseCount; + + // Need to figure out the price per item *before* this item was added/removed + int oldCount = __instance.Int32_0 + (selected ? -item.StackObjectsCount : item.StackObjectsCount); + if (oldCount <= 0) + { + return; + } + + SetRequirement(__instance, rublesRequirement, currentPrice / oldCount); + } + } + + private static void SetRequirement(AddOfferWindow window, RequirementView requirement, double price) + { + if (window.R().BulkOffer) + { + price *= window.Int32_0; // offer item count + } + + requirement.method_0(price.ToString("F0")); + } + + private static void PopulateOfferPrice(AddOfferWindow window, ItemMarketPricesPanel pricesPanel, RequirementView rublesRequirement) + { + switch (Settings.AutoOfferPrice.Value) + { + case AutoFleaPrice.Minimum: + SetRequirement(window, rublesRequirement, pricesPanel.Minimum); + break; + case AutoFleaPrice.Average: + SetRequirement(window, rublesRequirement, pricesPanel.Average); + break; + case AutoFleaPrice.Maximum: + SetRequirement(window, rublesRequirement, pricesPanel.Maximum); + break; + case AutoFleaPrice.None: + default: + break; + } + } + + + public class HighlightButton : Button + { + private Color originalColor; + bool originalOverrideColorTags; + + private TextMeshProUGUI _text; + private TextMeshProUGUI Text + { + get + { + if (_text == null) + { + _text = GetComponent(); + } + + return _text; + } + } + + public override void OnPointerEnter([NotNull] PointerEventData eventData) + { + base.OnPointerEnter(eventData); + + originalColor = Text.color; + originalOverrideColorTags = Text.overrideColorTags; + + Text.overrideColorTags = true; + Text.color = Color.white; + } + + public override void OnPointerExit([NotNull] PointerEventData eventData) + { + base.OnPointerExit(eventData); + + Text.overrideColorTags = originalOverrideColorTags; + Text.color = originalColor; + } + } +} diff --git a/src/Patches/AddOfferContextMenuPatches.cs b/src/Patches/AddOfferContextMenuPatches.cs new file mode 100644 index 0000000..d7c6e62 --- /dev/null +++ b/src/Patches/AddOfferContextMenuPatches.cs @@ -0,0 +1,219 @@ +using Comfort.Common; +using EFT.InventoryLogic; +using EFT.UI; +using EFT.UI.Ragfair; +using HarmonyLib; +using SPT.Reflection.Patching; +using SPT.Reflection.Utils; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEngine; + +namespace UIFixes; + +public static class AddOfferContextMenuPatches +{ + private static Item AddOfferItem = null; + + public static void Enable() + { + new AddOfferInventoryMenuPatch().Enable(); + new AddOfferTradingMenuPatch().Enable(); + new AddOfferIsActivePatch().Enable(); + new AddOfferIsInteractivePatch().Enable(); + new AddOfferNameIconPatch().Enable(); + + new AddOfferExecutePatch().Enable(); + new ShowAddOfferWindowPatch().Enable(); + new SelectItemPatch().Enable(); + } + + public class AddOfferInventoryMenuPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.DeclaredProperty(R.InventoryInteractions.CompleteType, "AvailableInteractions").GetMethod; + } + + [PatchPostfix] + public static void Postfix(ref IEnumerable __result) + { + if (Settings.AddOfferContextMenu.Value) + { + var list = __result.ToList(); + list.Insert(list.IndexOf(EItemInfoButton.Tag), EItemInfoButtonExt.AddOffer); + __result = list; + } + } + } + + public class AddOfferTradingMenuPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.DeclaredProperty(R.TradingInteractions.Type, "AvailableInteractions").GetMethod; + } + + [PatchPostfix] + public static void Postfix(ref IEnumerable __result) + { + if (Settings.AddOfferContextMenu.Value) + { + var list = __result.ToList(); + list.Insert(list.IndexOf(EItemInfoButton.Tag), EItemInfoButtonExt.AddOffer); + __result = list; + } + } + } + + public class AddOfferNameIconPatch : ModulePatch + { + private static Sprite FleaSprite = null; + + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(InteractionButtonsContainer), nameof(InteractionButtonsContainer.Show)).MakeGenericMethod(typeof(EItemInfoButton)); + } + + [PatchPrefix] + public static void Prefix(ref IReadOnlyDictionary names, ref IReadOnlyDictionary icons) + { + names ??= new Dictionary() + { + { EItemInfoButtonExt.AddOffer, "ragfair/OFFER ADD" } + }; + + FleaSprite ??= Resources.FindObjectsOfTypeAll().Single(s => s.name == "icon_flea_market"); + icons ??= new Dictionary() + { + { EItemInfoButtonExt.AddOffer, FleaSprite } + }; + } + } + + public class AddOfferIsActivePatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(R.ContextMenuHelper.Type, "IsActive"); + } + + [PatchPrefix] + public static bool Prefix(EItemInfoButton button, ref bool __result) + { + if (button != EItemInfoButtonExt.AddOffer) + { + return true; + } + + if (Plugin.InRaid()) + { + __result = false; + return false; + } + + __result = true; + return false; + } + } + + public class AddOfferIsInteractivePatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(R.ContextMenuHelper.Type, "IsInteractive"); + } + + [PatchPostfix] + public static void Postfix(EItemInfoButton button, ref IResult __result, Item ___item_0) + { + if (button != EItemInfoButtonExt.AddOffer) + { + return; + } + + ISession session = PatchConstants.BackEndSession; + RagFairClass ragfair = session.RagFair; + if (ragfair.Status != RagFairClass.ERagFairStatus.Available) + { + __result = new FailedResult(ragfair.GetFormattedStatusDescription()); + return; + } + + if (ragfair.MyOffersCount >= ragfair.GetMaxOffersCount(ragfair.MyRating)) + { + __result = new FailedResult("ragfair/Reached maximum amount of offers"); + return; + } + + RagfairOfferSellHelperClass ragfairHelper = new(session.Profile, session.Profile.Inventory.Stash.Grid); + if (!ragfairHelper.method_4(___item_0, out string error)) + { + __result = new FailedResult(error); + return; + } + } + } + + public class AddOfferExecutePatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(BaseItemInfoInteractions), nameof(BaseItemInfoInteractions.ExecuteInteractionInternal)); + } + + [PatchPrefix] + public static bool Prefix(ItemInfoInteractionsAbstractClass __instance, EItemInfoButton interaction, Item ___item_0) + { + if (interaction != EItemInfoButtonExt.AddOffer) + { + return true; + } + + AddOfferItem = ___item_0; + + __instance.ExecuteInteractionInternal(EItemInfoButton.FilterSearch); + return false; + } + } + + public class ShowAddOfferWindowPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(RagfairScreen), nameof(RagfairScreen.Show)); + } + + [PatchPostfix] + public static void Postfix(RagfairScreen __instance) + { + if (AddOfferItem == null) + { + return; + } + + __instance.method_27(); // click the add offer button + } + } + + public class SelectItemPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(AddOfferWindow), nameof(AddOfferWindow.Show)); + } + + [PatchPostfix] + public static void Postfix(RagfairOfferSellHelperClass ___ragfairOfferSellHelperClass) + { + if (AddOfferItem == null) + { + return; + } + + ___ragfairOfferSellHelperClass.SelectItem(AddOfferItem); + AddOfferItem = null; + } + } +} \ No newline at end of file diff --git a/Patches/AddOfferRememberAutoselectPatches.cs b/src/Patches/AddOfferRememberAutoselectPatches.cs similarity index 100% rename from Patches/AddOfferRememberAutoselectPatches.cs rename to src/Patches/AddOfferRememberAutoselectPatches.cs diff --git a/src/Patches/AimToggleHoldPatches.cs b/src/Patches/AimToggleHoldPatches.cs new file mode 100644 index 0000000..039d8ed --- /dev/null +++ b/src/Patches/AimToggleHoldPatches.cs @@ -0,0 +1,146 @@ +using Comfort.Common; +using EFT.InputSystem; +using HarmonyLib; +using JsonType; +using SPT.Reflection.Patching; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace UIFixes; + +public static class AimToggleHoldPatches +{ + public static void Enable() + { + new AddTwoKeyStatesPatch().Enable(); + new AddOneKeyStatesPatch().Enable(); + new UpdateInputPatch().Enable(); + + Settings.ToggleOrHoldAim.SettingChanged += OnSettingChanged; + Settings.ToggleOrHoldSprint.SettingChanged += OnSettingChanged; + Settings.ToggleOrHoldTactical.SettingChanged += OnSettingChanged; + Settings.ToggleOrHoldHeadlight.SettingChanged += OnSettingChanged; + Settings.ToggleOrHoldGoggles.SettingChanged += OnSettingChanged; + } + + public class AddTwoKeyStatesPatch : ModulePatch + { + private static FieldInfo StateMachineArray; + + protected override MethodBase GetTargetMethod() + { + StateMachineArray = AccessTools.Field(typeof(KeyCombination), "keyCombinationState_1"); + return AccessTools.GetDeclaredConstructors(typeof(ToggleKeyCombination)).Single(); + } + + [PatchPostfix] + public static void Postfix(ToggleKeyCombination __instance, EGameKey gameKey, ECommand disableCommand, KeyCombination.KeyCombinationState[] ___keyCombinationState_1) + { + bool useToggleHold = gameKey switch + { + EGameKey.Aim => Settings.ToggleOrHoldAim.Value, + EGameKey.Sprint => Settings.ToggleOrHoldSprint.Value, + _ => false + }; + + if (!useToggleHold) + { + return; + } + + List states = new(___keyCombinationState_1) + { + new ToggleHoldIdleState(__instance), + new ToggleHoldClickOrHoldState(__instance), + new ToggleHoldHoldState(__instance, disableCommand) + }; + + StateMachineArray.SetValue(__instance, states.ToArray()); + } + } + + public class AddOneKeyStatesPatch : ModulePatch + { + private static FieldInfo StateMachineArray; + + protected override MethodBase GetTargetMethod() + { + StateMachineArray = AccessTools.Field(typeof(KeyCombination), "keyCombinationState_1"); + return AccessTools.GetDeclaredConstructors(typeof(KeyCombination)).Single(); + } + + [PatchPostfix] + public static void Postfix(ToggleKeyCombination __instance, EGameKey gameKey, ECommand command, KeyCombination.KeyCombinationState[] ___keyCombinationState_1) + { + if (!UseToggleHold(gameKey)) + { + return; + } + + List states = new(___keyCombinationState_1) + { + new ToggleHoldIdleState(__instance), + new ToggleHoldClickOrHoldState(__instance), + new ToggleHoldHoldState(__instance, command) + }; + + StateMachineArray.SetValue(__instance, states.ToArray()); + } + } + + public class UpdateInputPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(KeyCombination), nameof(KeyCombination.UpdateInput)); + } + + [PatchPostfix] + public static void Postfix(KeyCombination __instance) + { + if (UseToggleHold(__instance.GameKey)) + { + __instance.method_0((KeyCombination.EKeyState)ToggleHoldState.Idle); + } + } + } + + private static bool UseToggleHold(EGameKey gameKey) + { + return gameKey switch + { + EGameKey.Aim => Settings.ToggleOrHoldAim.Value, + EGameKey.Tactical => Settings.ToggleOrHoldTactical.Value, + EGameKey.ToggleGoggles => Settings.ToggleOrHoldGoggles.Value, + EGameKey.ToggleHeadLight => Settings.ToggleOrHoldHeadlight.Value, + EGameKey.Sprint => Settings.ToggleOrHoldSprint.Value, + EGameKey.Slot4 => UseToggleHoldQuickBind(EGameKey.Slot4), + EGameKey.Slot5 => UseToggleHoldQuickBind(EGameKey.Slot5), + EGameKey.Slot6 => UseToggleHoldQuickBind(EGameKey.Slot6), + EGameKey.Slot7 => UseToggleHoldQuickBind(EGameKey.Slot7), + EGameKey.Slot8 => UseToggleHoldQuickBind(EGameKey.Slot8), + EGameKey.Slot9 => UseToggleHoldQuickBind(EGameKey.Slot9), + EGameKey.Slot0 => UseToggleHoldQuickBind(EGameKey.Slot0), + _ => false + }; + } + + private static bool UseToggleHoldQuickBind(EGameKey gameKey) + { + return Quickbind.GetType(gameKey) switch + { + Quickbind.ItemType.Tactical => Settings.ToggleOrHoldTactical.Value, + Quickbind.ItemType.Headlight => Settings.ToggleOrHoldHeadlight.Value, + Quickbind.ItemType.NightVision => Settings.ToggleOrHoldGoggles.Value, + _ => false, + }; + } + + private static void OnSettingChanged(object sender, EventArgs args) + { + // Will "save" control settings, running GClass1911.UpdateInput, which will set (or unset) toggle/hold behavior + Singleton.Instance.Control.Controller.method_3(); + } +} diff --git a/Patches/AssortUnlocksPatch.cs b/src/Patches/AssortUnlocksPatch.cs similarity index 100% rename from Patches/AssortUnlocksPatch.cs rename to src/Patches/AssortUnlocksPatch.cs diff --git a/Patches/AutofillQuestItemsPatch.cs b/src/Patches/AutofillQuestItemsPatch.cs similarity index 100% rename from Patches/AutofillQuestItemsPatch.cs rename to src/Patches/AutofillQuestItemsPatch.cs diff --git a/Patches/BarterOfferPatches.cs b/src/Patches/BarterOfferPatches.cs similarity index 100% rename from Patches/BarterOfferPatches.cs rename to src/Patches/BarterOfferPatches.cs diff --git a/Patches/ConfirmationDialogKeysPatches.cs b/src/Patches/ConfirmationDialogKeysPatches.cs similarity index 100% rename from Patches/ConfirmationDialogKeysPatches.cs rename to src/Patches/ConfirmationDialogKeysPatches.cs diff --git a/Patches/ContextMenuPatches.cs b/src/Patches/ContextMenuPatches.cs similarity index 81% rename from Patches/ContextMenuPatches.cs rename to src/Patches/ContextMenuPatches.cs index 2347dfc..811a0c6 100644 --- a/Patches/ContextMenuPatches.cs +++ b/src/Patches/ContextMenuPatches.cs @@ -4,6 +4,7 @@ using EFT.UI; using EFT.UI.DragAndDrop; using HarmonyLib; using SPT.Reflection.Patching; +using SPT.Reflection.Utils; using System; using System.Collections.Generic; using System.Linq; @@ -13,6 +14,11 @@ using UnityEngine; namespace UIFixes; +public static class EItemInfoButtonExt +{ + public const EItemInfoButton AddOffer = (EItemInfoButton)77; +} + public static class ContextMenuPatches { private static InsuranceInteractions CurrentInsuranceInteractions = null; @@ -42,6 +48,9 @@ public static class ContextMenuPatches new EmptyModSlotMenuRemovePatch().Enable(); new EmptySlotMenuPatch().Enable(); new EmptySlotMenuRemovePatch().Enable(); + + new InventoryWishlistPatch().Enable(); + new TradingWishlistPatch().Enable(); } public class ContextMenuNamesPatch : ModulePatch @@ -59,65 +68,51 @@ public static class ContextMenuPatches return; } + int count = 0; if (caption == EItemInfoButton.Insure.ToString()) { InsuranceCompanyClass insurance = ItemUiContext.Instance.Session.InsuranceCompany; - int count = MultiSelect.ItemContexts.Select(ic => InsuranceItem.FindOrCreate(ic.Item)) + count = MultiSelect.ItemContexts.Select(ic => InsuranceItem.FindOrCreate(ic.Item)) .Where(i => insurance.ItemTypeAvailableForInsurance(i) && !insurance.InsuredItems.Contains(i)) .Count(); - if (count > 0) - { - ____text.text += " (x" + count + ")"; - } } else if (caption == EItemInfoButton.Equip.ToString()) { - int count = MultiSelect.InteractionCount(EItemInfoButton.Equip, ItemUiContext.Instance); - if (count > 0) - { - ____text.text += " (x" + count + ")"; - } + count = MultiSelect.InteractionCount(EItemInfoButton.Equip, ItemUiContext.Instance); } else if (caption == EItemInfoButton.Unequip.ToString()) { - int count = MultiSelect.InteractionCount(EItemInfoButton.Unequip, ItemUiContext.Instance); - if (count > 0) - { - ____text.text += " (x" + count + ")"; - } + count = MultiSelect.InteractionCount(EItemInfoButton.Unequip, ItemUiContext.Instance); } else if (caption == EItemInfoButton.LoadAmmo.ToString()) { - int count = MultiSelect.InteractionCount(EItemInfoButton.LoadAmmo, ItemUiContext.Instance); - if (count > 0) - { - ____text.text += " (x" + count + ")"; - } + count = MultiSelect.InteractionCount(EItemInfoButton.LoadAmmo, ItemUiContext.Instance); } else if (caption == EItemInfoButton.UnloadAmmo.ToString()) { - int count = MultiSelect.InteractionCount(EItemInfoButton.UnloadAmmo, ItemUiContext.Instance); - if (count > 0) - { - ____text.text += " (x" + count + ")"; - } + count = MultiSelect.InteractionCount(EItemInfoButton.UnloadAmmo, ItemUiContext.Instance); } else if (caption == EItemInfoButton.ApplyMagPreset.ToString()) { - int count = MultiSelect.InteractionCount(EItemInfoButton.ApplyMagPreset, ItemUiContext.Instance); - if (count > 0) - { - ____text.text += " (x" + count + ")"; - } + count = MultiSelect.InteractionCount(EItemInfoButton.ApplyMagPreset, ItemUiContext.Instance); } else if (caption == EItemInfoButton.Unpack.ToString()) { - int count = MultiSelect.InteractionCount(EItemInfoButton.Unpack, ItemUiContext.Instance); - if (count > 0) - { - ____text.text += " (x" + count + ")"; - } + count = MultiSelect.InteractionCount(EItemInfoButton.Unpack, ItemUiContext.Instance); + } + else if (caption == EItemInfoButton.AddToWishlist.ToString()) + { + count = MultiSelect.InteractionCount(EItemInfoButton.AddToWishlist, ItemUiContext.Instance); + } + else if (caption == EItemInfoButton.RemoveFromWishlist.ToString()) + { + count = MultiSelect.InteractionCount(EItemInfoButton.RemoveFromWishlist, ItemUiContext.Instance); + } + + if (count > 0) + { + ____text.text += " (x" + count + ")"; } } } @@ -130,9 +125,20 @@ public static class ContextMenuPatches } [PatchPostfix] - public static void Postfix(ref IEnumerable __result) + public static void Postfix(ref IEnumerable __result, Item ___item_0) { __result = __result.Append(EItemInfoButton.Repair).Append(EItemInfoButton.Insure); + + if (___item_0 is LootItemClass container && container.Grids.Any()) + { + var innerContainers = container.GetFirstLevelItems() + .Where(i => i != container) + .Where(i => i is LootItemClass innerContainer && innerContainer.Grids.Any()); + if (innerContainers.Count() == 1) + { + __result = __result.Append(EItemInfoButton.Open); + } + } } } @@ -146,7 +152,12 @@ public static class ContextMenuPatches } [PatchPrefix] - public static bool Prefix(EItemInfoButton parentInteraction, ISubInteractions subInteractionsWrapper, Item ___item_0, ItemUiContext ___itemUiContext_1) + public static bool Prefix( + EItemInfoButton parentInteraction, + ISubInteractions subInteractionsWrapper, + Item ___item_0, + ItemContextAbstractClass ___itemContextAbstractClass, + ItemUiContext ___itemUiContext_1) { // Clear this, since something else should be active (even a different mouseover of the insurance button) LoadingInsuranceActions = false; @@ -184,6 +195,12 @@ public static class ContextMenuPatches return false; } + if (Settings.OpenAllContextMenu.Value && parentInteraction == EItemInfoButton.Open) + { + subInteractionsWrapper.SetSubInteractions(new OpenInteractions(___itemContextAbstractClass, ___itemUiContext_1)); + return false; + } + return true; } } @@ -514,7 +531,9 @@ public static class ContextMenuPatches { protected override MethodBase GetTargetMethod() { - return AccessTools.Method(typeof(InteractionButtonsContainer), nameof(InteractionButtonsContainer.SetSubInteractions)).MakeGenericMethod([typeof(InsuranceInteractions.EInsurers)]); + return AccessTools.Method( + typeof(InteractionButtonsContainer), + nameof(InteractionButtonsContainer.SetSubInteractions)).MakeGenericMethod([typeof(InsuranceInteractions.EInsurers)]); } // Existing logic tries to place it on the right, moving to the left if necessary. They didn't do it correctly, so it always goes on the left. @@ -525,6 +544,54 @@ public static class ContextMenuPatches } } + public class InventoryWishlistPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + // R.InventoryActions.Type is only ever referenced by it's child class, which overrides AvailableInteractions + Type type = PatchConstants.EftTypes.First(t => t.BaseType == R.InventoryInteractions.Type); + return AccessTools.DeclaredProperty(type, "AvailableInteractions").GetMethod; + } + + [PatchPostfix] + public static void Postfix(ref IEnumerable __result) + { + if (!Settings.WishlistContextEverywhere.Value) + { + return; + } + + var list = __result.ToList(); + int index = list.IndexOf(EItemInfoButton.Tag); + list.Insert(index, EItemInfoButton.RemoveFromWishlist); + list.Insert(index, EItemInfoButton.AddToWishlist); + __result = list; + } + } + + public class TradingWishlistPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.DeclaredProperty(R.TradingInteractions.Type, "AvailableInteractions").GetMethod; + } + + [PatchPostfix] + public static void Postfix(ref IEnumerable __result) + { + if (!Settings.WishlistContextEverywhere.Value) + { + return; + } + + var list = __result.ToList(); + int index = list.IndexOf(EItemInfoButton.Tag); + list.Insert(index, EItemInfoButton.RemoveFromWishlist); + list.Insert(index, EItemInfoButton.AddToWishlist); + __result = list; + } + } + private static void PositionContextMenuFlyout(SimpleContextMenuButton button, SimpleContextMenu flyoutMenu) { RectTransform buttonTransform = button.RectTransform(); diff --git a/Patches/ContextMenuShortcutPatches.cs b/src/Patches/ContextMenuShortcutPatches.cs similarity index 93% rename from Patches/ContextMenuShortcutPatches.cs rename to src/Patches/ContextMenuShortcutPatches.cs index e2de8f9..24e3c9c 100644 --- a/Patches/ContextMenuShortcutPatches.cs +++ b/src/Patches/ContextMenuShortcutPatches.cs @@ -46,9 +46,7 @@ public static class ContextMenuShortcutPatches return; } - if (!Settings.ItemContextBlocksTextInputs.Value && - EventSystem.current?.currentSelectedGameObject != null && - EventSystem.current.currentSelectedGameObject.GetComponent() != null) + if (!Settings.ItemContextBlocksTextInputs.Value && Plugin.TextboxActive()) { return; } @@ -78,6 +76,11 @@ public static class ContextMenuShortcutPatches TryInteraction(__instance, itemContext, EItemInfoButton.UseAll, [EItemInfoButton.Use]); } + if (Settings.ReloadKeyBind.Value.IsDown()) + { + TryInteraction(__instance, itemContext, EItemInfoButton.Reload); + } + if (Settings.UnloadKeyBind.Value.IsDown()) { TryInteraction(__instance, itemContext, EItemInfoButton.Unload, [EItemInfoButton.UnloadAmmo]); @@ -98,6 +101,11 @@ public static class ContextMenuShortcutPatches TryInteraction(__instance, itemContext, EItemInfoButton.LinkedSearch); } + if (Settings.RequiredSearchKeyBind.Value.IsDown()) + { + TryInteraction(__instance, itemContext, EItemInfoButton.NeededSearch); + } + if (Settings.SortingTableKeyBind.Value.IsDown()) { MoveToFromSortingTable(itemContext, __instance); @@ -109,6 +117,11 @@ public static class ContextMenuShortcutPatches [EItemInfoButton.Fold, EItemInfoButton.Unfold, EItemInfoButton.TurnOn, EItemInfoButton.TurnOff, EItemInfoButton.CheckMagazine]); } + if (Settings.AddOfferKeyBind.Value.IsDown()) + { + TryInteraction(__instance, itemContext, EItemInfoButtonExt.AddOffer); + } + Interactions = null; } diff --git a/Patches/FilterOutOfStockPatches.cs b/src/Patches/FilterOutOfStockPatches.cs similarity index 100% rename from Patches/FilterOutOfStockPatches.cs rename to src/Patches/FilterOutOfStockPatches.cs diff --git a/Patches/FixFleaPatches.cs b/src/Patches/FixFleaPatches.cs similarity index 54% rename from Patches/FixFleaPatches.cs rename to src/Patches/FixFleaPatches.cs index 1590249..b178f3b 100644 --- a/Patches/FixFleaPatches.cs +++ b/src/Patches/FixFleaPatches.cs @@ -1,9 +1,11 @@ -using EFT.UI; +using EFT.HandBook; +using EFT.UI; using EFT.UI.Ragfair; using HarmonyLib; using SPT.Reflection.Patching; using System.Linq; using System.Reflection; +using System.Threading.Tasks; using TMPro; using UnityEngine; using UnityEngine.UI; @@ -12,6 +14,8 @@ namespace UIFixes; public static class FixFleaPatches { + private static Task SearchFilterTask; + public static void Enable() { // These are anal AF @@ -19,10 +23,15 @@ public static class FixFleaPatches new ToggleOnOpenPatch().Enable(); new DropdownHeightPatch().Enable(); + new AddOfferWindowDoubleScrollPatch().Enable(); + new OfferItemFixMaskPatch().Enable(); new OfferViewTweaksPatch().Enable(); + new SearchFilterPatch().Enable(); new SearchPatch().Enable(); + new SearchKeyPatch().Enable(); + new SearchKeyHandbookPatch().Enable(); } public class DoNotToggleOnMouseOverPatch : ModulePatch @@ -101,6 +110,42 @@ public static class FixFleaPatches } } + public class AddOfferWindowDoubleScrollPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(AddOfferWindow), nameof(AddOfferWindow.Awake)); + } + + [PatchPostfix] + public static void Postfix(AddOfferWindow __instance, GameObject ____noOfferPanel, GameObject ____selectedItemPanel) + { + // Not sure how they messed it this up, but the widths on some of these are hardcoded + // badly, so things move around + Transform stashPart = __instance.transform.Find("Inner/Contents/StashPart"); + var stashLayout = stashPart.gameObject.GetComponent(); + stashLayout.preferredWidth = 644f; + + var noItemLayout = ____noOfferPanel.GetComponent(); + var requirementLayout = ____selectedItemPanel.GetComponent(); + requirementLayout.preferredWidth = noItemLayout.preferredWidth = 450f; + } + } + + public class SearchFilterPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(BrowseCategoriesPanel), nameof(BrowseCategoriesPanel.Filter)); + } + + [PatchPostfix] + public static void Postfix(Task __result) + { + SearchFilterTask = __result; + } + } + public class SearchPatch : ModulePatch { protected override MethodBase GetTargetMethod() @@ -121,19 +166,64 @@ public static class FixFleaPatches return true; } + if (SearchFilterTask != null && !SearchFilterTask.IsCompleted) + { + SearchFilterTask.ContinueWith(t => DoSearch(__instance), TaskScheduler.FromCurrentSynchronizationContext()); + return true; + } + if (__instance.FilteredNodes.Values.Sum(node => node.Count) > 0) { return true; } - __instance.Ragfair.CancellableFilters.Clear(); + DoSearch(__instance); + return false; + } - FilterRule filterRule = __instance.Ragfair.method_3(EViewListType.AllOffers); + private static void DoSearch(RagfairCategoriesPanel panel) + { + if (panel.FilteredNodes.Values.Sum(node => node.Count) > 0) + { + return; + } + + panel.Ragfair.CancellableFilters.Clear(); + + FilterRule filterRule = panel.Ragfair.method_3(EViewListType.AllOffers); filterRule.HandbookId = string.Empty; - __instance.Ragfair.AddSearchesInRule(filterRule, true); + panel.Ragfair.AddSearchesInRule(filterRule, true); + } + } - return false; + public class SearchKeyPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(BrowseCategoriesPanel), nameof(BrowseCategoriesPanel.Awake)); + } + + [PatchPostfix] + public static void Postfix(TMP_InputField ___SearchInputField) + { + ___SearchInputField.GetOrAddComponent(); + } + } + + // Have to target HandbookCategoriesPanel specifically because even though it inherits from BrowseCategoriesPanel, + // BSG couldn't be bothered to call base.Awake() + public class SearchKeyHandbookPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(HandbookCategoriesPanel), nameof(HandbookCategoriesPanel.Awake)); + } + + [PatchPostfix] + public static void Postfix(TMP_InputField ___SearchInputField) + { + ___SearchInputField.GetOrAddComponent(); } } @@ -141,15 +231,15 @@ public static class FixFleaPatches { protected override MethodBase GetTargetMethod() { - return AccessTools.DeclaredMethod(typeof(DropDownBox), nameof(DropDownBox.Init)); + return AccessTools.DeclaredMethod(typeof(DropDownBox), nameof(DropDownBox.Show)); } [PatchPostfix] - public static void Postfix(ref float ____maxVisibleHeight) + public static void Postfix(DropDownBox __instance, ref float ____maxVisibleHeight) { if (____maxVisibleHeight == 120f) { - ____maxVisibleHeight = 240f; + ____maxVisibleHeight = 150f; } } } diff --git a/Patches/FixMailRecieveAllPatch.cs b/src/Patches/FixMailRecieveAllPatch.cs similarity index 100% rename from Patches/FixMailRecieveAllPatch.cs rename to src/Patches/FixMailRecieveAllPatch.cs diff --git a/Patches/FixTooltipPatches.cs b/src/Patches/FixTooltipPatches.cs similarity index 100% rename from Patches/FixTooltipPatches.cs rename to src/Patches/FixTooltipPatches.cs diff --git a/Patches/FixTraderControllerSimulateFalsePatch.cs b/src/Patches/FixTraderControllerSimulateFalsePatch.cs similarity index 79% rename from Patches/FixTraderControllerSimulateFalsePatch.cs rename to src/Patches/FixTraderControllerSimulateFalsePatch.cs index 0a68ee6..fbb6322 100644 --- a/Patches/FixTraderControllerSimulateFalsePatch.cs +++ b/src/Patches/FixTraderControllerSimulateFalsePatch.cs @@ -16,8 +16,25 @@ public class FixTraderControllerSimulateFalsePatch : ModulePatch // Recreating this function to add the comment section, so calling this with simulate = false doesn't break everything [PatchPrefix] [HarmonyPriority(Priority.Last)] - public static bool Prefix(TraderControllerClass __instance, ItemContextAbstractClass itemContext, Item targetItem, bool partialTransferOnly, bool simulate, ref ItemOperation __result) + public static bool Prefix( + TraderControllerClass __instance, + ItemContextAbstractClass itemContext, + Item targetItem, + bool partialTransferOnly, + bool simulate, + ref ItemOperation __result, + bool __runOriginal) { + if (!__runOriginal) + { + // This is a little hairy, as *some* prefix didn't want to run. If MergeConsumables is present, assume it's that. + // If MC succeeded, bail out. If it failed, we might still want to swap + if (Plugin.MergeConsumablesPresent() && __result.Succeeded) + { + return __runOriginal; + } + } + TargetItemOperation opStruct; opStruct.targetItem = targetItem; opStruct.traderControllerClass = __instance; diff --git a/Patches/FixUnloadLastBulletPatch.cs b/src/Patches/FixUnloadLastBulletPatch.cs similarity index 100% rename from Patches/FixUnloadLastBulletPatch.cs rename to src/Patches/FixUnloadLastBulletPatch.cs diff --git a/Patches/FleaPrevSearchPatches.cs b/src/Patches/FleaPrevSearchPatches.cs similarity index 98% rename from Patches/FleaPrevSearchPatches.cs rename to src/Patches/FleaPrevSearchPatches.cs index baa031a..2cd0e40 100644 --- a/Patches/FleaPrevSearchPatches.cs +++ b/src/Patches/FleaPrevSearchPatches.cs @@ -327,7 +327,10 @@ public static class FleaPrevSearchPatches [PatchPostfix] public static void Postfix(EViewListType type) { - PreviousFilterButton.Instance?.gameObject.SetActive(type == EViewListType.AllOffers); + if (PreviousFilterButton.Instance != null) + { + PreviousFilterButton.Instance.gameObject.SetActive(type == EViewListType.AllOffers); + } } } @@ -369,7 +372,10 @@ public static class FleaPrevSearchPatches return; } - PreviousFilterButton.Instance.OnOffersLoaded(__instance); + if (PreviousFilterButton.Instance != null) + { + PreviousFilterButton.Instance.OnOffersLoaded(__instance); + } if (Settings.AutoExpandCategories.Value) { diff --git a/Patches/FleaSlotSearchPatches.cs b/src/Patches/FleaSlotSearchPatches.cs similarity index 100% rename from Patches/FleaSlotSearchPatches.cs rename to src/Patches/FleaSlotSearchPatches.cs diff --git a/Patches/FocusFleaOfferNumberPatches.cs b/src/Patches/FocusFleaOfferNumberPatches.cs similarity index 100% rename from Patches/FocusFleaOfferNumberPatches.cs rename to src/Patches/FocusFleaOfferNumberPatches.cs diff --git a/Patches/FocusTradeQuantityPatch.cs b/src/Patches/FocusTradeQuantityPatch.cs similarity index 100% rename from Patches/FocusTradeQuantityPatch.cs rename to src/Patches/FocusTradeQuantityPatch.cs diff --git a/Patches/GPCoinPatches.cs b/src/Patches/GPCoinPatches.cs similarity index 100% rename from Patches/GPCoinPatches.cs rename to src/Patches/GPCoinPatches.cs diff --git a/Patches/GridWindowButtonsPatch.cs b/src/Patches/GridWindowButtonsPatch.cs similarity index 97% rename from Patches/GridWindowButtonsPatch.cs rename to src/Patches/GridWindowButtonsPatch.cs index dc009ae..568a27f 100644 --- a/Patches/GridWindowButtonsPatch.cs +++ b/src/Patches/GridWindowButtonsPatch.cs @@ -73,6 +73,11 @@ public class GridWindowButtonsPatch : ModulePatch public void Update() { + if (Plugin.TextboxActive()) + { + return; + } + bool isTopWindow = window.transform.GetSiblingIndex() == window.transform.parent.childCount - 1; if (Settings.SnapLeftKeybind.Value.IsDown() && isTopWindow) { diff --git a/src/Patches/HideoutCameraPatches.cs b/src/Patches/HideoutCameraPatches.cs new file mode 100644 index 0000000..076e932 --- /dev/null +++ b/src/Patches/HideoutCameraPatches.cs @@ -0,0 +1,20 @@ +using EFT.Hideout; +using HarmonyLib; +using SPT.Reflection.Patching; +using System.Reflection; + +namespace UIFixes; + +public class HideoutCameraPatch : ModulePatch +{ + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(HideoutCameraController), nameof(HideoutCameraController.LateUpdate)); + } + + [PatchPrefix] + public static bool Prefix(HideoutCameraController __instance) + { + return !__instance.AreaSelected; + } +} diff --git a/Patches/HideoutLevelPatches.cs b/src/Patches/HideoutLevelPatches.cs similarity index 100% rename from Patches/HideoutLevelPatches.cs rename to src/Patches/HideoutLevelPatches.cs diff --git a/Patches/HideoutSearchPatches.cs b/src/Patches/HideoutSearchPatches.cs similarity index 97% rename from Patches/HideoutSearchPatches.cs rename to src/Patches/HideoutSearchPatches.cs index 9dd0c8a..1198a00 100644 --- a/Patches/HideoutSearchPatches.cs +++ b/src/Patches/HideoutSearchPatches.cs @@ -7,8 +7,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; -using TMPro; -using UnityEngine.EventSystems; using UnityEngine.UI; namespace UIFixes; @@ -108,6 +106,8 @@ public static class HideoutSearchPatches areaScreenSubstrate.method_8(); } + ____searchInputField.GetOrAddComponent(); + ____searchInputField.ActivateInputField(); ____searchInputField.Select(); } @@ -223,9 +223,7 @@ public static class HideoutSearchPatches [PatchPrefix] public static bool Prefix(ECommand command, ref InputNode.ETranslateResult __result) { - if (command == ECommand.Enter && - EventSystem.current?.currentSelectedGameObject != null && - EventSystem.current.currentSelectedGameObject.GetComponent() != null) + if (command == ECommand.Enter && Plugin.TextboxActive()) { __result = InputNode.ETranslateResult.Block; return false; diff --git a/Patches/InspectWindowResizePatches.cs b/src/Patches/InspectWindowResizePatches.cs similarity index 100% rename from Patches/InspectWindowResizePatches.cs rename to src/Patches/InspectWindowResizePatches.cs diff --git a/Patches/InspectWindowStatsPatches.cs b/src/Patches/InspectWindowStatsPatches.cs similarity index 100% rename from Patches/InspectWindowStatsPatches.cs rename to src/Patches/InspectWindowStatsPatches.cs diff --git a/Patches/KeepMessagesOpenPatches.cs b/src/Patches/KeepMessagesOpenPatches.cs similarity index 100% rename from Patches/KeepMessagesOpenPatches.cs rename to src/Patches/KeepMessagesOpenPatches.cs diff --git a/Patches/KeepOfferWindowOpenPatches.cs b/src/Patches/KeepOfferWindowOpenPatches.cs similarity index 100% rename from Patches/KeepOfferWindowOpenPatches.cs rename to src/Patches/KeepOfferWindowOpenPatches.cs diff --git a/Patches/KeepWindowsOnScreenPatches.cs b/src/Patches/KeepWindowsOnScreenPatches.cs similarity index 100% rename from Patches/KeepWindowsOnScreenPatches.cs rename to src/Patches/KeepWindowsOnScreenPatches.cs diff --git a/src/Patches/LimitDragPatches.cs b/src/Patches/LimitDragPatches.cs new file mode 100644 index 0000000..3795443 --- /dev/null +++ b/src/Patches/LimitDragPatches.cs @@ -0,0 +1,44 @@ +using System; +using System.Reflection; +using EFT.UI; +using HarmonyLib; +using SPT.Reflection.Patching; +using UnityEngine; +using UnityEngine.EventSystems; + +namespace UIFixes; + +public static class LimitDragPatches +{ + public static void Enable() + { + new OnDragEventPatch(typeof(DragTrigger), nameof(DragTrigger.OnDrag)).Enable(); + new OnDragEventPatch(typeof(DragTrigger), nameof(DragTrigger.OnBeginDrag)).Enable(); + new OnDragEventPatch(typeof(DragTrigger), nameof(DragTrigger.OnEndDrag)).Enable(); + + new OnDragEventPatch(typeof(UIDragComponent), "UnityEngine.EventSystems.IDragHandler.OnDrag").Enable(); + new OnDragEventPatch(typeof(UIDragComponent), "UnityEngine.EventSystems.IBeginDragHandler.OnBeginDrag").Enable(); + } + + public class OnDragEventPatch(Type type, string methodName) : ModulePatch + { + private readonly string methodName = methodName; + private readonly Type type = type; + + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(type, methodName); + } + + [PatchPrefix] + public static bool Prefix(PointerEventData eventData) + { + if (!Settings.LimitNonstandardDrags.Value) + { + return true; + } + + return eventData.button == PointerEventData.InputButton.Left && !Input.GetKey(KeyCode.LeftShift) && !Input.GetKey(KeyCode.RightShift); + } + } +} \ No newline at end of file diff --git a/Patches/LoadAmmoInRaidPatches.cs b/src/Patches/LoadAmmoInRaidPatches.cs similarity index 100% rename from Patches/LoadAmmoInRaidPatches.cs rename to src/Patches/LoadAmmoInRaidPatches.cs diff --git a/Patches/LoadMagPresetsPatch.cs b/src/Patches/LoadMagPresetsPatch.cs similarity index 100% rename from Patches/LoadMagPresetsPatch.cs rename to src/Patches/LoadMagPresetsPatch.cs diff --git a/Patches/LoadMultipleMagazinesPatches.cs b/src/Patches/LoadMultipleMagazinesPatches.cs similarity index 100% rename from Patches/LoadMultipleMagazinesPatches.cs rename to src/Patches/LoadMultipleMagazinesPatches.cs diff --git a/Patches/MoveSortingTablePatches.cs b/src/Patches/MoveSortingTablePatches.cs similarity index 100% rename from Patches/MoveSortingTablePatches.cs rename to src/Patches/MoveSortingTablePatches.cs diff --git a/Patches/MoveTaskbarPatch.cs b/src/Patches/MoveTaskbarPatch.cs similarity index 100% rename from Patches/MoveTaskbarPatch.cs rename to src/Patches/MoveTaskbarPatch.cs diff --git a/Patches/MultiSelectPatches.cs b/src/Patches/MultiSelectPatches.cs similarity index 95% rename from Patches/MultiSelectPatches.cs rename to src/Patches/MultiSelectPatches.cs index 011fee0..01aa329 100644 --- a/Patches/MultiSelectPatches.cs +++ b/src/Patches/MultiSelectPatches.cs @@ -38,6 +38,8 @@ public static class MultiSelectPatches private static bool DisableMerge = false; private static bool IgnoreItemParent = false; + private static bool DisableMagnify = false; // Causes issues during multi drag + private static readonly Color ValidMoveColor = new(0.06f, 0.38f, 0.06f, 0.57f); public static void Enable() @@ -58,6 +60,7 @@ public static class MultiSelectPatches new DisableSplitPatch().Enable(); new DisableSplitTargetPatch().Enable(); new FixSearchedContextPatch().Enable(); + new DisableMagnifyPatch().Enable(); // Actions new ItemViewClickPatch().Enable(); @@ -170,7 +173,7 @@ public static class MultiSelectPatches ___ItemController is InventoryControllerClass inventoryController) { SortingTableClass sortingTable = inventoryController.Inventory.SortingTable; - if (sortingTable != null && sortingTable.IsVisible) + if (sortingTable != null && sortingTable.IsVisible && !Plugin.InRaid()) { couldBeSortingTableMove = true; } @@ -286,7 +289,7 @@ public static class MultiSelectPatches } DisableMerge = false; - IgnoreItemParent = true; + IgnoreItemParent = false; if (succeeded) { @@ -460,7 +463,7 @@ public static class MultiSelectPatches } [PatchPrefix] - public static bool Prefix(EItemInfoButton interaction, ItemUiContext ___itemUiContext_1) + public static bool Prefix(BaseItemInfoInteractions __instance, EItemInfoButton interaction, ItemUiContext ___itemUiContext_1) { if (!MultiSelect.Active) { @@ -481,6 +484,12 @@ public static class MultiSelectPatches case EItemInfoButton.Unpack: MultiSelect.UnpackAll(___itemUiContext_1, false); return false; + case EItemInfoButton.AddToWishlist: + MultiSelect.WishlistAll(___itemUiContext_1, __instance, true, false); + return false; + case EItemInfoButton.RemoveFromWishlist: + MultiSelect.WishlistAll(___itemUiContext_1, __instance, false, false); + return false; default: return true; } @@ -674,6 +683,24 @@ public static class MultiSelectPatches } } + // MagnifyIfPossible gets called when a dynamic grid (sorting table) resizes. It causes GridViews to be killed and recreated asynchronously (!) + // This causes all sorts of issues with multiselect move, as there are race conditions and items get dropped and views duplicated + // I'm not 100% sure what it does, it appears to be trying to unload items that may now be out of sight, an optimization I'm willing + // to sacrifice for this actually work properly. + public class DisableMagnifyPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(GridView), nameof(GridView.MagnifyIfPossible), []); + } + + [PatchPrefix] + public static bool Prefix() + { + return !DisableMagnify; + } + } + public class GridViewCanAcceptPatch : ModulePatch { protected override MethodBase GetTargetMethod() @@ -731,6 +758,7 @@ public static class MultiSelectPatches Item targetItem = __instance.method_8(targetItemContext); DisableMerge = targetItem == null; + DisableMagnify = true; bool isGridPlacement = targetItem == null; // If everything selected is the same type and is a stackable type, allow partial success @@ -858,6 +886,8 @@ public static class MultiSelectPatches operations.Pop().Value?.RollBack(); } + DisableMagnify = false; + // result and operation are set to the last one that completed - so success if they all passed, or the first failure return false; } @@ -1453,28 +1483,35 @@ public static class MultiSelectPatches int firstStart = FindOrigin != null ? invertDimensions ? FindOrigin.LocationInGrid.x : FindOrigin.LocationInGrid.y : 0; int secondStart = FindOrigin != null ? invertDimensions ? FindOrigin.LocationInGrid.y : FindOrigin.LocationInGrid.x : 0; - // Walks the first dimension until it finds a row/column with enough space, then walks down that row - // /column until it finds a column/row with enough space + // Walks the first dimension until it finds a row/column with enough space, + // then walks down that row/column until it finds a column/row with enough space // Starts at origin, wraps around for (int i = 0; i < firstDimensionSize; i++) { - int firstDim = (firstStart + i) % firstDimensionSize; - //for (int j = i == firstStart ? secondStart : 0; j + itemSecondSize <= secondDimensionSize; j++) + int firstDim = (firstStart + i) % firstDimensionSize; // loop around from start for (int j = 0; j < secondDimensionSize; j++) { + // second dimension starts at FindOrigin, but after first dimension increases, starts back at 0 + // e.g. there wasn't room on the first row, then on the second row we start with first column int secondDim = firstDim == firstStart ? (secondStart + j) % secondDimensionSize : j; if (secondDim + itemSecondSize > secondDimensionSize) { continue; } - int secondDimOpenSpaces = (invertDimensions ? secondDimensionSpaces[secondDim * firstDimensionSize + firstDim] : secondDimensionSpaces[firstDim * secondDimensionSize + secondDim]); - if (secondDimOpenSpaces >= itemSecondSize || secondDimOpenSpaces == -1) // no idea what -1 means + // Open spaces is a look-ahead number of open spaces in that dimension + // -1 means "infinite", the grid can stretch in that direction (and there's no item further in that direction) + int secondDimOpenSpaces = invertDimensions ? + secondDimensionSpaces[secondDim * firstDimensionSize + firstDim] : + secondDimensionSpaces[firstDim * secondDimensionSize + secondDim]; + if (secondDimOpenSpaces >= itemSecondSize || secondDimOpenSpaces == -1) { bool enoughSpace = true; for (int k = secondDim; enoughSpace && k < secondDim + itemSecondSize; k++) { - int firstDimOpenSpaces = (invertDimensions ? firstDimensionSpaces[k * firstDimensionSize + firstDim] : firstDimensionSpaces[firstDim * secondDimensionSize + k]); + int firstDimOpenSpaces = invertDimensions ? + firstDimensionSpaces[k * firstDimensionSize + firstDim] : + firstDimensionSpaces[firstDim * secondDimensionSize + k]; enoughSpace &= firstDimOpenSpaces >= itemMainSize || firstDimOpenSpaces == -1; } @@ -1535,6 +1572,11 @@ public static class MultiSelectPatches return; } + if (gridAddress == null) + { + return; + } + if (gridAddress.Grid != gridView.Grid) { GridView otherGridView = gridView.transform.parent.GetComponentsInChildren().FirstOrDefault(gv => gv.Grid == gridAddress.Grid); diff --git a/Patches/NoRandomGrenadesPatch.cs b/src/Patches/NoRandomGrenadesPatch.cs similarity index 100% rename from Patches/NoRandomGrenadesPatch.cs rename to src/Patches/NoRandomGrenadesPatch.cs diff --git a/Patches/OpenSortingTablePatches.cs b/src/Patches/OpenSortingTablePatches.cs similarity index 100% rename from Patches/OpenSortingTablePatches.cs rename to src/Patches/OpenSortingTablePatches.cs diff --git a/src/Patches/OperationQueuePatch.cs b/src/Patches/OperationQueuePatch.cs new file mode 100644 index 0000000..bf50b87 --- /dev/null +++ b/src/Patches/OperationQueuePatch.cs @@ -0,0 +1,24 @@ +using System.Reflection; +using HarmonyLib; +using SPT.Reflection.Patching; +using UnityEngine; + +namespace UIFixes; + +public class OperationQueuePatch : ModulePatch +{ + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(ProfileEndpointFactoryAbstractClass), nameof(ProfileEndpointFactoryAbstractClass.TrySendCommands)); + } + + [PatchPrefix] + public static void Prefix(ref float ___float_0) + { + // The target method is hardcoded to 60 seconds. Rather than try to change that, just lie to it about when it last sent + if (Time.realtimeSinceStartup - ___float_0 > Settings.OperationQueueTime.Value) + { + ___float_0 = 0; + } + } +} \ No newline at end of file diff --git a/Patches/PutToolsBackPatch.cs b/src/Patches/PutToolsBackPatch.cs similarity index 100% rename from Patches/PutToolsBackPatch.cs rename to src/Patches/PutToolsBackPatch.cs diff --git a/Patches/QuickAccessPanelPatches.cs b/src/Patches/QuickAccessPanelPatches.cs similarity index 74% rename from Patches/QuickAccessPanelPatches.cs rename to src/Patches/QuickAccessPanelPatches.cs index 580568c..5cf6f34 100644 --- a/Patches/QuickAccessPanelPatches.cs +++ b/src/Patches/QuickAccessPanelPatches.cs @@ -3,10 +3,13 @@ using Comfort.Common; using EFT.InputSystem; using EFT.InventoryLogic; using EFT.UI; +using EFT.UI.DragAndDrop; using EFT.UI.Settings; using HarmonyLib; using SPT.Reflection.Patching; using System.Reflection; +using UnityEngine; +using UnityEngine.UI; namespace UIFixes; @@ -17,6 +20,7 @@ public static class QuickAccessPanelPatches new FixWeaponBindsDisplayPatch().Enable(); new FixVisibilityPatch().Enable(); new TranslateCommandHackPatch().Enable(); + new RotationPatch().Enable(); } public class FixWeaponBindsDisplayPatch : ModulePatch @@ -101,4 +105,34 @@ public static class QuickAccessPanelPatches FixVisibilityPatch.Ignorable = false; } } + + public class RotationPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(QuickSlotItemView), nameof(QuickSlotItemView.UpdateScale)); + } + + [PatchPostfix] + public static void Postfix(QuickSlotItemView __instance, Image ___MainImage) + { + if (__instance.IconScale == null) + { + return; + } + + // Already square items don't need to be rotated. Still need to be scaled though! + XYCellSizeStruct cellSize = __instance.Item.CalculateCellSize(); + if (cellSize.X == cellSize.Y) + { + Transform transform = ___MainImage.transform; + transform.localRotation = Quaternion.identity; + + Vector3 size = ___MainImage.rectTransform.rect.size; + float xScale = __instance.IconScale.Value.x / Mathf.Abs(size.x); + float yScale = __instance.IconScale.Value.y / Mathf.Abs(size.y); + transform.localScale = Vector3.one * Mathf.Min(xScale, yScale); + } + } + } } diff --git a/Patches/RebindGrenadesPatch.cs b/src/Patches/RebindGrenadesPatch.cs similarity index 100% rename from Patches/RebindGrenadesPatch.cs rename to src/Patches/RebindGrenadesPatch.cs diff --git a/Patches/ReloadInPlacePatches.cs b/src/Patches/ReloadInPlacePatches.cs similarity index 77% rename from Patches/ReloadInPlacePatches.cs rename to src/Patches/ReloadInPlacePatches.cs index 1d6dbce..364bb4b 100644 --- a/Patches/ReloadInPlacePatches.cs +++ b/src/Patches/ReloadInPlacePatches.cs @@ -1,4 +1,5 @@ using EFT; +using EFT.InventoryLogic; using EFT.UI; using HarmonyLib; using SPT.Reflection.Patching; @@ -12,6 +13,7 @@ public static class ReloadInPlacePatches { private static bool IsReloading = false; private static MagazineClass FoundMagazine = null; + private static ItemAddress FoundAddress = null; public static void Enable() { @@ -19,6 +21,7 @@ public static class ReloadInPlacePatches new ReloadInPlacePatch().Enable(); new ReloadInPlaceFindMagPatch().Enable(); new ReloadInPlaceFindSpotPatch().Enable(); + new AlwaysSwapPatch().Enable(); // This patches the firearmsController code when you hit R in raid with an external magazine class new SwapIfNoSpacePatch().Enable(); @@ -42,6 +45,7 @@ public static class ReloadInPlacePatches { IsReloading = false; FoundMagazine = null; + FoundAddress = null; } } @@ -55,9 +59,10 @@ public static class ReloadInPlacePatches [PatchPostfix] public static void Postfix(MagazineClass __result) { - if (IsReloading) + if (__result != null && IsReloading) { FoundMagazine = __result; + FoundAddress = FoundMagazine.Parent; } } } @@ -66,7 +71,7 @@ public static class ReloadInPlacePatches { protected override MethodBase GetTargetMethod() { - Type type = typeof(ItemUiContext).GetNestedTypes().Single(t => t.GetField("currentMagazine") != null); + Type type = typeof(ItemUiContext).GetNestedTypes().Single(t => t.GetField("currentMagazine") != null); // ItemUiContext.Class2546 return AccessTools.Method(type, "method_0"); } @@ -99,10 +104,40 @@ public static class ReloadInPlacePatches } } + public class AlwaysSwapPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + Type type = typeof(ItemUiContext).GetNestedTypes().Single(t => t.GetField("func_3") != null); // ItemUiContext.Class2536 + return AccessTools.Method(type, "method_4"); + } + + [PatchPostfix] + public static void Postfix(ItemAddressClass g, ref int __result) + { + if (!Settings.AlwaysSwapMags.Value) + { + return; + } + + if (!g.Equals(FoundAddress)) + { + // Addresses that aren't the found address get massive value increase so found address is sorted first + __result += 1000; + } + } + } + public class SwapIfNoSpacePatch : ModulePatch { protected override MethodBase GetTargetMethod() { + if (Plugin.FikaPresent()) + { + Type type = Type.GetType("Fika.Core.Coop.ClientClasses.CoopClientFirearmController, Fika.Core"); + return AccessTools.Method(type, "ReloadMag"); + } + return AccessTools.Method(typeof(Player.FirearmController), nameof(Player.FirearmController.ReloadMag)); } @@ -126,6 +161,7 @@ public static class ReloadInPlacePatches } InventoryControllerClass controller = __instance.Weapon.Owner as InventoryControllerClass; + ItemAddress magAddress = magazine.Parent; // 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); @@ -137,6 +173,7 @@ public static class ReloadInPlacePatches gridItemAddress = controller.Inventory.Equipment.GetPrioritizedGridsForUnloadedObject(false) .Select(grid => grid.FindLocationForItem(currentMagazine)) .Where(address => address != null) + .OrderByDescending(address => Settings.AlwaysSwapMags.Value && address.Equals(magAddress)) // Prioritize swapping if desired .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 diff --git a/Patches/RememberRepairerPatches.cs b/src/Patches/RememberRepairerPatches.cs similarity index 100% rename from Patches/RememberRepairerPatches.cs rename to src/Patches/RememberRepairerPatches.cs diff --git a/Patches/RemoveDoorActionsPatch.cs b/src/Patches/RemoveDoorActionsPatch.cs similarity index 100% rename from Patches/RemoveDoorActionsPatch.cs rename to src/Patches/RemoveDoorActionsPatch.cs diff --git a/Patches/ReorderGridsPatch.cs b/src/Patches/ReorderGridsPatch.cs similarity index 86% rename from Patches/ReorderGridsPatch.cs rename to src/Patches/ReorderGridsPatch.cs index 019d5f9..abd5901 100644 --- a/Patches/ReorderGridsPatch.cs +++ b/src/Patches/ReorderGridsPatch.cs @@ -64,6 +64,8 @@ public static class ReorderGridsPatches ____presetGridViews = orderedGridView; __instance.SetReordered(false); } + + GridMaps.Remove(compoundItem.TemplateId); } return; @@ -99,26 +101,9 @@ public static class ReorderGridsPatches } var pairs = compoundItem.Grids.Zip(____presetGridViews, (g, gv) => new KeyValuePair(g, gv)); + var sortedPairs = SortGrids(__instance, pairs); - RectTransform parentView = __instance.RectTransform(); - Vector2 parentPosition = parentView.pivot.y == 1 ? parentView.position : new Vector2(parentView.position.x, parentView.position.y + parentView.sizeDelta.y); - Vector2 gridSize = new(64f * parentView.lossyScale.x, 64f * parentView.lossyScale.y); - - var sorted = pairs.OrderBy(pair => - { - var grid = pair.Key; - var gridView = pair.Value; - - float xOffset = gridView.transform.position.x - parentPosition.x; - float yOffset = -(gridView.transform.position.y - parentPosition.y); // invert y since grid coords are upper-left origin - - int x = (int)Math.Round(xOffset / gridSize.x, MidpointRounding.AwayFromZero); - int y = (int)Math.Round(yOffset / gridSize.y, MidpointRounding.AwayFromZero); - - return y * 100 + x; - }); - - GridView[] orderedGridViews = sorted.Select(pair => pair.Value).ToArray(); + GridView[] orderedGridViews = sortedPairs.Select(pair => pair.Value).ToArray(); // Populate the gridmap if (!GridMaps.ContainsKey(compoundItem.TemplateId)) @@ -132,11 +117,41 @@ public static class ReorderGridsPatches GridMaps.Add(compoundItem.TemplateId, map); } - compoundItem.Grids = sorted.Select(pair => pair.Key).ToArray(); + compoundItem.Grids = sortedPairs.Select(pair => pair.Key).ToArray(); ____presetGridViews = orderedGridViews; compoundItem.SetReordered(true); __instance.SetReordered(true); } + + private static IOrderedEnumerable> SortGrids( + TemplatedGridsView __instance, + IEnumerable> pairs) + { + RectTransform parentView = __instance.RectTransform(); + Vector2 parentPosition = parentView.pivot.y == 1 ? parentView.position : new Vector2(parentView.position.x, parentView.position.y + parentView.sizeDelta.y); + Vector2 gridSize = new(64f * parentView.lossyScale.x, 64f * parentView.lossyScale.y); + + int calculateCoords(KeyValuePair pair) + { + var grid = pair.Key; + var gridView = pair.Value; + + float xOffset = gridView.transform.position.x - parentPosition.x; + float yOffset = -(gridView.transform.position.y - parentPosition.y); // invert y since grid coords are upper-left origin + + int x = (int)Math.Round(xOffset / gridSize.x, MidpointRounding.AwayFromZero); + int y = (int)Math.Round(yOffset / gridSize.y, MidpointRounding.AwayFromZero); + + return y * 100 + x; + } + + if (Settings.PrioritizeSmallerGrids.Value) + { + return pairs.OrderBy(pair => pair.Key.GridWidth.Value).ThenBy(pair => pair.Key.GridHeight.Value).ThenBy(calculateCoords); + } + + return pairs.OrderBy(calculateCoords); + } } } diff --git a/Patches/ScrollPatches.cs b/src/Patches/ScrollPatches.cs similarity index 99% rename from Patches/ScrollPatches.cs rename to src/Patches/ScrollPatches.cs index 9ea6d43..cc0876d 100644 --- a/Patches/ScrollPatches.cs +++ b/src/Patches/ScrollPatches.cs @@ -8,6 +8,7 @@ using SPT.Reflection.Patching; using System; using System.Collections.Generic; using System.Reflection; +using TMPro; using UnityEngine; using UnityEngine.Events; using UnityEngine.EventSystems; @@ -34,6 +35,11 @@ public static class ScrollPatches private static bool HandleInput(ScrollRect scrollRect) { + if (Plugin.TextboxActive()) + { + return false; + } + if (scrollRect != null) { if (Settings.UseHomeEnd.Value) diff --git a/src/Patches/SliderPatch.cs b/src/Patches/SliderPatch.cs new file mode 100644 index 0000000..8953d19 --- /dev/null +++ b/src/Patches/SliderPatch.cs @@ -0,0 +1,73 @@ +using EFT.UI; +using HarmonyLib; +using SPT.Reflection.Patching; +using System.Reflection; +using UnityEngine; +using UnityEngine.UI; + +namespace UIFixes; + +public static class SliderPatches +{ + public static void Enable() + { + new IntSliderPatch().Enable(); + new StepSliderPatch().Enable(); + } + + public class IntSliderPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(IntSlider), nameof(IntSlider.Awake)); + } + + [PatchPostfix] + public static void Postfix(Slider ____slider) + { + ____slider.GetOrAddComponent().Init(____slider); + } + } + + public class StepSliderPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(StepSlider), nameof(StepSlider.Awake)); + } + + [PatchPostfix] + public static void Postfix(Slider ____slider) + { + ____slider.GetOrAddComponent().Init(____slider); + } + } + + public class SliderMouseListener : MonoBehaviour + { + private Slider slider; + + public void Init(Slider slider) + { + this.slider = slider; + } + + public void Update() + { + if (slider == null) + { + return; + } + + if (Input.mouseScrollDelta.y > float.Epsilon) + { + slider.value = Mathf.Min(slider.value + 1, slider.maxValue); + + } + else if (Input.mouseScrollDelta.y < -float.Epsilon) + { + slider.value = Mathf.Max(slider.value - 1, slider.minValue); + } + } + } +} \ No newline at end of file diff --git a/Patches/SortPatches.cs b/src/Patches/SortPatches.cs similarity index 100% rename from Patches/SortPatches.cs rename to src/Patches/SortPatches.cs diff --git a/Patches/StackFirItemsPatches.cs b/src/Patches/StackFirItemsPatches.cs similarity index 100% rename from Patches/StackFirItemsPatches.cs rename to src/Patches/StackFirItemsPatches.cs diff --git a/Patches/StackMoveGreedyPatches.cs b/src/Patches/StackMoveGreedyPatches.cs similarity index 100% rename from Patches/StackMoveGreedyPatches.cs rename to src/Patches/StackMoveGreedyPatches.cs diff --git a/Patches/SwapPatches.cs b/src/Patches/SwapPatches.cs similarity index 94% rename from Patches/SwapPatches.cs rename to src/Patches/SwapPatches.cs index af16c2d..87b07d0 100644 --- a/Patches/SwapPatches.cs +++ b/src/Patches/SwapPatches.cs @@ -43,6 +43,7 @@ public static class SwapPatches new DetectGridHighlightPrecheckPatch().Enable(); new DetectSlotHighlightPrecheckPatch().Enable(); new SlotCanAcceptSwapPatch().Enable(); + new WeaponApplyPatch().Enable(); new DetectFilterForSwapPatch().Enable(); new FixNoGridErrorPatch().Enable(); new SwapOperationRaiseEventsPatch().Enable(); @@ -145,7 +146,7 @@ public static class SwapPatches { protected override MethodBase GetTargetMethod() { - return AccessTools.Method(typeof(ItemView), nameof(ItemView.OnDrag)); + return AccessTools.Method(typeof(ItemView), nameof(ItemView.OnBeginDrag)); } [PatchPrefix] @@ -408,6 +409,39 @@ public static class SwapPatches } } + public class WeaponApplyPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(Weapon), nameof(Weapon.Apply)); + } + + // Allow dragging magazines onto weapons and do a mag swap + [PatchPostfix] + public static void Postfix(Weapon __instance, TraderControllerClass itemController, Item item, bool simulate, ref ItemOperation __result) + { + if (!Settings.SwapItems.Value || MultiSelect.Active) + { + return; + } + + // Check if the source container is a non-interactable GridView. Specifically for StashSearch, but may exist in other scenarios? + if (SourceContainer != null && SourceContainer is GridView && new R.GridView(SourceContainer).NonInteractable) + { + return; + } + + if (__result.Succeeded || item is not MagazineClass || __result.Error is not SlotNotEmptyError) + { + return; + } + + Slot magazineSlot = __instance.GetMagazineSlot(); + + __result = InteractionsHandlerClass.Swap(item, magazineSlot.ContainedItem.Parent, magazineSlot.ContainedItem, item.Parent, itemController, simulate); + } + } + // The patched method here is called when iterating over all slots to highlight ones that the dragged item can interact with // Since swap has no special highlight, I just skip the patch here (minor perf savings, plus makes debugging a million times easier) public class DetectGridHighlightPrecheckPatch : ModulePatch diff --git a/Patches/SyncScrollPositionPatches.cs b/src/Patches/SyncScrollPositionPatches.cs similarity index 100% rename from Patches/SyncScrollPositionPatches.cs rename to src/Patches/SyncScrollPositionPatches.cs diff --git a/src/Patches/TacticalBindsPatches.cs b/src/Patches/TacticalBindsPatches.cs new file mode 100644 index 0000000..d0ce65c --- /dev/null +++ b/src/Patches/TacticalBindsPatches.cs @@ -0,0 +1,297 @@ +using Comfort.Common; +using EFT; +using EFT.InputSystem; +using EFT.InventoryLogic; +using HarmonyLib; +using SPT.Reflection.Patching; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using UnityEngine; + +namespace UIFixes; + +public static class TacticalBindsPatches +{ + public static void Enable() + { + new BindableTacticalPatch().Enable(); + new ReachableTacticalPatch().Enable(); + new UseTacticalPatch().Enable(); + + new BindTacticalPatch().Enable(); + new UnbindTacticalPatch().Enable(); + new InitQuickBindsPatch().Enable(); + } + + public class BindableTacticalPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(InventoryControllerClass), nameof(InventoryControllerClass.IsAtBindablePlace)); + } + + [PatchPostfix] + public static void Postfix(InventoryControllerClass __instance, Item item, ref bool __result) + { + if (__result) + { + return; + } + + __result = IsEquippedTacticalDevice(__instance, item); + } + } + + public class ReachableTacticalPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(InventoryControllerClass), nameof(InventoryControllerClass.IsAtReachablePlace)); + } + + [PatchPostfix] + public static void Postfix(InventoryControllerClass __instance, Item item, ref bool __result) + { + if (__result) + { + return; + } + + __result = IsEquippedTacticalDevice(__instance, item); + } + } + + public class UseTacticalPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(Player), nameof(Player.SetQuickSlotItem)); + } + + [PatchPrefix] + public static bool Prefix(Player __instance, EBoundItem quickSlot, Callback callback) + { + Item boundItem = __instance.InventoryControllerClass.Inventory.FastAccess.GetBoundItem(quickSlot); + if (boundItem == null) + { + return true; + } + + LightComponent lightComponent = boundItem.GetItemComponent(); + if (lightComponent != null) + { + ToggleLight(__instance, boundItem, lightComponent); + callback(null); + return false; + } + + NightVisionComponent nightVisionComponent = boundItem.GetItemComponent(); + if (nightVisionComponent != null) + { + Item rootItem = boundItem.GetRootItemNotEquipment(); + if (rootItem is Helmet helmet && + __instance.Inventory.Equipment.GetSlot(EquipmentSlot.Headwear).ContainedItem == helmet) + { + __instance.InventoryControllerClass.TryRunNetworkTransaction( + nightVisionComponent.Togglable.Set(!nightVisionComponent.Togglable.On, true, false)); + } + + callback(null); + return false; + } + + return true; + } + + private static void ToggleLight(Player player, Item boundItem, LightComponent lightComponent) + { + FirearmLightStateStruct lightState = new() + { + Id = lightComponent.Item.Id, + IsActive = lightComponent.IsActive, + LightMode = lightComponent.SelectedMode + }; + + if (IsTacticalModeModifierPressed()) + { + lightState.LightMode++; + } + else + { + lightState.IsActive = !lightState.IsActive; + } + + Item rootItem = boundItem.GetRootItemNotEquipment(); + if (rootItem is Weapon weapon && + player.HandsController is Player.FirearmController firearmController && + firearmController.Item == weapon) + { + firearmController.SetLightsState([lightState], false); + } + + if (rootItem is Helmet helmet && + player.Inventory.Equipment.GetSlot(EquipmentSlot.Headwear).ContainedItem == helmet) + { + lightComponent.SetLightState(lightState); + player.SendHeadlightsPacket(false); + player.SwitchHeadLightsAnimation(); + } + } + } + + public class InitQuickBindsPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(MainMenuController), nameof(MainMenuController.method_5)); + } + + [PatchPostfix] + public static async void Postfix(MainMenuController __instance, Task __result) + { + await __result; + + for (EBoundItem index = EBoundItem.Item4; index <= EBoundItem.Item10; index++) + { + if (__instance.InventoryController.Inventory.FastAccess.BoundItems.ContainsKey(index)) + { + UpdateQuickbindType(__instance.InventoryController.Inventory.FastAccess.BoundItems[index], index); + } + } + + // Will "save" control settings, running GClass1911.UpdateInput, which will set (or unset) toggle/hold behavior + Singleton.Instance.Control.Controller.method_3(); + } + } + + public class BindTacticalPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(GClass2818), nameof(GClass2818.Run)); + } + + [PatchPostfix] + public static void Postfix(InventoryControllerClass controller, Item item, EBoundItem index) + { + UpdateQuickbindType(item, index); + + // Will "save" control settings, running GClass1911.UpdateInput, which will set (or unset) toggle/hold behavior + Singleton.Instance.Control.Controller.method_3(); + } + } + + public class UnbindTacticalPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(GClass2819), nameof(GClass2819.Run)); + } + + [PatchPostfix] + public static void Postfix(InventoryControllerClass controller, Item item, EBoundItem index) + { + Quickbind.SetType(index, Quickbind.ItemType.Other); + + // Will "save" control settings, running GClass1911.UpdateInput, which will set (or unset) toggle/hold behavior + Singleton.Instance.Control.Controller.method_3(); + } + } + + private static bool IsEquippedTacticalDevice(InventoryControllerClass inventoryController, Item item) + { + LightComponent lightComponent = item.GetItemComponent(); + NightVisionComponent nightVisionComponent = item.GetItemComponent(); + if (lightComponent == null && nightVisionComponent == null) + { + return false; + } + + Item rootItem = item.GetRootItemNotEquipment(); + if (rootItem is Weapon || rootItem is Helmet) + { + return inventoryController.Inventory.Equipment.Contains(rootItem); + } + + return false; + } + + private static bool IsTacticalModeModifierPressed() + { + return Settings.TacticalModeModifier.Value switch + { + TacticalBindModifier.Shift => Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift), + TacticalBindModifier.Control => Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl), + TacticalBindModifier.Alt => Input.GetKey(KeyCode.LeftAlt) || Input.GetKey(KeyCode.RightAlt), + _ => false, + }; + } + + private static void UpdateQuickbindType(Item item, EBoundItem index) + { + if (item == null) + { + Quickbind.SetType(index, Quickbind.ItemType.Other); + return; + } + + LightComponent lightComponent = item.GetItemComponent(); + if (lightComponent != null) + { + Item rootItem = item.GetRootItemNotEquipment(); + if (rootItem is Weapon) + { + Quickbind.SetType(index, Quickbind.ItemType.Tactical); + return; + } + + if (rootItem is Helmet) + { + Quickbind.SetType(index, Quickbind.ItemType.Headlight); + return; + } + } + + NightVisionComponent nvComponent = item.GetItemComponent(); + if (nvComponent != null) + { + Quickbind.SetType(index, Quickbind.ItemType.NightVision); + return; + } + + Quickbind.SetType(index, Quickbind.ItemType.Other); + } +} + +public static class Quickbind +{ + public enum ItemType + { + Other, + Tactical, + Headlight, + NightVision + } + + private static readonly Dictionary TacticalQuickbinds = new() + { + { EBoundItem.Item4, ItemType.Other }, + { EBoundItem.Item5, ItemType.Other }, + { EBoundItem.Item6, ItemType.Other }, + { EBoundItem.Item7, ItemType.Other }, + { EBoundItem.Item8, ItemType.Other }, + { EBoundItem.Item9, ItemType.Other }, + { EBoundItem.Item10, ItemType.Other }, + }; + + public static ItemType GetType(EBoundItem index) => TacticalQuickbinds[index]; + public static void SetType(EBoundItem index, ItemType type) => TacticalQuickbinds[index] = type; + + public static ItemType GetType(EGameKey gameKey) + { + int offset = gameKey - EGameKey.Slot4; + return GetType(EBoundItem.Item4 + offset); + } +} \ No newline at end of file diff --git a/src/Patches/TagPatches.cs b/src/Patches/TagPatches.cs new file mode 100644 index 0000000..c53679c --- /dev/null +++ b/src/Patches/TagPatches.cs @@ -0,0 +1,75 @@ +using EFT.UI; +using EFT.UI.DragAndDrop; +using HarmonyLib; +using SPT.Reflection.Patching; +using System.Reflection; +using System.Threading.Tasks; +using TMPro; +using UnityEngine; +using UnityEngine.UI; + +namespace UIFixes; + +public static class TagPatches +{ + public static void Enable() + { + new OnEnterPatch().Enable(); + new TagsOverCaptionsPatch().Enable(); + } + + public class OnEnterPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.DeclaredMethod(typeof(EditTagWindow), nameof(EditTagWindow.Show)); + } + + [PatchPostfix] + public static void Postfix(EditTagWindow __instance, ValidationInputField ____tagInput) + { + ____tagInput.onSubmit.AddListener(value => __instance.method_4()); + ____tagInput.ActivateInputField(); + ____tagInput.Select(); + } + } + + public class TagsOverCaptionsPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(GridItemView), nameof(GridItemView.method_21)); + } + + [PatchPostfix] + public static async void Postfix(GridItemView __instance, TextMeshProUGUI ___TagName, TextMeshProUGUI ___Caption, Image ____tagColor, Task __result) + { + await __result; + + // Rerun logic with preferred priority. Running again rather than prefix overwrite because this also fixes the existing race condition + ___TagName.gameObject.SetActive(false); + ___Caption.gameObject.SetActive(true); + await Task.Yield(); + RectTransform tagTransform = ____tagColor.rectTransform; + float tagSpace = __instance.RectTransform.sizeDelta.x - ___Caption.renderedWidth - 2f; + if (tagSpace < 40f) + { + tagTransform.sizeDelta = new Vector2(__instance.RectTransform.sizeDelta.x, tagTransform.sizeDelta.y); + if (Settings.TagsOverCaptions.Value) + { + ___TagName.gameObject.SetActive(true); + float tagSize = Mathf.Clamp(___TagName.preferredWidth + 12f, 40f, __instance.RectTransform.sizeDelta.x - 2f); + tagTransform.sizeDelta = new Vector2(tagSize, ____tagColor.rectTransform.sizeDelta.y); + + ___Caption.gameObject.SetActive(false); + } + } + else + { + ___TagName.gameObject.SetActive(true); + float tagSize = Mathf.Clamp(___TagName.preferredWidth + 12f, 40f, tagSpace); + tagTransform.sizeDelta = new Vector2(tagSize, ____tagColor.rectTransform.sizeDelta.y); + } + } + } +} \ No newline at end of file diff --git a/Patches/TradingAutoSwitchPatches.cs b/src/Patches/TradingAutoSwitchPatches.cs similarity index 66% rename from Patches/TradingAutoSwitchPatches.cs rename to src/Patches/TradingAutoSwitchPatches.cs index 7926174..e89aba9 100644 --- a/Patches/TradingAutoSwitchPatches.cs +++ b/src/Patches/TradingAutoSwitchPatches.cs @@ -3,6 +3,7 @@ using EFT.UI; using EFT.UI.DragAndDrop; using HarmonyLib; using SPT.Reflection.Patching; +using System; using System.Reflection; using UnityEngine; using UnityEngine.EventSystems; @@ -58,44 +59,56 @@ public static class TradingAutoSwitchPatches TradingItemView __instance, PointerEventData.InputButton button, bool doubleClick, - ETradingItemViewType ___etradingItemViewType_0, bool ___bool_8) + ETradingItemViewType ___etradingItemViewType_0, + bool ___bool_8) { - if (!Settings.AutoSwitchTrading.Value) + if (!Settings.AutoSwitchTrading.Value || SellTab == null || BuyTab == null) + { + return true; + } + + var assortmentController = __instance.R().TraderAssortmentController; + if (assortmentController == null) { 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.TraderAssortmentController.QuickFindTradingAppropriatePlace(__instance.Item, null)) + try { - __instance.ItemContext.CloseDependentWindows(); - __instance.HideTooltip(); - Singleton.Instance.PlayItemSound(__instance.Item.ItemSound, EInventorySoundType.pickup, false); + if (!___bool_8 && ctrlPressed && assortmentController.QuickFindTradingAppropriatePlace(__instance.Item, null)) + { + __instance.ItemContext?.CloseDependentWindows(); + __instance.HideTooltip(); + Singleton.Instance.PlayItemSound(__instance.Item.ItemSound, EInventorySoundType.pickup, false); - SellTab.OnPointerClick(null); + SellTab.OnPointerClick(null); - return false; + return false; + } + + if (___bool_8) + { + assortmentController.SelectItem(__instance.Item); + + BuyTab.OnPointerClick(null); + + return false; + } } - - if (___bool_8) + catch (Exception e) { - tradingItemView.TraderAssortmentController.SelectItem(__instance.Item); - - BuyTab.OnPointerClick(null); - - return false; + Logger.LogError(e); } return true; diff --git a/Patches/TransferConfirmPatch.cs b/src/Patches/TransferConfirmPatch.cs similarity index 100% rename from Patches/TransferConfirmPatch.cs rename to src/Patches/TransferConfirmPatch.cs diff --git a/src/Patches/UnloadAmmoPatches.cs b/src/Patches/UnloadAmmoPatches.cs new file mode 100644 index 0000000..7de05b6 --- /dev/null +++ b/src/Patches/UnloadAmmoPatches.cs @@ -0,0 +1,231 @@ +using Comfort.Common; +using EFT; +using EFT.Communications; +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; +using System.Reflection; +using System.Threading.Tasks; + +namespace UIFixes; + +public static class UnloadAmmoPatches +{ + public static void Enable() + { + new TradingPlayerPatch().Enable(); + new TransferPlayerPatch().Enable(); + new UnloadScavTransferPatch().Enable(); + new NoScavStashPatch().Enable(); + + new UnloadAmmoBoxPatch().Enable(); + } + + public class TradingPlayerPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.DeclaredProperty(R.TradingInteractions.Type, "AvailableInteractions").GetMethod; + } + + [PatchPostfix] + public static void Postfix(ref IEnumerable __result) + { + var list = __result.ToList(); + list.Insert(list.IndexOf(EItemInfoButton.Repair), EItemInfoButton.UnloadAmmo); + __result = list; + } + } + + public class TransferPlayerPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.DeclaredProperty(R.TransferInteractions.Type, "AvailableInteractions").GetMethod; + } + + [PatchPostfix] + public static void Postfix(ref IEnumerable __result) + { + var list = __result.ToList(); + list.Insert(list.IndexOf(EItemInfoButton.Fold), EItemInfoButton.UnloadAmmo); + __result = list; + } + } + + // The scav inventory screen has two inventory controllers, the player's and the scav's. Unload always uses the player's, which causes issues + // because the bullets are never marked as "known" by the scav, so if you click back/next they show up as unsearched, with no way to search + // This patch forces unload to use the controller of whoever owns the magazine. + public class UnloadScavTransferPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.DeclaredMethod(typeof(InventoryControllerClass), nameof(InventoryControllerClass.UnloadMagazine)); + } + + [PatchPrefix] + public static bool Prefix(InventoryControllerClass __instance, MagazineClass magazine, ref Task __result) + { + if (ItemUiContext.Instance.ContextType != EItemUiContextType.ScavengerInventoryScreen) + { + return true; + } + + if (magazine.Owner == __instance || magazine.Owner is not InventoryControllerClass ownerInventoryController) + { + return true; + } + + __result = ownerInventoryController.UnloadMagazine(magazine); + return false; + } + } + + // Because of the above patch, unload uses the scav's inventory controller, which provides locations to unload ammo: equipment and stash. Why do scavs have a stash? + // If the equipment is full, the bullets would go to the scav stash, aka a black hole, and are never seen again. + // Remove the scav's stash + public class NoScavStashPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + Type type = typeof(ScavengerInventoryScreen).GetNestedTypes().Single(t => t.GetField("ScavController") != null); // ScavengerInventoryScreen.GClass3156 + return AccessTools.GetDeclaredConstructors(type).Single(); + } + + [PatchPrefix] + public static void Prefix(InventoryContainerClass scavController) + { + scavController.Inventory.Stash = null; + } + } + + public class UnloadAmmoBoxPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(ItemUiContext), nameof(ItemUiContext.UnloadAmmo)); + } + + [PatchPrefix] + public static bool Prefix(Item item, ref Task __result, InventoryContainerClass ___inventoryControllerClass) + { + if (!Settings.UnloadAmmoBoxInPlace.Value || item is not AmmoBox ammoBox) + { + return true; + } + + if (ammoBox.Cartridges.Last is not BulletClass lastBullet) + { + return true; + } + + __result = UnloadAmmoBox(ammoBox, ___inventoryControllerClass); + return false; + } + + private static async Task UnloadAmmoBox(AmmoBox ammoBox, InventoryControllerClass inventoryController) + { + BulletClass lastBullet = ammoBox.Cartridges.Last as BulletClass; + IEnumerable containers = inventoryController.Inventory.Stash != null ? + [inventoryController.Inventory.Equipment, inventoryController.Inventory.Stash] : + [inventoryController.Inventory.Equipment]; + + // Explicitly add the current parent before its moved. IgnoreParentItem will be sent along later + containers = containers.Prepend(ammoBox.Parent.Container.ParentItem as LootItemClass); + + // Move the box to a temporary stash so it can unload in place + TraderControllerClass tempController = GetTempController(); + StashClass tempStash = tempController.RootItem as StashClass; + var moveOperation = InteractionsHandlerClass.Move(ammoBox, tempStash.Grid.FindLocationForItem(ammoBox), inventoryController, true); + if (moveOperation.Succeeded) + { + IResult networkResult = await inventoryController.TryRunNetworkTransaction(moveOperation); + if (networkResult.Failed) + { + moveOperation = new GClass3370(networkResult.Error); + } + + // Surprise! The operation is STILL not done. + await Task.Yield(); + } + + if (moveOperation.Failed) + { + NotificationManagerClass.DisplayWarningNotification(moveOperation.Error.ToString(), ENotificationDurationType.Default); + return; + } + + bool unloadedAny = false; + ItemOperation operation = default; + for (BulletClass bullet = lastBullet; bullet != null; bullet = ammoBox.Cartridges.Last as BulletClass) + { + operation = InteractionsHandlerClass.QuickFindAppropriatePlace( + bullet, + inventoryController, + containers, + InteractionsHandlerClass.EMoveItemOrder.UnloadAmmo | InteractionsHandlerClass.EMoveItemOrder.IgnoreItemParent, + true); + + if (operation.Failed) + { + break; + } + + unloadedAny = true; + + IResult networkResult = await inventoryController.TryRunNetworkTransaction(operation); + if (networkResult.Failed) + { + operation = new GClass3370(networkResult.Error); + break; + } + + if (operation.Value is GInterface343 raisable) + { + raisable.TargetItem.RaiseRefreshEvent(false, true); + } + + // Surprise! The operation STILL IS NOT DONE. + await Task.Yield(); + } + + if (unloadedAny && Singleton.Instantiated) + { + Singleton.Instance.PlayItemSound(lastBullet.ItemSound, EInventorySoundType.drop, false); + } + + if (operation.Succeeded) + { + inventoryController.DestroyItem(ammoBox); + } + else + { + ammoBox.RaiseRefreshEvent(false, true); + } + + if (operation.Failed) + { + NotificationManagerClass.DisplayWarningNotification(operation.Error.ToString(), ENotificationDurationType.Default); + } + } + + private static TraderControllerClass GetTempController() + { + if (Plugin.InRaid()) + { + return Singleton.Instance.R().TraderController; + } + else + { + var profile = PatchConstants.BackEndSession.Profile; + StashClass fakeStash = Singleton.Instance.CreateFakeStash(); + return new TraderControllerClass(fakeStash, profile.ProfileId, profile.Nickname); + } + } + } +} diff --git a/Patches/UnlockCursorPatch.cs b/src/Patches/UnlockCursorPatch.cs similarity index 100% rename from Patches/UnlockCursorPatch.cs rename to src/Patches/UnlockCursorPatch.cs diff --git a/src/Patches/WeaponModdingPatches.cs b/src/Patches/WeaponModdingPatches.cs new file mode 100644 index 0000000..f7e5f4b --- /dev/null +++ b/src/Patches/WeaponModdingPatches.cs @@ -0,0 +1,535 @@ +using Comfort.Common; +using EFT; +using EFT.InventoryLogic; +using EFT.UI.DragAndDrop; +using HarmonyLib; +using SPT.Reflection.Patching; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEngine; + +namespace UIFixes; + +public static class WeaponModdingPatches +{ + private const string MultitoolId = "544fb5454bdc2df8738b456a"; + private static readonly string[] EquippedSlots = ["FirstPrimaryWeapon", "SecondPrimaryWeapon", "Holster"]; + + public static void Enable() + { + new ResizePatch().Enable(); + new ResizeHelperPatch().Enable(); + new ResizeOperationRollbackPatch().Enable(); + new MoveBeforeNetworkTransactionPatch().Enable(); + + new ModEquippedPatch().Enable(); + new InspectLockedPatch().Enable(); + new ModCanBeMovedPatch().Enable(); + new ModCanDetachPatch().Enable(); + new ModCanApplyPatch().Enable(); + new ModRaidModdablePatch().Enable(); + new EmptyVitalPartsPatch().Enable(); + } + + public class ResizePatch : ModulePatch + { + public static MoveOperation NecessaryMoveOperation = null; + + private static bool InPatch = false; + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(StashGridClass), nameof(StashGridClass.Resize)); + } + + [PatchPostfix] + public static void Postfix(StashGridClass __instance, Item item, XYCellSizeStruct oldSize, XYCellSizeStruct newSize, bool simulate, ref bool __result) + { + if (__result || InPatch) + { + return; + } + + if (item.Owner is not InventoryControllerClass inventoryController) + { + return; + } + + LocationInGrid itemLocation = __instance.GetItemLocation(item); + + // The sizes passed in are the template sizes, need to make match the item's rotation + XYCellSizeStruct actualOldSize = itemLocation.r.Rotate(oldSize); + XYCellSizeStruct actualNewSize = itemLocation.r.Rotate(newSize); + + // Figure out which direction(s) its growing + int horizontalGrowth = actualNewSize.X - actualOldSize.X; + int verticalGrowth = actualNewSize.Y - actualOldSize.Y; + + // Can't move up/left more than the position + horizontalGrowth = Math.Min(horizontalGrowth, itemLocation.x); + verticalGrowth = Math.Min(verticalGrowth, itemLocation.y); + + // Try moving it + try + { + InPatch = true; + for (int x = 0; x <= horizontalGrowth; x++) + { + for (int y = 0; y <= verticalGrowth; y++) + { + if (x + y == 0) + { + continue; + } + + LocationInGrid newLocation = new(itemLocation.x - x, itemLocation.y - y, itemLocation.r); + ItemAddress newAddress = new GridItemAddress(__instance, newLocation); + + var moveOperation = InteractionsHandlerClass.Move(item, newAddress, inventoryController, false); + if (moveOperation.Failed || moveOperation.Value == null) + { + continue; + } + + bool resizeResult = __instance.Resize(item, oldSize, newSize, simulate); + + // If simulating, rollback. Note that for some reason, only the Fold case even uses simulate + // The other cases (adding a mod, etc) never simulate, and then rollback later. Likely because there is normally + // no server side-effect of a resize - the only effect is updating the grid's free/used map. + if (simulate || !resizeResult) + { + moveOperation.Value.RollBack(); + } + + if (resizeResult) + { + // Stash the move operation so it can be executed or rolled back later + NecessaryMoveOperation = moveOperation.Value; + + __result = true; + return; + } + } + } + } + finally + { + InPatch = false; + } + } + } + + public class ResizeHelperPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(InteractionsHandlerClass), nameof(InteractionsHandlerClass.Resize_Helper)); + } + + [PatchPostfix] + public static void Postfix(ref GStruct414 __result) + { + if (__result.Failed || __result.Value == null) + { + return; + } + + if (ResizePatch.NecessaryMoveOperation != null) + { + __result.Value.SetMoveOperation(ResizePatch.NecessaryMoveOperation); + ResizePatch.NecessaryMoveOperation = null; + } + } + } + + public class ResizeOperationRollbackPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(ResizeOperation), nameof(ResizeOperation.RollBack)); + } + + [PatchPostfix] + public static void Postfix(ResizeOperation __instance) + { + MoveOperation moveOperation = __instance.GetMoveOperation(); + if (moveOperation != null) + { + moveOperation.RollBack(); + } + } + } + + public class MoveBeforeNetworkTransactionPatch : ModulePatch + { + private static bool InPatch = false; + + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(TraderControllerClass), nameof(TraderControllerClass.RunNetworkTransaction)); + } + + [PatchPrefix] + public static void Prefix(TraderControllerClass __instance, IRaiseEvents operationResult) + { + if (InPatch) + { + return; + } + + MoveOperation extraOperation = null; + if (operationResult is MoveOperation moveOperation) + { + extraOperation = moveOperation.R().AddOperation?.R().ResizeOperation?.GetMoveOperation(); + } + else if (operationResult is FoldOperation foldOperation) + { + extraOperation = foldOperation.ResizeResult?.GetMoveOperation(); + } + + if (extraOperation != null) + { + try + { + InPatch = true; + __instance.RunNetworkTransaction(extraOperation); + } + finally + { + InPatch = false; + } + } + } + } + + public class ModEquippedPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(R.ContextMenuHelper.Type, "IsInteractive"); + } + + // Enable/disable options in the context menu + [PatchPostfix] + public static void Postfix(EItemInfoButton button, ref IResult __result, Item ___item_0) + { + // These two are only visible out of raid, enable them + if (Settings.ModifyEquippedWeapons.Value && (button == EItemInfoButton.Modding || button == EItemInfoButton.EditBuild)) + { + if (__result.Succeed || !Singleton.Instance.HasBonus(EBonusType.UnlockWeaponModification)) + { + return; + } + + __result = SuccessfulResult.New; + return; + } + + // This is surprisingly active in raid? Only enable out of raid. + if (button == EItemInfoButton.Disassemble) + { + if (!Plugin.InRaid() && Settings.ModifyEquippedWeapons.Value) + { + __result = SuccessfulResult.New; + return; + } + } + + // These are on mods; normally the context menu is disabled so these are individually not disabled + // Need to do the disabling as appropriate + if (___item_0 is Mod mod && (button == EItemInfoButton.Uninstall || button == EItemInfoButton.Discard)) + { + if (!CanModify(mod, out string error)) + { + __result = new FailedResult(error); + return; + } + } + } + } + + public class InspectLockedPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(ModSlotView), nameof(ModSlotView.method_14)); + } + + // Enable context menu on normally unmoddable slots, maybe keep them gray + [PatchPostfix] + public static void Postfix(ModSlotView __instance, ref bool ___bool_1, CanvasGroup ____canvasGroup) + { + // Keep it grayed out and warning text if its not draggable, even if context menu is enabled + if (__instance.Slot.ContainedItem is Mod mod && CanModify(mod, out string error)) + { + ___bool_1 = false; + ____canvasGroup.alpha = 1f; + } + + ____canvasGroup.blocksRaycasts = true; + ____canvasGroup.interactable = true; + } + } + + public class ModCanBeMovedPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(Mod), nameof(Mod.CanBeMoved)); + } + + // As far as I can tell this never gets called, but hey + [PatchPostfix] + public static void Postfix(Mod __instance, IContainer toContainer, ref GStruct416 __result) + { + if (__result.Succeeded) + { + return; + } + + if (!CanModify(__instance, out string itemError)) + { + return; + } + + if (toContainer is not Slot toSlot || !CanModify(R.SlotItemAddress.Create(toSlot), out string slotError)) + { + return; + } + + __result = true; + } + } + + public class ModCanDetachPatch : ModulePatch + { + private static Type TargetMethodReturnType; + + protected override MethodBase GetTargetMethod() + { + MethodInfo method = AccessTools.Method(typeof(InteractionsHandlerClass), nameof(InteractionsHandlerClass.smethod_1)); + TargetMethodReturnType = method.ReturnType; + return method; + } + + // This gets invoked when dragging items around between slots + [PatchPostfix] + public static void Postfix(Item item, ItemAddress to, TraderControllerClass itemController, ref GStruct416 __result) + { + if (item is not Mod mod) + { + return; + } + + if (Plugin.InRaid() && __result.Succeeded) + { + // In raid successes are all fine + return; + } + + bool canModify = CanModify(mod, out string error) && CanModify(to, out error); + if (canModify == __result.Succeeded) + { + // In agreement, just check the error is best to show + if (Settings.ModifyRaidWeapons.Value == ModRaidWeapon.WithTool && + (__result.Error is NotModdableInRaidError || __result.Error is ModVitalPartInRaidError)) + { + // Double check this is an unequipped weapon + Weapon weapon = item.GetRootItemNotEquipment() as Weapon ?? to.GetRootItemNotEquipment() as Weapon; + if (weapon != null && !EquippedSlots.Contains(weapon.Parent.Container.ID)) + { + __result = new MultitoolNeededError(item); + } + } + } + + if (__result.Failed && canModify) + { + // Override result with success if DestinationCheck passes + var destinationCheck = InteractionsHandlerClass.DestinationCheck(item.Parent, to, itemController.OwnerType); + if (destinationCheck.Failed) + { + return; + } + + __result = default; + } + else if (__result.Succeeded && !canModify) + { + // Out of raid, likely dragging a mod that was previously non-interactive, need to actually block + __result = new VitalPartInHandsError(); + } + } + + private class VitalPartInHandsError : InventoryError + { + public override string GetLocalizedDescription() + { + return "Vital mod weapon in hands".Localized(); + } + + public override string ToString() + { + return "Vital mod weapon in hands"; + } + } + } + + public class ModCanApplyPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(LootItemClass), nameof(LootItemClass.Apply)); + } + + // Gets called when dropping mods on top of weapons + [PatchPrefix] + public static void Prefix(LootItemClass __instance, Item item) + { + if (!Plugin.InRaid()) + { + return; + } + + if (__instance is not Weapon weapon || item is not Mod mod || EquippedSlots.Contains(weapon.Parent.Container.ID)) + { + return; + } + + if (CanModify(mod, out string error)) + { + ModRaidModdablePatch.Override = true; + EmptyVitalPartsPatch.Override = true; + } + } + + [PatchPostfix] + public static void Postfix(LootItemClass __instance, ref ItemOperation __result) + { + ModRaidModdablePatch.Override = false; + EmptyVitalPartsPatch.Override = false; + + // If setting is multitool, may need to change some errors + if (Settings.ModifyRaidWeapons.Value == ModRaidWeapon.WithTool) + { + if (__instance is not Weapon weapon || EquippedSlots.Contains(weapon.Parent.Container.ID)) + { + return; + } + + if (__result.Error is NotModdableInRaidError || __result.Error is ModVitalPartInRaidError) + { + __result = new MultitoolNeededError(__instance); + } + } + } + } + + public class ModRaidModdablePatch : ModulePatch + { + public static bool Override = false; + + protected override MethodBase GetTargetMethod() + { + return AccessTools.Property(typeof(Mod), nameof(Mod.RaidModdable)).GetMethod; + } + + [PatchPostfix] + public static void Postfix(ref bool __result) + { + __result = __result || Override; + } + } + + public class EmptyVitalPartsPatch : ModulePatch + { + public static bool Override = false; + + protected override MethodBase GetTargetMethod() + { + return AccessTools.Property(typeof(LootItemClass), nameof(LootItemClass.VitalParts)).GetMethod; + } + + [PatchPrefix] + public static bool Prefix(ref IEnumerable __result) + { + if (Override) + { + __result = []; + return false; + } + + return true; + } + } + + private static bool CanModify(Mod item, out string error) + { + return CanModify(item, item?.Parent, out error); + } + + private static bool CanModify(ItemAddress itemAddress, out string error) + { + return CanModify(null, itemAddress, out error); + } + + private static bool CanModify(Mod item, ItemAddress itemAddress, out string error) + { + error = null; + + // If it's raidmoddable and not in a vital slot, then it's all good + if ((item == null || item.RaidModdable) && + (!R.SlotItemAddress.Type.IsAssignableFrom(itemAddress.GetType()) || !new R.SlotItemAddress(itemAddress).Slot.Required)) + { + return true; + } + + Item rootItem = itemAddress.GetRootItemNotEquipment(); + if (rootItem is not Weapon weapon || weapon.CurrentAddress == null) + { + return true; + } + + // Can't modify weapon in hands + if (EquippedSlots.Contains(weapon.Parent.Container.ID)) + { + if (Plugin.InRaid()) + { + error = "Inventory Errors/Not moddable in raid"; + return false; + } + + + if (!Settings.ModifyEquippedWeapons.Value) + { + error = "Vital mod weapon in hands"; + return false; + } + } + + // Not in raid, not in hands: anything is possible + if (!Plugin.InRaid()) + { + return true; + } + + if (Settings.ModifyRaidWeapons.Value == ModRaidWeapon.Never) + { + error = "Inventory Errors/Not moddable in raid"; + return false; + } + + Player player = Singleton.Instance.MainPlayer; + bool hasMultitool = player.Equipment.GetAllItems().Any(i => i.TemplateId == MultitoolId); + + if (Settings.ModifyRaidWeapons.Value == ModRaidWeapon.WithTool && !hasMultitool) + { + error = "Inventory Errors/Not moddable without multitool"; + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/Patches/WeaponPresetConfirmPatches.cs b/src/Patches/WeaponPresetConfirmPatches.cs similarity index 100% rename from Patches/WeaponPresetConfirmPatches.cs rename to src/Patches/WeaponPresetConfirmPatches.cs diff --git a/Patches/WeaponZoomPatches.cs b/src/Patches/WeaponZoomPatches.cs similarity index 100% rename from Patches/WeaponZoomPatches.cs rename to src/Patches/WeaponZoomPatches.cs diff --git a/Plugin.cs b/src/Plugin.cs similarity index 68% rename from Plugin.cs rename to src/Plugin.cs index 5072daa..ca3f3db 100644 --- a/Plugin.cs +++ b/src/Plugin.cs @@ -1,10 +1,14 @@ using BepInEx; +using BepInEx.Bootstrap; using Comfort.Common; using EFT; +using TMPro; +using UnityEngine.EventSystems; namespace UIFixes; [BepInPlugin(PluginInfo.PLUGIN_GUID, PluginInfo.PLUGIN_NAME, PluginInfo.PLUGIN_VERSION)] +[BepInDependency("com.fika.core", BepInDependency.DependencyFlags.SoftDependency)] public class Plugin : BaseUnityPlugin { public void Awake() @@ -68,6 +72,14 @@ public class Plugin : BaseUnityPlugin ReloadInPlacePatches.Enable(); BarterOfferPatches.Enable(); new UnlockCursorPatch().Enable(); + LimitDragPatches.Enable(); + new HideoutCameraPatch().Enable(); + WeaponModdingPatches.Enable(); + TagPatches.Enable(); + TacticalBindsPatches.Enable(); + AddOfferContextMenuPatches.Enable(); + new OperationQueuePatch().Enable(); + SliderPatches.Enable(); } public static bool InRaid() @@ -75,4 +87,34 @@ public class Plugin : BaseUnityPlugin bool? inRaid = Singleton.Instance?.InRaid; return inRaid.HasValue && inRaid.Value; } + + public static bool TextboxActive() + { + return EventSystem.current?.currentSelectedGameObject != null && + EventSystem.current.currentSelectedGameObject.GetComponent() != null; + } + + private static bool? IsFikaPresent; + + public static bool FikaPresent() + { + if (!IsFikaPresent.HasValue) + { + IsFikaPresent = Chainloader.PluginInfos.ContainsKey("com.fika.core"); + } + + return IsFikaPresent.Value; + } + + private static bool? IsMergeConsumablesPresent; + + public static bool MergeConsumablesPresent() + { + if (!IsMergeConsumablesPresent.HasValue) + { + IsMergeConsumablesPresent = Chainloader.PluginInfos.ContainsKey("com.lacyway.mc"); + } + + return IsMergeConsumablesPresent.Value; + } } diff --git a/R.cs b/src/R.cs similarity index 95% rename from R.cs rename to src/R.cs index 6ea5f3d..1d2ad26 100644 --- a/R.cs +++ b/src/R.cs @@ -67,6 +67,9 @@ public static class R InventoryScreen.InitTypes(); ScavengerInventoryScreen.InitTypes(); LocalizedText.InitTypes(); + GameWorld.InitTypes(); + MoveOperationResult.InitTypes(); + AddOperationResult.InitTypes(); } public abstract class Wrapper(object value) @@ -733,10 +736,12 @@ public static class R public class InventoryInteractions(object value) : Wrapper(value) { public static Type Type { get; private set; } + public static Type CompleteType { get; private set; } public static void InitTypes() { Type = PatchConstants.EftTypes.Single(t => t.GetField("HIDEOUT_WEAPON_MODIFICATION_REQUIRED") != null); // GClass3045 + CompleteType = PatchConstants.EftTypes.Single(t => t != Type && Type.IsAssignableFrom(t)); // GClass3046 } } @@ -867,6 +872,48 @@ public static class R set { StringCaseField.SetValue(Value, value); } } } + + public class GameWorld(object value) : Wrapper(value) + { + public static Type Type { get; private set; } + private static FieldInfo TraderControllerField; + + public static void InitTypes() + { + Type = typeof(EFT.GameWorld); + TraderControllerField = AccessTools.Field(Type, "traderControllerClass"); + } + + public TraderControllerClass TraderController { get { return (TraderControllerClass)TraderControllerField.GetValue(Value); } } + } + + public class MoveOperationResult(object value) : Wrapper(value) + { + public static Type Type { get; private set; } + private static FieldInfo AddOperationField; + + public static void InitTypes() + { + Type = typeof(MoveOperation); + AddOperationField = AccessTools.Field(Type, "gclass2798_0"); + } + + public AddOperation AddOperation { get { return (AddOperation)AddOperationField.GetValue(Value); } } + } + + public class AddOperationResult(object value) : Wrapper(value) + { + public static Type Type { get; private set; } + private static FieldInfo ResizeOperationField; + + public static void InitTypes() + { + Type = typeof(AddOperation); + ResizeOperationField = AccessTools.Field(Type, "gclass2803_0"); + } + + public ResizeOperation ResizeOperation { get { return (ResizeOperation)ResizeOperationField.GetValue(Value); } } + } } public static class RExtentensions @@ -898,4 +945,7 @@ public static class RExtentensions public static R.InventoryScreen R(this InventoryScreen value) => new(value); public static R.ScavengerInventoryScreen R(this ScavengerInventoryScreen value) => new(value); public static R.LocalizedText R(this LocalizedText value) => new(value); + public static R.GameWorld R(this GameWorld value) => new(value); + public static R.MoveOperationResult R(this MoveOperation value) => new(value); + public static R.AddOperationResult R(this AddOperation value) => new(value); } diff --git a/src/SearchKeyListener.cs b/src/SearchKeyListener.cs new file mode 100644 index 0000000..ad73078 --- /dev/null +++ b/src/SearchKeyListener.cs @@ -0,0 +1,20 @@ +using TMPro; +using UnityEngine; + +namespace UIFixes; + +public class SearchKeyListener : MonoBehaviour +{ + public void Update() + { + if (Settings.SearchKeyBind.Value.IsDown()) + { + TMP_InputField searchField = GetComponent(); + if (searchField != null) + { + searchField.ActivateInputField(); + searchField.Select(); + } + } + } +} \ No newline at end of file diff --git a/Settings.cs b/src/Settings.cs similarity index 78% rename from Settings.cs rename to src/Settings.cs index 7d1cc59..4bb9dc7 100644 --- a/Settings.cs +++ b/src/Settings.cs @@ -1,4 +1,5 @@ -using BepInEx.Configuration; +using BepInEx.Bootstrap; +using BepInEx.Configuration; using System; using System.Collections.Generic; using System.ComponentModel; @@ -37,6 +38,29 @@ internal enum SortingTableDisplay Both } +internal enum AutoFleaPrice +{ + None, + Minimum, + Average, + Maximum +} + +internal enum TacticalBindModifier +{ + Shift, + Control, + Alt +} + +internal enum ModRaidWeapon +{ + Never, + [Description("With Multitool")] + WithTool, + Always +} + internal class Settings { // Categories @@ -56,25 +80,36 @@ internal class Settings public static ConfigEntry AutoSwitchTrading { get; set; } public static ConfigEntry ClickOutOfDialogs { get; set; } // Advanced public static ConfigEntry RestoreAsyncScrollPositions { get; set; } // Advanced + public static ConfigEntry OperationQueueTime { get; set; } // Advanced // Input public static ConfigEntry ToggleOrHoldAim { get; set; } + public static ConfigEntry ToggleOrHoldSprint { get; set; } + public static ConfigEntry ToggleOrHoldTactical { get; set; } + public static ConfigEntry ToggleOrHoldHeadlight { get; set; } + public static ConfigEntry ToggleOrHoldGoggles { get; set; } + public static ConfigEntry TacticalModeModifier { get; set; } public static ConfigEntry UseHomeEnd { get; set; } public static ConfigEntry RebindPageUpDown { get; set; } public static ConfigEntry MouseScrollMulti { get; set; } + public static ConfigEntry UseRaidMouseScrollMulti { get; set; } // Advanced + public static ConfigEntry MouseScrollMultiInRaid { get; set; } // Advanced 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; } + public static ConfigEntry ReloadKeyBind { get; set; } public static ConfigEntry UnloadKeyBind { get; set; } public static ConfigEntry UnpackKeyBind { get; set; } public static ConfigEntry FilterByKeyBind { get; set; } public static ConfigEntry LinkedSearchKeyBind { get; set; } + public static ConfigEntry RequiredSearchKeyBind { get; set; } + public static ConfigEntry AddOfferKeyBind { 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 SearchKeyBind { get; set; } + public static ConfigEntry LimitNonstandardDrags { get; set; } // Advanced public static ConfigEntry ItemContextBlocksTextInputs { get; set; } // Advanced // Inventory @@ -87,8 +122,12 @@ internal class Settings public static ConfigEntry SwapItems { get; set; } public static ConfigEntry SwapMags { get; set; } public static ConfigEntry AlwaysSwapMags { get; set; } + public static ConfigEntry UnloadAmmoBoxInPlace { get; set; } // Advanced public static ConfigEntry SwapImpossibleContainers { get; set; } + public static ConfigEntry ModifyEquippedWeapons { get; set; } + public static ConfigEntry ModifyRaidWeapons { get; set; } public static ConfigEntry ReorderGrids { get; set; } + public static ConfigEntry PrioritizeSmallerGrids { get; set; } public static ConfigEntry SynchronizeStashScrolling { get; set; } public static ConfigEntry GreedyStackMove { get; set; } public static ConfigEntry StackBeforeSort { get; set; } @@ -98,9 +137,13 @@ internal class Settings public static ConfigEntry AutoOpenSortingTable { get; set; } public static ConfigEntry DefaultSortingTableBind { get; set; } // Advanced public static ConfigEntry ContextMenuOnRight { get; set; } + public static ConfigEntry AddOfferContextMenu { get; set; } + public static ConfigEntry WishlistContextEverywhere { get; set; } + public static ConfigEntry OpenAllContextMenu { get; set; } public static ConfigEntry ShowGPCurrency { get; set; } public static ConfigEntry ShowOutOfStockCheckbox { get; set; } public static ConfigEntry SortingTableButton { get; set; } + public static ConfigEntry TagsOverCaptions { get; set; } public static ConfigEntry LoadMagPresetOnBullets { get; set; } // Advanced // Inspect Panels @@ -125,6 +168,8 @@ internal class Settings public static ConfigEntry ShowRequiredQuest { get; set; } public static ConfigEntry AutoExpandCategories { get; set; } public static ConfigEntry ClearFiltersOnSearch { get; set; } + public static ConfigEntry AutoOfferPrice { get; set; } + public static ConfigEntry UpdatePriceOnBulk { get; set; } public static ConfigEntry KeepAddOfferOpen { get; set; } public static ConfigEntry PurchaseAllKeybind { get; set; } public static ConfigEntry KeepAddOfferOpenIgnoreMaxOffers { get; set; } // Advanced @@ -207,13 +252,67 @@ internal class Settings null, new ConfigurationManagerAttributes { IsAdvanced = true }))); + configEntries.Add(OperationQueueTime = config.Bind( + GeneralSection, + "Server Operation Queue Time", + 15, + new ConfigDescription( + "The client waits this long to batch inventory operations before sending them to the server. Vanilla Tarkov is 60 (!)", + new AcceptableValueRange(0, 60), + new ConfigurationManagerAttributes { IsAdvanced = true }))); + // Input configEntries.Add(ToggleOrHoldAim = config.Bind( InputSection, "Use Toggle/Hold Aiming", false, new ConfigDescription( - "Tap the aim key to toggle aiming, or hold the aim key for continuous aiming", + "Tap the aim key to toggle aiming, or hold the key for continuous aiming", + null, + new ConfigurationManagerAttributes { }))); + + configEntries.Add(ToggleOrHoldSprint = config.Bind( + InputSection, + "Use Toggle/Hold Sprint", + false, + new ConfigDescription( + "Tap the sprint key to toggle sprinting, or hold the key for continuous sprinting", + null, + new ConfigurationManagerAttributes { }))); + + configEntries.Add(ToggleOrHoldTactical = config.Bind( + InputSection, + "Use Toggle/Hold Tactical Device", + false, + new ConfigDescription( + "Tap the tactical device key to toggle your tactical device, or hold the key for continuous", + null, + new ConfigurationManagerAttributes { }))); + + configEntries.Add(ToggleOrHoldHeadlight = config.Bind( + InputSection, + "Use Toggle/Hold Headlight", + false, + new ConfigDescription( + "Tap the headlight key to toggle your headlight, or hold the key for continuous", + null, + new ConfigurationManagerAttributes { }))); + + configEntries.Add(ToggleOrHoldGoggles = config.Bind( + InputSection, + "Use Toggle/Hold Goggles", + false, + new ConfigDescription( + "Tap the goggles key to toggle night vision/goggles/faceshield, or hold the key for continuous", + null, + new ConfigurationManagerAttributes { }))); + + configEntries.Add(TacticalModeModifier = config.Bind( + InputSection, + "Change Quickbound Tactical Mode", + TacticalBindModifier.Shift, + new ConfigDescription( + "Holding this modifer when activating a quickbound tactical device will switch its active mode", null, new ConfigurationManagerAttributes { }))); @@ -316,6 +415,15 @@ internal class Settings null, new ConfigurationManagerAttributes { }))); + configEntries.Add(ReloadKeyBind = config.Bind( + InputSection, + "Reload Weapon Shortcut", + new KeyboardShortcut(KeyCode.R), + new ConfigDescription( + "Keybind to reload a weapon. Note that this is solely in the menus, and doesn't affect the normal reload key.", + null, + new ConfigurationManagerAttributes { }))); + configEntries.Add(UnloadKeyBind = config.Bind( InputSection, "Unload Mag/Ammo Shortcut", @@ -352,6 +460,24 @@ internal class Settings null, new ConfigurationManagerAttributes { }))); + configEntries.Add(RequiredSearchKeyBind = config.Bind( + InputSection, + "Required Search Shortcut", + new KeyboardShortcut(KeyCode.None), + new ConfigDescription( + "Keybind to search flea market for items to barter for this item", + null, + new ConfigurationManagerAttributes { }))); + + configEntries.Add(AddOfferKeyBind = config.Bind( + InputSection, + "Add Offer Shortcut", + new KeyboardShortcut(KeyCode.None), + new ConfigDescription( + "Keybind to list item on the flea market", + null, + new ConfigurationManagerAttributes { }))); + configEntries.Add(SortingTableKeyBind = config.Bind( InputSection, "Transfer to/from Sorting Table", @@ -361,6 +487,24 @@ internal class Settings null, new ConfigurationManagerAttributes { }))); + configEntries.Add(SearchKeyBind = config.Bind( + InputSection, + "Highlight Search Box", + new KeyboardShortcut(KeyCode.F, KeyCode.LeftControl), + new ConfigDescription( + "Keybind to highlight the search box in hideout crafting, handbook, and flea market", + null, + new ConfigurationManagerAttributes { }))); + + configEntries.Add(LimitNonstandardDrags = config.Bind( + InputSection, + "Limit Nonstandard Drags", + true, + new ConfigDescription( + "Constrain dragging to the left mouse, when shift is not down", + null, + new ConfigurationManagerAttributes { IsAdvanced = true }))); + configEntries.Add(ItemContextBlocksTextInputs = config.Bind( InputSection, "Block Text Inputs on Item Mouseover", @@ -452,6 +596,15 @@ internal class Settings null, new ConfigurationManagerAttributes { }))); + configEntries.Add(UnloadAmmoBoxInPlace = config.Bind( + InventorySection, + "Unload Ammo Boxes In-Place", + !Chainloader.PluginInfos.ContainsKey("com.fika.core"), // default false if fika present, has issues with ground loot + new ConfigDescription( + "Whether to unload ammo boxes in-place, otherwise there needs to be free space somewhere", + null, + new ConfigurationManagerAttributes { IsAdvanced = true }))); + configEntries.Add(SwapImpossibleContainers = config.Bind( InventorySection, "Swap with Incompatible Containers", @@ -461,6 +614,24 @@ internal class Settings null, new ConfigurationManagerAttributes { }))); + configEntries.Add(ModifyEquippedWeapons = config.Bind( + InventorySection, + "Modify Equipped Weapons", + true, + new ConfigDescription( + "Enable the modification of equipped weapons, including vital parts, out of raid", + null, + new ConfigurationManagerAttributes { }))); + + configEntries.Add(ModifyRaidWeapons = config.Bind( + InventorySection, + "Modify Weapons In Raid", + ModRaidWeapon.Never, + new ConfigDescription( + "When to enable the modification of vital parts of unequipped weapons, in raid", + null, + new ConfigurationManagerAttributes { }))); + configEntries.Add(ReorderGrids = config.Bind( InventorySection, "Standardize Grid Order", @@ -470,6 +641,15 @@ internal class Settings null, new ConfigurationManagerAttributes { }))); + configEntries.Add(PrioritizeSmallerGrids = config.Bind( + InventorySection, + "Prioritize Smaller Slots (requires restart)", + false, + new ConfigDescription( + "When adding items to containers with multiple slots, place the item in the smallest slot that can hold it, rather than just the first empty space. Requires Standardize Grid Order.", + null, + new ConfigurationManagerAttributes { }))); + configEntries.Add(SynchronizeStashScrolling = config.Bind( InventorySection, "Synchronize Stash Scroll Position", @@ -551,6 +731,33 @@ internal class Settings null, new ConfigurationManagerAttributes { }))); + configEntries.Add(AddOfferContextMenu = config.Bind( + InventorySection, + "Add Offer Context Menu", + true, + new ConfigDescription( + "Add a context menu to list the item on the flea market", + null, + new ConfigurationManagerAttributes { }))); + + configEntries.Add(WishlistContextEverywhere = config.Bind( + InventorySection, + "Wishlist Context Menu Everywhere", + true, + new ConfigDescription( + "Add/Remove to wishlist available in the context menu on all screens", + null, + new ConfigurationManagerAttributes { }))); + + configEntries.Add(OpenAllContextMenu = config.Bind( + InventorySection, + "Open All Context Flyout", + true, + new ConfigDescription( + "Add a flyout to the Open context menu to recursively open a stack of containers", + null, + new ConfigurationManagerAttributes { }))); + configEntries.Add(ShowGPCurrency = config.Bind( InventorySection, "Show GP Coins in Currency", @@ -578,6 +785,15 @@ internal class Settings null, new ConfigurationManagerAttributes { }))); + configEntries.Add(TagsOverCaptions = config.Bind( + InventorySection, + "Prioritize Tags Over Names", + true, + new ConfigDescription( + "When there isn't enough space to show both the tag and the name of an item, show the tag", + null, + new ConfigurationManagerAttributes { }))); + configEntries.Add(LoadMagPresetOnBullets = config.Bind( InventorySection, "Mag Presets Context Menu on Bullets", @@ -734,6 +950,24 @@ internal class Settings null, new ConfigurationManagerAttributes { }))); + configEntries.Add(AutoOfferPrice = config.Bind( + FleaMarketSection, + "Autopopulate Offer Price", + AutoFleaPrice.None, + new ConfigDescription( + "Autopopulte new offers with min/avg/max market price, or leave blank", + null, + new ConfigurationManagerAttributes { }))); + + configEntries.Add(UpdatePriceOnBulk = config.Bind( + FleaMarketSection, + "Update Offer Price on Bulk", + true, + new ConfigDescription( + "Automatically multiply or divide the price when you check/uncheck bulk, or or when you change the number of selected items while bulk is checked.", + null, + new ConfigurationManagerAttributes { }))); + configEntries.Add(ShowRequiredQuest = config.Bind( FleaMarketSection, "Show Required Quest for Locked Offers", @@ -781,12 +1015,13 @@ internal class Settings RecalcOrder(configEntries); - MakeDependent(EnableMultiSelect, EnableMultiSelectInRaid); MakeDependent(EnableMultiSelect, ShowMultiSelectDebug, false); MakeDependent(EnableMultiSelect, EnableMultiClick); MakeExclusive(EnableMultiClick, AutoOpenSortingTable, false); + + MakeDependent(ReorderGrids, PrioritizeSmallerGrids, false); } private static void RecalcOrder(List configEntries) diff --git a/Sorter.cs b/src/Sorter.cs similarity index 100% rename from Sorter.cs rename to src/Sorter.cs diff --git a/TaskSerializer.cs b/src/TaskSerializer.cs similarity index 100% rename from TaskSerializer.cs rename to src/TaskSerializer.cs diff --git a/ToggleHold.cs b/src/ToggleHold.cs similarity index 100% rename from ToggleHold.cs rename to src/ToggleHold.cs