diff --git a/ContextMenus/EmptySlotMenu.cs b/ContextMenus/EmptySlotMenu.cs new file mode 100644 index 0000000..d997620 --- /dev/null +++ b/ContextMenus/EmptySlotMenu.cs @@ -0,0 +1,29 @@ +using EFT.InventoryLogic; +using EFT.UI; +using EFT.UI.Ragfair; +using System; +using System.Collections.Generic; + +namespace UIFixes.ContextMenus +{ + public class EmptySlotMenu(Slot slot, ItemContextAbstractClass itemContext, ItemUiContext itemUiContext, Action closeAction) : GClass3042(itemContext, itemUiContext, closeAction) + { + private static readonly List Actions = [EItemInfoButton.LinkedSearch]; + + private readonly Slot slot = slot; + + public override IEnumerable AvailableInteractions => Actions; + + public override void ExecuteInteractionInternal(EItemInfoButton interaction) + { + switch (interaction) + { + case EItemInfoButton.LinkedSearch: + Search(new GClass3219(EFilterType.LinkedSearch, slot.ParentItem.TemplateId + ":" + slot.Id, true)); + break; + default: + break; + } + } + } +} diff --git a/ContextMenus/EmptySlotMenuTrigger.cs b/ContextMenus/EmptySlotMenuTrigger.cs new file mode 100644 index 0000000..038272c --- /dev/null +++ b/ContextMenus/EmptySlotMenuTrigger.cs @@ -0,0 +1,82 @@ +using EFT.InventoryLogic; +using EFT.UI; +using System; +using UnityEngine; +using UnityEngine.EventSystems; + +namespace UIFixes.ContextMenus +{ + public class EmptySlotMenuTrigger : MonoBehaviour, IPointerClickHandler, IPointerDownHandler, IPointerUpHandler, IPointerEnterHandler, IPointerExitHandler + { + private ItemUiContext itemUiContext; + private Slot slot; + private ItemContextAbstractClass parentContext; + private bool hovered = false; + + public void Init(Slot slot, ItemContextAbstractClass parentContext, ItemUiContext itemUiContext) + { + this.itemUiContext = itemUiContext; + this.slot = slot; + this.parentContext = parentContext; + } + + public void Update() + { + if (!hovered) + { + return; + } + + if (Settings.LinkedSearchKeyBind.Value.IsDown()) + { + using EmptySlotContext context = new(slot, parentContext, itemUiContext); + var interactions = itemUiContext.GetItemContextInteractions(context, null); + interactions.ExecuteInteraction(EItemInfoButton.LinkedSearch); + } + } + + public void OnPointerClick(PointerEventData eventData) + { + if (eventData.button == PointerEventData.InputButton.Right) + { + EmptySlotContext context = new(slot, parentContext, itemUiContext); + itemUiContext.ShowContextMenu(context, eventData.position); + } + } + + public void OnPointerDown(PointerEventData eventData) { } + + public void OnPointerEnter(PointerEventData eventData) + { + hovered = true; + } + + public void OnPointerExit(PointerEventData eventData) + { + hovered = false; + } + + public void OnPointerUp(PointerEventData eventData) { } + } + + public class EmptySlotContext(Slot slot, ItemContextAbstractClass parentContext, ItemUiContext itemUiContext) : ItemContextAbstractClass(parentContext.Item, parentContext.ViewType, parentContext) + { + private readonly Slot slot = slot; + private readonly ItemUiContext itemUiContext = itemUiContext; + + public override ItemInfoInteractionsAbstractClass GetItemContextInteractions(Action closeAction) + { + return new EmptySlotMenu(slot, ItemContextAbstractClass, itemUiContext, () => + { + Dispose(); + closeAction?.Invoke(); + }); + } + + public override ItemContextAbstractClass CreateChild(Item item) + { + // Should never happen + throw new NotImplementedException(); + } + } +} diff --git a/Patches/ContextMenuPatches.cs b/Patches/ContextMenuPatches.cs index 86fa881..4284714 100644 --- a/Patches/ContextMenuPatches.cs +++ b/Patches/ContextMenuPatches.cs @@ -1,6 +1,7 @@ using Comfort.Common; using EFT.InventoryLogic; using EFT.UI; +using EFT.UI.DragAndDrop; using HarmonyLib; using SPT.Reflection.Patching; using SPT.Reflection.Utils; @@ -9,6 +10,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using TMPro; +using UIFixes.ContextMenus; using UnityEngine; namespace UIFixes @@ -81,6 +83,9 @@ namespace UIFixes new EnableInsureInnerItemsPatch().Enable(); new DisableLoadPresetOnBulletsPatch().Enable(); + + new EmptySlotMenuPatch().Enable(); + new EmptySlotMenuRemovePatch().Enable(); } public class ContextMenuNamesPatch : ModulePatch @@ -422,6 +427,44 @@ namespace UIFixes } } + public class EmptySlotMenuPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.DeclaredMethod(typeof(ModSlotView), nameof(ModSlotView.Show)); + } + + [PatchPostfix] + public static void Postfix(ModSlotView __instance, Slot slot, ItemContextAbstractClass parentItemContext, ItemUiContext itemUiContext) + { + if (!Settings.EnableSlotSearch.Value || slot.ContainedItem != null) + { + return; + } + + EmptySlotMenuTrigger menuTrigger = __instance.GetOrAddComponent(); + menuTrigger.Init(slot, parentItemContext, itemUiContext); + } + } + + public class EmptySlotMenuRemovePatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.DeclaredMethod(typeof(ModSlotView), nameof(ModSlotView.SetupItemView)); + } + + [PatchPostfix] + public static void Postfix(ModSlotView __instance) + { + EmptySlotMenuTrigger menuTrigger = __instance.GetComponent(); + if (menuTrigger != null) + { + UnityEngine.Object.Destroy(menuTrigger); + } + } + } + public class PositionSubMenuPatch : ModulePatch { protected override MethodBase GetTargetMethod() diff --git a/Patches/ContextMenuShortcutPatches.cs b/Patches/ContextMenuShortcutPatches.cs index 4b00092..4bc7900 100644 --- a/Patches/ContextMenuShortcutPatches.cs +++ b/Patches/ContextMenuShortcutPatches.cs @@ -5,6 +5,7 @@ using HarmonyLib; using SPT.Reflection.Patching; using System.Reflection; using TMPro; +using UnityEngine; using UnityEngine.EventSystems; namespace UIFixes @@ -29,6 +30,8 @@ namespace UIFixes public class ItemUiContextPatch : ModulePatch { + private static ItemInfoInteractionsAbstractClass Interactions; + protected override MethodBase GetTargetMethod() { return AccessTools.Method(typeof(ItemUiContext), nameof(ItemUiContext.Update)); @@ -51,57 +54,60 @@ namespace UIFixes return; } - - var interactions = __instance.GetItemContextInteractions(itemContext, null); if (Settings.InspectKeyBind.Value.IsDown()) { - interactions.ExecuteInteraction(EItemInfoButton.Inspect); + TryInteraction(__instance, itemContext, EItemInfoButton.Inspect); } if (Settings.OpenKeyBind.Value.IsDown()) { - interactions.ExecuteInteraction(EItemInfoButton.Open); + TryInteraction(__instance, itemContext, EItemInfoButton.Open); } if (Settings.TopUpKeyBind.Value.IsDown()) { - interactions.ExecuteInteraction(EItemInfoButton.TopUp); + TryInteraction(__instance, itemContext, EItemInfoButton.TopUp); } if (Settings.UseKeyBind.Value.IsDown()) { - interactions.ExecuteInteraction(EItemInfoButton.Use); + TryInteraction(__instance, itemContext, EItemInfoButton.Use); } if (Settings.UseAllKeyBind.Value.IsDown()) { - if (!interactions.ExecuteInteraction(EItemInfoButton.UseAll)) - { - interactions.ExecuteInteraction(EItemInfoButton.Use); - } + TryInteraction(__instance, itemContext, EItemInfoButton.UseAll, EItemInfoButton.Use); } if (Settings.UnloadKeyBind.Value.IsDown()) { - if (!interactions.ExecuteInteraction(EItemInfoButton.Unload)) - { - interactions.ExecuteInteraction(EItemInfoButton.UnloadAmmo); - } + TryInteraction(__instance, itemContext, EItemInfoButton.Unload, EItemInfoButton.UnloadAmmo); } if (Settings.UnpackKeyBind.Value.IsDown()) { - interactions.ExecuteInteraction(EItemInfoButton.Unpack); + TryInteraction(__instance, itemContext, EItemInfoButton.Unpack); } if (Settings.FilterByKeyBind.Value.IsDown()) { - interactions.ExecuteInteraction(EItemInfoButton.FilterSearch); + TryInteraction(__instance, itemContext, EItemInfoButton.FilterSearch); } if (Settings.LinkedSearchKeyBind.Value.IsDown()) { - interactions.ExecuteInteraction(EItemInfoButton.LinkedSearch); + TryInteraction(__instance, itemContext, EItemInfoButton.LinkedSearch); + } + + Interactions = null; + } + + private static void TryInteraction(ItemUiContext itemUiContext, ItemContextAbstractClass itemContext, EItemInfoButton interaction, EItemInfoButton? fallbackInteraction = null) + { + Interactions ??= itemUiContext.GetItemContextInteractions(itemContext, null); + if (!Interactions.ExecuteInteraction(interaction) && fallbackInteraction.HasValue) + { + Interactions.ExecuteInteraction(fallbackInteraction.Value); } } } diff --git a/Patches/FleaSlotSearchPatches.cs b/Patches/FleaSlotSearchPatches.cs new file mode 100644 index 0000000..6c38bb9 --- /dev/null +++ b/Patches/FleaSlotSearchPatches.cs @@ -0,0 +1,55 @@ +using EFT.UI.Ragfair; +using HarmonyLib; +using SPT.Reflection.Patching; +using System.Linq; +using System.Reflection; + +namespace UIFixes +{ + public static class FleaSlotSearchPatches + { + public static void Enable() + { + new HandbookWorkaroundPatch().Enable(); + } + + public class HandbookWorkaroundPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(RagFairClass), nameof(RagFairClass.method_24)); + } + + [PatchPrefix] + public static void Prefix(GClass3219[] searches, ref string __state, HandbookClass ___handbookClass) + { + if (!Settings.EnableSlotSearch.Value) + { + return; + } + + GClass3219 search = searches.FirstOrDefault(s => s.Type == EFilterType.LinkedSearch && s.StringValue.Contains(":")); + if (search != null) + { + __state = search.StringValue.Split(':')[0]; + ___handbookClass[__state].Data.Id = search.StringValue; + searches[searches.IndexOf(search)] = new GClass3219(EFilterType.LinkedSearch, __state, search.Add); + } + } + + [PatchPostfix] + public static void Postfix(ref string __state, HandbookClass ___handbookClass) + { + if (!Settings.EnableSlotSearch.Value) + { + return; + } + + if (__state != null) + { + ___handbookClass[__state].Data.Id = __state; + } + } + } + } +} diff --git a/Patches/GPCoinPatches.cs b/Patches/GPCoinPatches.cs index e1c263f..2364a2f 100644 --- a/Patches/GPCoinPatches.cs +++ b/Patches/GPCoinPatches.cs @@ -63,10 +63,7 @@ namespace UIFixes var sums = R.Money.GetMoneySums(inventoryItems); - NumberFormatInfo numberFormatInfo = new NumberFormatInfo - { - NumberGroupSeparator = " " - }; + NumberFormatInfo numberFormatInfo = new() { NumberGroupSeparator = " " }; ____roubles.text = sums[ECurrencyType.RUB].ToString("N0", numberFormatInfo); ____euros.text = sums[ECurrencyType.EUR].ToString("N0", numberFormatInfo); @@ -114,10 +111,7 @@ namespace UIFixes var sums = R.Money.GetMoneySums(inventoryItems); - NumberFormatInfo numberFormatInfo = new NumberFormatInfo - { - NumberGroupSeparator = " " - }; + NumberFormatInfo numberFormatInfo = new() { NumberGroupSeparator = " " }; ____roubles.text = GClass2531.GetCurrencyChar(ECurrencyType.RUB) + " " + sums[ECurrencyType.RUB].ToString("N0", numberFormatInfo); ____euros.text = GClass2531.GetCurrencyChar(ECurrencyType.EUR) + " " + sums[ECurrencyType.EUR].ToString("N0", numberFormatInfo); diff --git a/Plugin.cs b/Plugin.cs index 4a3d7af..aed606a 100644 --- a/Plugin.cs +++ b/Plugin.cs @@ -61,6 +61,7 @@ namespace UIFixes ReorderGridsPatches.Enable(); NoRandomGrenadesPatch.Init(); GPCoinPatches.Enable(); + FleaSlotSearchPatches.Enable(); } public static bool InRaid() diff --git a/Settings.cs b/Settings.cs index 657636e..bfac63a 100644 --- a/Settings.cs +++ b/Settings.cs @@ -104,6 +104,7 @@ namespace UIFixes // Flea Market public static ConfigEntry EnableFleaHistory { get; set; } + public static ConfigEntry EnableSlotSearch { get; set; } public static ConfigEntry ShowRequiredQuest { get; set; } public static ConfigEntry AutoExpandCategories { get; set; } public static ConfigEntry KeepAddOfferOpen { get; set; } @@ -598,6 +599,15 @@ namespace UIFixes null, new ConfigurationManagerAttributes { }))); + configEntries.Add(EnableSlotSearch = config.Bind( + FleaMarketSection, + "Enable Linked Slot Search", + true, + new ConfigDescription( + "Add a context menu to empty mod slots and allow linked searches for specifically that slot", + null, + new ConfigurationManagerAttributes { }))); + configEntries.Add(AutoExpandCategories = config.Bind( FleaMarketSection, "Auto-expand Categories", diff --git a/server/src/RagfairLinkedSlotItemService.ts b/server/src/RagfairLinkedSlotItemService.ts new file mode 100644 index 0000000..7f083e6 --- /dev/null +++ b/server/src/RagfairLinkedSlotItemService.ts @@ -0,0 +1,45 @@ +import { ItemHelper } from "@spt/helpers/ItemHelper"; +import { ITemplateItem } from "@spt/models/eft/common/tables/ITemplateItem"; +import { ILogger } from "@spt/models/spt/utils/ILogger"; +import { DatabaseService } from "@spt/services/DatabaseService"; +import { RagfairLinkedItemService } from "@spt/services/RagfairLinkedItemService"; +import { inject, injectable } from "tsyringe"; + +@injectable() +export class RagfairLinkedSlotItemService extends RagfairLinkedItemService { + constructor( + @inject("DatabaseService") protected databaseService: DatabaseService, + @inject("ItemHelper") protected itemHelper: ItemHelper, + @inject("PrimaryLogger") protected logger: ILogger + ) { + super(databaseService, itemHelper); + } + + public override getLinkedItems(linkedSearchId: string): Set { + const [tpl, slotName] = linkedSearchId.split(":", 2); + + if (slotName) { + this.logger.info(`UIFixes: Finding items for specific slot ${tpl}:${slotName}`); + return this.getSpecificFilter(this.databaseService.getItems()[tpl], slotName); + } + + return super.getLinkedItems(tpl); + } + + protected getSpecificFilter(item: ITemplateItem, slotName: string): Set { + const results = new Set(); + + // For whatever reason, all chamber slots have the name "patron_in_weapon" + const groupName = slotName === "patron_in_weapon" ? "Chambers" : "Slots"; + const group = item._props[groupName] ?? []; + + const sub = group.find(slot => slot._name === slotName); + for (const filter of sub?._props?.filters ?? []) { + for (const f of filter.Filter) { + results.add(f); + } + } + + return results; + } +} diff --git a/server/src/mod.ts b/server/src/mod.ts index 1020953..c46acfb 100644 --- a/server/src/mod.ts +++ b/server/src/mod.ts @@ -10,6 +10,7 @@ import type { ILogger } from "@spt/models/spt/utils/ILogger"; import type { DatabaseService } from "@spt/services/DatabaseService"; import type { StaticRouterModService } from "@spt/services/mod/staticRouter/StaticRouterModService"; import type { ICloner } from "@spt/utils/cloners/ICloner"; +import { RagfairLinkedSlotItemService } from "./RagfairLinkedSlotItemService"; import config from "../config/config.json"; @@ -143,6 +144,10 @@ class UIFixes implements IPreSptLoadMod { ); } + // Register slot-aware linked item service + container.register("RagfairLinkedSlotItemService", RagfairLinkedSlotItemService); + container.register("RagfairLinkedItemService", { useToken: "RagfairLinkedSlotItemService" }); + staticRouterModService.registerStaticRouter( "UIFixesRoutes", [