Merge branch 'main' into CykaFix

This commit is contained in:
2024-07-24 15:40:39 +02:00
92 changed files with 3238 additions and 604 deletions

View File

@@ -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<HighlightButton>();
lowestButton.onClick.AddListener(() => SetRequirement(__instance, rublesRequirement, ____pricesPanel.Minimum));
____pricesPanel.AddDisposable(lowestButton.onClick.RemoveAllListeners);
Button averageButton = panel.AverageLabel.GetOrAddComponent<HighlightButton>();
averageButton.onClick.AddListener(() => SetRequirement(__instance, rublesRequirement, ____pricesPanel.Average));
____pricesPanel.AddDisposable(averageButton.onClick.RemoveAllListeners);
Button maximumButton = panel.MaximumLabel.GetOrAddComponent<HighlightButton>();
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<TextMeshProUGUI>();
}
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;
}
}
}

View File

@@ -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<SharedGameSettingsClass>.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<KeyCombination.KeyCombinationState> 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);
}
}
}

View File

@@ -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<EItemInfoButton> __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<EItemInfoButton> __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<IResult> __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<LootItemClass> 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<LootItemClass> targets;
public UnloadAmmoBoxState()
{
fakeStash = (StashClass)Singleton<ItemFactory>.Instance.CreateItem("FakeStash", "566abbc34bdc2d92178b4576", null);
var profile = PatchConstants.BackEndSession.Profile;
fakeController = new(fakeStash, profile.ProfileId, profile.Nickname);
}
}
}

View File

@@ -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<Item> 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<Item> action, ItemUiContext context = null);
public static Task Apply(Func<Item, Task> func, ItemUiContext context = null);
}
```

View File

@@ -4,7 +4,7 @@
<TargetFramework>net471</TargetFramework>
<AssemblyName>Tyfon.UIFixes</AssemblyName>
<Description>SPT UI Fixes</Description>
<Version>2.3.1</Version>
<Version>2.5.1</Version>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>latest</LangVersion>
<Configurations>Debug;Release</Configurations>
@@ -79,7 +79,8 @@
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework.TrimEnd(`0123456789`))' == 'net'">
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.2" PrivateAssets="all" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.2"
PrivateAssets="all" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,6 +1,6 @@
{
"name": "uifixes",
"version": "2.3.1",
"version": "2.5.1",
"main": "src/mod.js",
"license": "MIT",
"author": "Tyfon",

View File

@@ -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
try {
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"];
}
} catch (error) {
this.logger.error(`UIFixes: Failed to restore quickbinds\n ${error}`);
}
return offer;
return result;
};
},
{ frequency: "Always" }
@@ -93,15 +67,30 @@ 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
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);
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,11 +110,13 @@ 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"]) {
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 [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,
@@ -168,6 +159,14 @@ class UIFixes implements IPreSptLoadMod {
}
}
}
} 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;
}

View File

@@ -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);
}
}

View File

@@ -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<OpenInteractions.Options>(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<NestedContainerTaskSerializer>();
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<ItemContextAbstractClass> GetNestedContainers(ItemContextAbstractClass first)
{
var windowRoot = Singleton<PreloaderUI>.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<GridItemView>().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<ItemContextAbstractClass> { }

22
src/Extensions.cs Normal file
View File

@@ -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();
}
}

View File

@@ -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<ItemMarketPricesPanel, Properties> 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<GClass2803, Properties> 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;
}

View File

@@ -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;

View File

@@ -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;
}
@@ -74,10 +74,14 @@ public class DrawMultiSelect : MonoBehaviour
secondary = shiftDown;
if (!secondary)
{
// 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();
}
}
}
if (drawing && !Settings.SelectionBoxKey.Value.IsPressedIgnoreOthers())
{
@@ -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()
{
return false;
if (MouseIsOverItem())
{
return true;
}
PointerEventData eventData = new(EventSystem.current)
@@ -179,26 +188,42 @@ public class DrawMultiSelect : MonoBehaviour
};
List<RaycastResult> 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<MonoBehaviour>()
return false;
}
var draggables = gameObject.GetComponentsInParent<MonoBehaviour>()
.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<MonoBehaviour>()
.Where(c => c is IPointerClickHandler || c is IPointerDownHandler || c is IPointerUpHandler);
var clickables = gameObject.GetComponentsInParent<MonoBehaviour>()
.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 false;
}
return true;
}
return true;
return false;
}
private bool IsOnTop(Rect itemRect, Transform itemTransform, GraphicRaycaster raycaster)

View File

@@ -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<MultiSelectItemContextTaskSerializer>();
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;

View File

@@ -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<Item> GetItems()
{
return MultiSelect.SortedItemContexts().Select(ic => ic.Item);
}
public static Task Apply(Func<Item, Task> func, ItemUiContext itemUiContext = null)
{
itemUiContext ??= ItemUiContext.Instance;
var taskSerializer = itemUiContext.gameObject.AddComponent<ItemTaskSerializer>();
return taskSerializer.Initialize(GetItems(), func);
}
}
public class ItemTaskSerializer : TaskSerializer<Item> { }

View File

@@ -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<Item, Task>. 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
{
/// <summary>
/// Provides access to UI Fixes' multiselect functionality.
/// </summary>
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;
/// <value><c>Count</c> represents the number of items in the current selection, 0 if UI Fixes is not present.</value>
public static int Count
{
get
{
if (!Loaded())
{
return 0;
}
return (int)GetCountMethod.Invoke(null, new object[] { });
}
}
/// <value><c>Items</c> is an enumerable list of items in the current selection, empty if UI Fixes is not present.</value>
public static IEnumerable<Item> Items
{
get
{
if (!Loaded())
{
return new Item[] { };
}
return (IEnumerable<Item>)GetItemsMethod.Invoke(null, new object[] { });
}
}
/// <summary>
/// This method takes an <c>Action</c> and calls it *sequentially* on each item in the current selection.
/// Will no-op if UI Fixes is not present.
/// </summary>
/// <param name="action">The action to call on each item.</param>
/// <param name="itemUiContext">Optional <c>ItemUiContext</c>; will use <c>ItemUiContext.Instance</c> if not provided.</param>
public static void Apply(Action<Item> action, ItemUiContext itemUiContext = null)
{
if (!Loaded())
{
return;
}
Func<Item, Task> func = item =>
{
action(item);
return Task.CompletedTask;
};
ApplyMethod.Invoke(null, new object[] { func, itemUiContext });
}
/// <summary>
/// This method takes an <c>Func</c> that returns a <c>Task</c> and calls it *sequentially* on each item in the current selection.
/// Will return a completed task immediately if UI Fixes is not present.
/// </summary>
/// <param name="func">The function to call on each item</param>
/// <param name="itemUiContext">Optional <c>ItemUiContext</c>; will use <c>ItemUiContext.Instance</c> if not provided.</param>
/// <returns>A <c>Task</c> that will complete when all the function calls are complete.</returns>
public static Task Apply(Func<Item, Task> 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;
}
}
}

View File

@@ -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<HighlightButton>();
lowestButton.onClick.AddListener(() => SetRequirement(__instance, rublesRequirement, ____pricesPanel.Minimum));
____pricesPanel.AddDisposable(lowestButton.onClick.RemoveAllListeners);
Button averageButton = panel.AverageLabel.GetOrAddComponent<HighlightButton>();
averageButton.onClick.AddListener(() => SetRequirement(__instance, rublesRequirement, ____pricesPanel.Average));
____pricesPanel.AddDisposable(averageButton.onClick.RemoveAllListeners);
Button maximumButton = panel.MaximumLabel.GetOrAddComponent<HighlightButton>();
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<TextMeshProUGUI>();
}
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;
}
}
}

View File

@@ -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<EItemInfoButton> __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<EItemInfoButton> __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<EItemInfoButton, string> names, ref IReadOnlyDictionary<EItemInfoButton, Sprite> icons)
{
names ??= new Dictionary<EItemInfoButton, string>()
{
{ EItemInfoButtonExt.AddOffer, "ragfair/OFFER ADD" }
};
FleaSprite ??= Resources.FindObjectsOfTypeAll<Sprite>().Single(s => s.name == "icon_flea_market");
icons ??= new Dictionary<EItemInfoButton, Sprite>()
{
{ 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<EItemInfoButton> __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;
}
}
}

View File

@@ -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<KeyCombination.KeyCombinationState> 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<KeyCombination.KeyCombinationState> 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<SharedGameSettingsClass>.Instance.Control.Controller.method_3();
}
}

View File

@@ -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,67 +68,53 @@ 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();
}
else if (caption == EItemInfoButton.Equip.ToString())
{
count = MultiSelect.InteractionCount(EItemInfoButton.Equip, ItemUiContext.Instance);
}
else if (caption == EItemInfoButton.Unequip.ToString())
{
count = MultiSelect.InteractionCount(EItemInfoButton.Unequip, ItemUiContext.Instance);
}
else if (caption == EItemInfoButton.LoadAmmo.ToString())
{
count = MultiSelect.InteractionCount(EItemInfoButton.LoadAmmo, ItemUiContext.Instance);
}
else if (caption == EItemInfoButton.UnloadAmmo.ToString())
{
count = MultiSelect.InteractionCount(EItemInfoButton.UnloadAmmo, ItemUiContext.Instance);
}
else if (caption == EItemInfoButton.ApplyMagPreset.ToString())
{
count = MultiSelect.InteractionCount(EItemInfoButton.ApplyMagPreset, ItemUiContext.Instance);
}
else if (caption == EItemInfoButton.Unpack.ToString())
{
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 + ")";
}
}
else if (caption == EItemInfoButton.Equip.ToString())
{
int count = MultiSelect.InteractionCount(EItemInfoButton.Equip, ItemUiContext.Instance);
if (count > 0)
{
____text.text += " (x" + count + ")";
}
}
else if (caption == EItemInfoButton.Unequip.ToString())
{
int count = MultiSelect.InteractionCount(EItemInfoButton.Unequip, ItemUiContext.Instance);
if (count > 0)
{
____text.text += " (x" + count + ")";
}
}
else if (caption == EItemInfoButton.LoadAmmo.ToString())
{
int count = MultiSelect.InteractionCount(EItemInfoButton.LoadAmmo, ItemUiContext.Instance);
if (count > 0)
{
____text.text += " (x" + count + ")";
}
}
else if (caption == EItemInfoButton.UnloadAmmo.ToString())
{
int count = MultiSelect.InteractionCount(EItemInfoButton.UnloadAmmo, ItemUiContext.Instance);
if (count > 0)
{
____text.text += " (x" + count + ")";
}
}
else if (caption == EItemInfoButton.ApplyMagPreset.ToString())
{
int count = MultiSelect.InteractionCount(EItemInfoButton.ApplyMagPreset, ItemUiContext.Instance);
if (count > 0)
{
____text.text += " (x" + count + ")";
}
}
else if (caption == EItemInfoButton.Unpack.ToString())
{
int count = MultiSelect.InteractionCount(EItemInfoButton.Unpack, ItemUiContext.Instance);
if (count > 0)
{
____text.text += " (x" + count + ")";
}
}
}
}
public class DeclareSubInteractionsInventoryPatch : ModulePatch
@@ -130,9 +125,20 @@ public static class ContextMenuPatches
}
[PatchPostfix]
public static void Postfix(ref IEnumerable<EItemInfoButton> __result)
public static void Postfix(ref IEnumerable<EItemInfoButton> __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<EItemInfoButton> __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<EItemInfoButton> __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();

View File

@@ -46,9 +46,7 @@ public static class ContextMenuShortcutPatches
return;
}
if (!Settings.ItemContextBlocksTextInputs.Value &&
EventSystem.current?.currentSelectedGameObject != null &&
EventSystem.current.currentSelectedGameObject.GetComponent<TMP_InputField>() != 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;
}

View File

@@ -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<LayoutElement>();
stashLayout.preferredWidth = 644f;
var noItemLayout = ____noOfferPanel.GetComponent<LayoutElement>();
var requirementLayout = ____selectedItemPanel.GetComponent<LayoutElement>();
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<SearchKeyListener>();
}
}
// 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<SearchKeyListener>();
}
}
@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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;
}
if (PreviousFilterButton.Instance != null)
{
PreviousFilterButton.Instance.OnOffersLoaded(__instance);
}
if (Settings.AutoExpandCategories.Value)
{

View File

@@ -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)
{

View File

@@ -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;
}
}

View File

@@ -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<SearchKeyListener>();
____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<TMP_InputField>() != null)
if (command == ECommand.Enter && Plugin.TextboxActive())
{
__result = InputNode.ETranslateResult.Block;
return false;

View File

@@ -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);
}
}
}

View File

@@ -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<GridView>().FirstOrDefault(gv => gv.Grid == gridAddress.Grid);

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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

View File

@@ -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<StashGridClass, GridView>(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<KeyValuePair<StashGridClass, GridView>> SortGrids(
TemplatedGridsView __instance,
IEnumerable<KeyValuePair<StashGridClass, GridView>> 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<StashGridClass, GridView> 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);
}
}
}

View File

@@ -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)

View File

@@ -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<SliderMouseListener>().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<SliderMouseListener>().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);
}
}
}
}

View File

@@ -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

View File

@@ -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<IHandsController> callback)
{
Item boundItem = __instance.InventoryControllerClass.Inventory.FastAccess.GetBoundItem(quickSlot);
if (boundItem == null)
{
return true;
}
LightComponent lightComponent = boundItem.GetItemComponent<LightComponent>();
if (lightComponent != null)
{
ToggleLight(__instance, boundItem, lightComponent);
callback(null);
return false;
}
NightVisionComponent nightVisionComponent = boundItem.GetItemComponent<NightVisionComponent>();
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<SharedGameSettingsClass>.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<SharedGameSettingsClass>.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<SharedGameSettingsClass>.Instance.Control.Controller.method_3();
}
}
private static bool IsEquippedTacticalDevice(InventoryControllerClass inventoryController, Item item)
{
LightComponent lightComponent = item.GetItemComponent<LightComponent>();
NightVisionComponent nightVisionComponent = item.GetItemComponent<NightVisionComponent>();
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<LightComponent>();
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<NightVisionComponent>();
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<EBoundItem, ItemType> 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);
}
}

75
src/Patches/TagPatches.cs Normal file
View File

@@ -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);
}
}
}
}

View File

@@ -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,29 +59,36 @@ 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();
if (!___bool_8 && ctrlPressed && assortmentController.QuickFindTradingAppropriatePlace(__instance.Item, null))
{
__instance.ItemContext?.CloseDependentWindows();
__instance.HideTooltip();
Singleton<GUISounds>.Instance.PlayItemSound(__instance.Item.ItemSound, EInventorySoundType.pickup, false);
@@ -91,12 +99,17 @@ public static class TradingAutoSwitchPatches
if (___bool_8)
{
tradingItemView.TraderAssortmentController.SelectItem(__instance.Item);
assortmentController.SelectItem(__instance.Item);
BuyTab.OnPointerClick(null);
return false;
}
}
catch (Exception e)
{
Logger.LogError(e);
}
return true;
}

View File

@@ -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<EItemInfoButton> __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<EItemInfoButton> __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<IResult> __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<LootItemClass> 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. <insert enraged, profanity-laced, unhinged anti-BSG rant here>
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. <insert enraged, profanity-laced, unhinged anti-BSG rant here>
await Task.Yield();
}
if (unloadedAny && Singleton<GUISounds>.Instantiated)
{
Singleton<GUISounds>.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<GameWorld>.Instance.R().TraderController;
}
else
{
var profile = PatchConstants.BackEndSession.Profile;
StashClass fakeStash = Singleton<ItemFactory>.Instance.CreateFakeStash();
return new TraderControllerClass(fakeStash, profile.ProfileId, profile.Nickname);
}
}
}
}

View File

@@ -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<ResizeOperation> __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<BonusController>.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<bool> __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<GClass3372> __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<Slot> __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<GameWorld>.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;
}
}

View File

@@ -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<AbstractGame>.Instance?.InRaid;
return inRaid.HasValue && inRaid.Value;
}
public static bool TextboxActive()
{
return EventSystem.current?.currentSelectedGameObject != null &&
EventSystem.current.currentSelectedGameObject.GetComponent<TMP_InputField>() != 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;
}
}

View File

@@ -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);
}

20
src/SearchKeyListener.cs Normal file
View File

@@ -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<TMP_InputField>();
if (searchField != null)
{
searchField.ActivateInputField();
searchField.Select();
}
}
}
}

View File

@@ -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<bool> AutoSwitchTrading { get; set; }
public static ConfigEntry<bool> ClickOutOfDialogs { get; set; } // Advanced
public static ConfigEntry<bool> RestoreAsyncScrollPositions { get; set; } // Advanced
public static ConfigEntry<int> OperationQueueTime { get; set; } // Advanced
// Input
public static ConfigEntry<bool> ToggleOrHoldAim { get; set; }
public static ConfigEntry<bool> ToggleOrHoldSprint { get; set; }
public static ConfigEntry<bool> ToggleOrHoldTactical { get; set; }
public static ConfigEntry<bool> ToggleOrHoldHeadlight { get; set; }
public static ConfigEntry<bool> ToggleOrHoldGoggles { get; set; }
public static ConfigEntry<TacticalBindModifier> TacticalModeModifier { get; set; }
public static ConfigEntry<bool> UseHomeEnd { get; set; }
public static ConfigEntry<bool> RebindPageUpDown { get; set; }
public static ConfigEntry<int> MouseScrollMulti { get; set; }
public static ConfigEntry<bool> UseRaidMouseScrollMulti { get; set; } // Advanced
public static ConfigEntry<int> MouseScrollMultiInRaid { get; set; } // Advanced
public static ConfigEntry<KeyboardShortcut> InspectKeyBind { get; set; }
public static ConfigEntry<KeyboardShortcut> OpenKeyBind { get; set; }
public static ConfigEntry<KeyboardShortcut> ExamineKeyBind { get; set; }
public static ConfigEntry<KeyboardShortcut> TopUpKeyBind { get; set; }
public static ConfigEntry<KeyboardShortcut> UseKeyBind { get; set; }
public static ConfigEntry<KeyboardShortcut> UseAllKeyBind { get; set; }
public static ConfigEntry<KeyboardShortcut> ReloadKeyBind { get; set; }
public static ConfigEntry<KeyboardShortcut> UnloadKeyBind { get; set; }
public static ConfigEntry<KeyboardShortcut> UnpackKeyBind { get; set; }
public static ConfigEntry<KeyboardShortcut> FilterByKeyBind { get; set; }
public static ConfigEntry<KeyboardShortcut> LinkedSearchKeyBind { get; set; }
public static ConfigEntry<KeyboardShortcut> RequiredSearchKeyBind { get; set; }
public static ConfigEntry<KeyboardShortcut> AddOfferKeyBind { get; set; }
public static ConfigEntry<KeyboardShortcut> SortingTableKeyBind { get; set; }
public static ConfigEntry<bool> UseRaidMouseScrollMulti { get; set; } // Advanced
public static ConfigEntry<int> MouseScrollMultiInRaid { get; set; } // Advanced
public static ConfigEntry<KeyboardShortcut> SearchKeyBind { get; set; }
public static ConfigEntry<bool> LimitNonstandardDrags { get; set; } // Advanced
public static ConfigEntry<bool> ItemContextBlocksTextInputs { get; set; } // Advanced
// Inventory
@@ -87,8 +122,12 @@ internal class Settings
public static ConfigEntry<bool> SwapItems { get; set; }
public static ConfigEntry<bool> SwapMags { get; set; }
public static ConfigEntry<bool> AlwaysSwapMags { get; set; }
public static ConfigEntry<bool> UnloadAmmoBoxInPlace { get; set; } // Advanced
public static ConfigEntry<bool> SwapImpossibleContainers { get; set; }
public static ConfigEntry<bool> ModifyEquippedWeapons { get; set; }
public static ConfigEntry<ModRaidWeapon> ModifyRaidWeapons { get; set; }
public static ConfigEntry<bool> ReorderGrids { get; set; }
public static ConfigEntry<bool> PrioritizeSmallerGrids { get; set; }
public static ConfigEntry<bool> SynchronizeStashScrolling { get; set; }
public static ConfigEntry<bool> GreedyStackMove { get; set; }
public static ConfigEntry<bool> StackBeforeSort { get; set; }
@@ -98,9 +137,13 @@ internal class Settings
public static ConfigEntry<bool> AutoOpenSortingTable { get; set; }
public static ConfigEntry<bool> DefaultSortingTableBind { get; set; } // Advanced
public static ConfigEntry<bool> ContextMenuOnRight { get; set; }
public static ConfigEntry<bool> AddOfferContextMenu { get; set; }
public static ConfigEntry<bool> WishlistContextEverywhere { get; set; }
public static ConfigEntry<bool> OpenAllContextMenu { get; set; }
public static ConfigEntry<bool> ShowGPCurrency { get; set; }
public static ConfigEntry<bool> ShowOutOfStockCheckbox { get; set; }
public static ConfigEntry<SortingTableDisplay> SortingTableButton { get; set; }
public static ConfigEntry<bool> TagsOverCaptions { get; set; }
public static ConfigEntry<bool> LoadMagPresetOnBullets { get; set; } // Advanced
// Inspect Panels
@@ -125,6 +168,8 @@ internal class Settings
public static ConfigEntry<bool> ShowRequiredQuest { get; set; }
public static ConfigEntry<bool> AutoExpandCategories { get; set; }
public static ConfigEntry<bool> ClearFiltersOnSearch { get; set; }
public static ConfigEntry<AutoFleaPrice> AutoOfferPrice { get; set; }
public static ConfigEntry<bool> UpdatePriceOnBulk { get; set; }
public static ConfigEntry<bool> KeepAddOfferOpen { get; set; }
public static ConfigEntry<KeyboardShortcut> PurchaseAllKeybind { get; set; }
public static ConfigEntry<bool> 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<int>(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<ConfigEntryBase> configEntries)