From f5d3431f71714ec35c4d87461c03e6c91c892520 Mon Sep 17 00:00:00 2001 From: IgorEisberg Date: Sat, 8 Jul 2023 23:42:42 +0300 Subject: [PATCH] Merged to one repo cause platform is limited to 5 repos --- CustomInteractions.Prepatch/Prepatch.cs | 38 +++ CustomInteractions.Prepatch/Prepatch.csproj | 16 ++ CustomInteractions.sln | 30 +++ CustomInteractions/CustomInteractions.cs | 206 +++++++++++++++++ CustomInteractions/CustomInteractions.csproj | 31 +++ CustomInteractions/Plugin.cs | 61 +++++ ItemAttributeFix.sln | 25 ++ ItemAttributeFix/ItemAttributeFix.csproj | 28 +++ ItemAttributeFix/Plugin.cs | 35 +++ ItemContextMenuExt.sln | 25 ++ ItemContextMenuExt/ItemContextMenuExt.cs | 185 +++++++++++++++ ItemContextMenuExt/ItemContextMenuExt.csproj | 37 +++ ItemContextMenuExt/Plugin.cs | 14 ++ ItemSellPrice.sln | 25 ++ ItemSellPrice/ItemSellPrice.cs | 217 ++++++++++++++++++ ItemSellPrice/ItemSellPrice.csproj | 34 +++ ItemSellPrice/Plugin.cs | 82 +++++++ MagazineInspector.sln | 25 ++ MagazineInspector/MagazineInspector.cs | 157 +++++++++++++ MagazineInspector/MagazineInspector.csproj | 34 +++ MagazineInspector/Plugin.cs | 27 +++ MunitionsExpert.sln | 25 ++ MunitionsExpert/MunitionsExpert.cs | 195 ++++++++++++++++ MunitionsExpert/MunitionsExpert.csproj | 55 +++++ MunitionsExpert/Plugin.cs | 96 ++++++++ .../Properties/Resources.Designer.cs | 113 +++++++++ MunitionsExpert/Properties/Resources.resx | 136 +++++++++++ MunitionsExpert/Resources/ArmorDamage.png | Bin 0 -> 649 bytes MunitionsExpert/Resources/Damage.png | Bin 0 -> 764 bytes .../Resources/FragmentationChance.png | Bin 0 -> 733 bytes .../Resources/PenetrationPower.png | Bin 0 -> 468 bytes MunitionsExpert/Resources/RicochetChance.png | Bin 0 -> 681 bytes 32 files changed, 1952 insertions(+) create mode 100644 CustomInteractions.Prepatch/Prepatch.cs create mode 100644 CustomInteractions.Prepatch/Prepatch.csproj create mode 100644 CustomInteractions.sln create mode 100644 CustomInteractions/CustomInteractions.cs create mode 100644 CustomInteractions/CustomInteractions.csproj create mode 100644 CustomInteractions/Plugin.cs create mode 100644 ItemAttributeFix.sln create mode 100644 ItemAttributeFix/ItemAttributeFix.csproj create mode 100644 ItemAttributeFix/Plugin.cs create mode 100644 ItemContextMenuExt.sln create mode 100644 ItemContextMenuExt/ItemContextMenuExt.cs create mode 100644 ItemContextMenuExt/ItemContextMenuExt.csproj create mode 100644 ItemContextMenuExt/Plugin.cs create mode 100644 ItemSellPrice.sln create mode 100644 ItemSellPrice/ItemSellPrice.cs create mode 100644 ItemSellPrice/ItemSellPrice.csproj create mode 100644 ItemSellPrice/Plugin.cs create mode 100644 MagazineInspector.sln create mode 100644 MagazineInspector/MagazineInspector.cs create mode 100644 MagazineInspector/MagazineInspector.csproj create mode 100644 MagazineInspector/Plugin.cs create mode 100644 MunitionsExpert.sln create mode 100644 MunitionsExpert/MunitionsExpert.cs create mode 100644 MunitionsExpert/MunitionsExpert.csproj create mode 100644 MunitionsExpert/Plugin.cs create mode 100644 MunitionsExpert/Properties/Resources.Designer.cs create mode 100644 MunitionsExpert/Properties/Resources.resx create mode 100644 MunitionsExpert/Resources/ArmorDamage.png create mode 100644 MunitionsExpert/Resources/Damage.png create mode 100644 MunitionsExpert/Resources/FragmentationChance.png create mode 100644 MunitionsExpert/Resources/PenetrationPower.png create mode 100644 MunitionsExpert/Resources/RicochetChance.png diff --git a/CustomInteractions.Prepatch/Prepatch.cs b/CustomInteractions.Prepatch/Prepatch.cs new file mode 100644 index 0000000..3e00f9b --- /dev/null +++ b/CustomInteractions.Prepatch/Prepatch.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; +using Mono.Cecil; + +namespace IcyClawz.CustomInteractions +{ + public static class Prepatch + { + public static IEnumerable TargetDLLs => new[] { "Assembly-CSharp.dll" }; + + public static void Patch(ref AssemblyDefinition assembly) + { + var type = assembly.MainModule.GetType("GClass2654"); // DynamicInteraction + if (type != null) + { + type.IsSealed = false; + var field = type.Fields.SingleOrDefault(c => c.Name == "action_0"); + if (field != null) + { + field.IsFamily = true; + field.IsInitOnly = false; + } + var ctor = type.Methods.SingleOrDefault(c => c.Name == ".ctor"); + if (ctor != null) + { + var param = ctor.Parameters.SingleOrDefault(c => c.Name == "callback"); + if (param != null) + { + param.IsOptional = true; + param.HasDefault = true; + param.Constant = null; + } + } + } + //assembly.Write("Assembly-CSharp-CustomInteractions.dll"); + } + } +} diff --git a/CustomInteractions.Prepatch/Prepatch.csproj b/CustomInteractions.Prepatch/Prepatch.csproj new file mode 100644 index 0000000..e6424ac --- /dev/null +++ b/CustomInteractions.Prepatch/Prepatch.csproj @@ -0,0 +1,16 @@ + + + + net472 + IcyClawz.CustomInteractions.Prepatch + 1.1.1 + IcyClawz.CustomInteractions + + + + + ..\Shared\Mono.Cecil.dll + + + + diff --git a/CustomInteractions.sln b/CustomInteractions.sln new file mode 100644 index 0000000..3b9a112 --- /dev/null +++ b/CustomInteractions.sln @@ -0,0 +1,30 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32616.157 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomInteractions", "CustomInteractions\CustomInteractions.csproj", "{F296FF55-A8D8-47E3-90C0-6975CA0787F9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomInteractions.Prepatch", "CustomInteractions.Prepatch\Prepatch.csproj", "{E8C5359B-95CE-4547-B72E-C9BEBB9220CC}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F296FF55-A8D8-47E3-90C0-6975CA0787F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F296FF55-A8D8-47E3-90C0-6975CA0787F9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F296FF55-A8D8-47E3-90C0-6975CA0787F9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F296FF55-A8D8-47E3-90C0-6975CA0787F9}.Release|Any CPU.Build.0 = Release|Any CPU + {E8C5359B-95CE-4547-B72E-C9BEBB9220CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E8C5359B-95CE-4547-B72E-C9BEBB9220CC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E8C5359B-95CE-4547-B72E-C9BEBB9220CC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E8C5359B-95CE-4547-B72E-C9BEBB9220CC}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {BF50C8C8-82A6-496A-B4A4-DB383A88705D} + EndGlobalSection +EndGlobal diff --git a/CustomInteractions/CustomInteractions.cs b/CustomInteractions/CustomInteractions.cs new file mode 100644 index 0000000..b3a71ae --- /dev/null +++ b/CustomInteractions/CustomInteractions.cs @@ -0,0 +1,206 @@ +using EFT.InventoryLogic; +using EFT.UI; +using JetBrains.Annotations; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using UnityEngine; + +using DynamicInteraction = GClass2654; +using EmptyInteractions = GClass2655; + +namespace IcyClawz.CustomInteractions +{ + [EditorBrowsable(EditorBrowsableState.Never)] + public interface ICustomInteractionsProvider { } // Do not implement this directly + + public interface IItemCustomInteractionsProvider : ICustomInteractionsProvider + { + IEnumerable GetCustomInteractions(ItemUiContext uiContext, EItemViewType viewType, Item item); + } + + public static class CustomInteractionsManager + { + internal static readonly List Providers = new List(); + + public static void Register(ICustomInteractionsProvider provider) + { + if (!Providers.Contains(provider)) + { + Providers.Add(provider); + } + } + } + + public class CustomInteraction + { + internal readonly CustomInteractionImpl Impl; + + public CustomInteraction() + { + Impl = new CustomInteractionImpl(); + } + + public Func Caption { get => Impl.Caption; set => Impl.Caption = value; } + public Func Icon { get => Impl.Icon; set => Impl.Icon = value; } + public Action Action { get => Impl.Action; set => Impl.Action = value; } + public Func SubMenu { get => Impl.SubMenu; set => Impl.SubMenu = value; } + public Func Enabled { get => Impl.Enabled; set => Impl.Enabled = value; } + public Func Error { get => Impl.Error; set => Impl.Error = value; } + } + + internal sealed class CustomInteractionImpl : DynamicInteraction + { + public CustomInteractionImpl() + : base(UnityEngine.Random.Range(0, int.MaxValue).ToString("x4")) { } + + public Func Caption { get; set; } + public new Func Icon { get; set; } + public Action Action { get => action_0; set => action_0 = value; } + public Func SubMenu { get; set; } + public Func Enabled { get; set; } + public Func Error { get; set; } + + public bool IsInteractive() => + Enabled?.Invoke() ?? true; + } + + public abstract class CustomSubInteractions + { + internal readonly CustomSubInteractionsImpl Impl; + + public CustomSubInteractions(ItemUiContext uiContext) + { + Impl = new CustomSubInteractionsImpl(uiContext); + } + + public bool ExaminationRequired { get => Impl._ExaminationRequired; set => Impl._ExaminationRequired = value; } + + public void Add(CustomInteraction customInteraction) => + Impl.AddCustomInteraction(customInteraction); + + public void Remove(CustomInteraction customInteraction) => + Impl.RemoveCustomInteraction(customInteraction); + + public void CallRedraw() => + Impl.CallRedraw(); + + public void CallRedraw(string templateId) => + Impl.CallRedraw(templateId); + } + + internal sealed class CustomSubInteractionsImpl : EmptyInteractions + { + public CustomSubInteractionsImpl(ItemUiContext uiContext) + : base(uiContext) { } + + public IEnumerable CustomInteractions => DynamicInteractions.OfType(); + + public bool _ExaminationRequired { get; set; } = true; + + public override bool ExaminationRequired => _ExaminationRequired; + + public override bool HasIcons => CustomInteractions.Any(customInteraction => customInteraction.Icon != null); + + public void CallRedraw() => + itemUiContext_0.RedrawContextMenus(null); + } + + internal static class AbstractInteractionsExtensions + { + private static Dictionary GetDynamicInteractions(this GClass2655 instance) where T : Enum => + typeof(GClass2655).GetField("dictionary_1", BindingFlags.NonPublic | BindingFlags.Instance) + .GetValue(instance) as Dictionary; + + public static void AddCustomInteraction(this GClass2655 instance, CustomInteraction customInteraction) where T : Enum => + instance.GetDynamicInteractions()[customInteraction.Impl.Key] = customInteraction.Impl; + + public static void RemoveCustomInteraction(this GClass2655 instance, CustomInteraction customInteraction) where T : Enum => + instance.GetDynamicInteractions().Remove(customInteraction.Impl.Key); + } + + internal static class InteractionButtonsContainerExtensions + { + private static readonly FieldInfo ButtonsContainerField = + typeof(InteractionButtonsContainer).GetField("_buttonsContainer", BindingFlags.NonPublic | BindingFlags.Instance); + + private static RectTransform GetButtonsContainer(this InteractionButtonsContainer instance) => + ButtonsContainerField.GetValue(instance) as RectTransform; + + private static readonly FieldInfo ButtonTemplateField = + typeof(InteractionButtonsContainer).GetField("_buttonTemplate", BindingFlags.NonPublic | BindingFlags.Instance); + + private static SimpleContextMenuButton GetButtonTemplate(this InteractionButtonsContainer instance) => + ButtonTemplateField.GetValue(instance) as SimpleContextMenuButton; + + private static readonly FieldInfo CurrentButtonField = + typeof(InteractionButtonsContainer).GetField("simpleContextMenuButton_0", BindingFlags.NonPublic | BindingFlags.Instance); + + private static void SetCurrentButton(this InteractionButtonsContainer instance, SimpleContextMenuButton button) => + CurrentButtonField.SetValue(instance, button); + + private static readonly MethodInfo CreateButtonMethod = + typeof(InteractionButtonsContainer).GetMethod("method_1", BindingFlags.NonPublic | BindingFlags.Instance); + + private static SimpleContextMenuButton CreateButton(this InteractionButtonsContainer instance, + string key, string caption, SimpleContextMenuButton template, RectTransform container, + [CanBeNull] Sprite sprite, [CanBeNull] Action onButtonClicked, [CanBeNull] Action onMouseHover, + bool subMenu = false, bool autoClose = true) => + (SimpleContextMenuButton)CreateButtonMethod.Invoke(instance, new object[] { + key, caption, template, container, sprite, onButtonClicked, onMouseHover, subMenu, autoClose + }); + + private static readonly MethodInfo CloseSubMenuMethod = + typeof(InteractionButtonsContainer).GetMethod("method_4", BindingFlags.NonPublic | BindingFlags.Instance); + + private static void CloseSubMenu(this InteractionButtonsContainer instance) => + CloseSubMenuMethod.Invoke(instance, null); + + private static readonly MethodInfo AddButtonMethod = + typeof(InteractionButtonsContainer).GetMethod("method_5", BindingFlags.NonPublic | BindingFlags.Instance); + + private static void AddButton(this InteractionButtonsContainer instance, SimpleContextMenuButton button) => + AddButtonMethod.Invoke(instance, new object[] { button }); + + public static void AddCustomButton(this InteractionButtonsContainer instance, CustomInteractionImpl customInteractionImpl) + { + bool isInteractive = customInteractionImpl.IsInteractive(); + SimpleContextMenuButton button = null; + button = instance.CreateButton( + customInteractionImpl.Key, + customInteractionImpl.Caption?.Invoke() ?? string.Empty, + instance.GetButtonTemplate(), + instance.GetButtonsContainer(), + customInteractionImpl.Icon?.Invoke(), + () => + { + if (isInteractive) + { + customInteractionImpl.Execute(); + } + }, + () => + { + instance.SetCurrentButton(button); + instance.CloseSubMenu(); + if (isInteractive) + { + var subMenu = customInteractionImpl.SubMenu?.Invoke(); + if (subMenu != null) + { + instance.SetSubInteractions(subMenu.Impl); + } + } + }, + customInteractionImpl.SubMenu != null, + false + ); + button.SetButtonInteraction( + (isInteractive, customInteractionImpl.Error?.Invoke() ?? string.Empty) + ); + instance.AddButton(button); + } + } +} \ No newline at end of file diff --git a/CustomInteractions/CustomInteractions.csproj b/CustomInteractions/CustomInteractions.csproj new file mode 100644 index 0000000..faa7847 --- /dev/null +++ b/CustomInteractions/CustomInteractions.csproj @@ -0,0 +1,31 @@ + + + + net472 + IcyClawz.CustomInteractions + 1.1.1 + IcyClawz.CustomInteractions + + + + + ..\Shared\Aki.Reflection.dll + + + ..\Shared\Assembly-CSharp-CustomInteractions.dll + + + ..\Shared\BepInEx.dll + + + ..\Shared\Sirenix.Serialization.dll + + + ..\Shared\UnityEngine.dll + + + ..\Shared\UnityEngine.CoreModule.dll + + + + diff --git a/CustomInteractions/Plugin.cs b/CustomInteractions/Plugin.cs new file mode 100644 index 0000000..fc2a80b --- /dev/null +++ b/CustomInteractions/Plugin.cs @@ -0,0 +1,61 @@ +using Aki.Reflection.Patching; +using BepInEx; +using EFT.UI; +using System.Linq; +using System.Reflection; + +using DynamicInteraction = GClass2654; +using ItemContext = GClass2466; +using ItemInfoInteractions = GClass2655; + +namespace IcyClawz.CustomInteractions +{ + [BepInPlugin("com.IcyClawz.CustomInteractions", "IcyClawz.CustomInteractions", "1.1.1")] + public class Plugin : BaseUnityPlugin + { + private void Awake() + { + new ItemUiContextPatch().Enable(); + new InteractionButtonsContainerPatch().Enable(); + } + } + + public class ItemUiContextPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() => + typeof(ItemUiContext).GetMethod("GetItemContextInteractions", BindingFlags.Public | BindingFlags.Instance); + + [PatchPostfix] + private static void Postfix(ref ItemInfoInteractions __result, ItemUiContext __instance, ItemContext itemContext) + { + foreach (var provider in CustomInteractionsManager.Providers.OfType()) + { + var customInteractions = provider.GetCustomInteractions(__instance, itemContext.ViewType, itemContext.Item); + if (customInteractions != null) + { + foreach (CustomInteraction customInteraction in customInteractions) + { + __result.AddCustomInteraction(customInteraction); + } + } + } + } + } + + public class InteractionButtonsContainerPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() => + typeof(InteractionButtonsContainer).GetMethod("method_3", BindingFlags.NonPublic | BindingFlags.Instance); + + [PatchPrefix] + private static bool Prefix(ref InteractionButtonsContainer __instance, DynamicInteraction interaction) + { + if (interaction is CustomInteractionImpl customInteractionImpl) + { + __instance.AddCustomButton(customInteractionImpl); + return false; + } + return true; + } + } +} diff --git a/ItemAttributeFix.sln b/ItemAttributeFix.sln new file mode 100644 index 0000000..3cc9e11 --- /dev/null +++ b/ItemAttributeFix.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32616.157 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{1A806857-2204-4EF9-ADDA-36F4C5688DC1}") = "ItemAttributeFix", "ItemAttributeFix\ItemAttributeFix.csproj", "{F3186B2D-F170-4E31-98BF-4CC8CDAD565D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F3186B2D-F170-4E31-98BF-4CC8CDAD565D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F3186B2D-F170-4E31-98BF-4CC8CDAD565D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F3186B2D-F170-4E31-98BF-4CC8CDAD565D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F3186B2D-F170-4E31-98BF-4CC8CDAD565D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {BD4846A6-29F2-4AD6-91A5-7A0CA28D88CC} + EndGlobalSection +EndGlobal diff --git a/ItemAttributeFix/ItemAttributeFix.csproj b/ItemAttributeFix/ItemAttributeFix.csproj new file mode 100644 index 0000000..acd11ff --- /dev/null +++ b/ItemAttributeFix/ItemAttributeFix.csproj @@ -0,0 +1,28 @@ + + + + net472 + IcyClawz.ItemAttributeFix + 1.0.0 + IcyClawz.ItemAttributeFix + + + + + ..\Shared\Aki.Reflection.dll + + + ..\Shared\Assembly-CSharp.dll + + + ..\Shared\BepInEx.dll + + + ..\Shared\UnityEngine.dll + + + ..\Shared\UnityEngine.CoreModule.dll + + + + diff --git a/ItemAttributeFix/Plugin.cs b/ItemAttributeFix/Plugin.cs new file mode 100644 index 0000000..c8a8fd6 --- /dev/null +++ b/ItemAttributeFix/Plugin.cs @@ -0,0 +1,35 @@ +using Aki.Reflection.Patching; +using BepInEx; +using EFT.UI; +using System.Reflection; + +namespace IcyClawz.ItemAttributeFix +{ + [BepInPlugin("com.IcyClawz.ItemAttributeFix", "IcyClawz.ItemAttributeFix", "1.0.0")] + public class Plugin : BaseUnityPlugin + { + private void Awake() + { + new CompactCharacteristicPanelPatch().Enable(); + } + } + + public class CompactCharacteristicPanelPatch : ModulePatch + { + private static readonly FieldInfo ItemAttributeField = + typeof(CompactCharacteristicPanel).GetField("ItemAttribute", BindingFlags.NonPublic | BindingFlags.Instance); + + private static readonly FieldInfo StringField = + typeof(CompactCharacteristicPanel).GetField("string_0", BindingFlags.NonPublic | BindingFlags.Instance); + + protected override MethodBase GetTargetMethod() => + typeof(CompactCharacteristicPanel).GetMethod("SetValues", BindingFlags.Public | BindingFlags.Instance); + + [PatchPostfix] + private static void PatchPostfix(ref CompactCharacteristicPanel __instance) + { + ItemAttributeClass attribute = ItemAttributeField.GetValue(__instance) as ItemAttributeClass; + StringField.SetValue(__instance, attribute.FullStringValue()); + } + } +} diff --git a/ItemContextMenuExt.sln b/ItemContextMenuExt.sln new file mode 100644 index 0000000..43fbc32 --- /dev/null +++ b/ItemContextMenuExt.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32616.157 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{16C72132-089A-4D00-BDA9-5DFC76E223AC}") = "ItemContextMenuExt", "ItemContextMenuExt\ItemContextMenuExt.csproj", "{F9FF8A66-A96D-45D2-BBD5-D976F101CCF7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F9FF8A66-A96D-45D2-BBD5-D976F101CCF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F9FF8A66-A96D-45D2-BBD5-D976F101CCF7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F9FF8A66-A96D-45D2-BBD5-D976F101CCF7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F9FF8A66-A96D-45D2-BBD5-D976F101CCF7}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B44F10FA-AB5A-4200-80B2-4ED636043F28} + EndGlobalSection +EndGlobal diff --git a/ItemContextMenuExt/ItemContextMenuExt.cs b/ItemContextMenuExt/ItemContextMenuExt.cs new file mode 100644 index 0000000..1793806 --- /dev/null +++ b/ItemContextMenuExt/ItemContextMenuExt.cs @@ -0,0 +1,185 @@ +using Comfort.Common; +using EFT; +using EFT.InventoryLogic; +using EFT.UI; +using IcyClawz.CustomInteractions; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEngine; + +using ILightTemplate = GInterface240; +using LightsState = GStruct143; +using ResourceCache = GClass2014; + +namespace IcyClawz.ItemContextMenuExt +{ + internal static class PlayerExtensions + { + private static readonly FieldInfo InventoryControllerField = + typeof(Player).GetField("_inventoryController", BindingFlags.NonPublic | BindingFlags.Instance); + + public static InventoryControllerClass GetInventoryController(this Player player) => + InventoryControllerField.GetValue(player) as InventoryControllerClass; + } + + internal static class LightComponentExtensions + { + public static int GetModesCount(this LightComponent component) => + ((ILightTemplate)component.Item.Template).ModesCount; + } + + public sealed class CustomInteractionsProvider : IItemCustomInteractionsProvider + { + internal const string IconsPrefix = "Characteristics/Icons/"; + internal static StaticIcons StaticIcons => EFTHardSettings.Instance.StaticIcons; + + public IEnumerable GetCustomInteractions(ItemUiContext uiContext, EItemViewType viewType, Item item) + { + if (viewType != EItemViewType.Inventory) + { + yield break; + } + { + var component = item.GetItemComponent(); + if (component != null) + { + // Firing mode + yield return new CustomInteraction() + { + Caption = () => "Firing mode", + Icon = () => StaticIcons.GetAttributeIcon(EItemAttributeId.Weapon), + SubMenu = () => new FireModeSubMenu(uiContext, component), + Enabled = () => component.AvailableEFireModes.Length > 1, + Error = () => "This weapon is incapable of selective fire" + }; + yield break; + } + } + { + var component = item.GetItemComponent(); + if (component != null) + { + // Turn on/off + yield return new CustomInteraction() + { + Caption = () => (component.IsActive ? "TurnOff" : "TurnOn").Localized(), + Icon = () => ResourceCache.Pop(IconsPrefix + (component.IsActive ? "TurnOff" : "TurnOn")), + Action = () => + { + Singleton.Instance.PlayUISound(EUISoundType.MenuContextMenu); + ComponentUtils.SetLightState(component, !component.IsActive, component.SelectedMode); + uiContext.RedrawContextMenus(new[] { item.TemplateId }); + } + }; + // Switch mode + yield return new CustomInteraction() + { + Caption = () => "Switch mode", + Icon = () => StaticIcons.GetAttributeIcon(EItemAttributeId.EncodeState), + SubMenu = () => new LightModeSubMenu(uiContext, component), + Enabled = () => component.GetModesCount() > 1, + Error = () => "This device has no alternative modes" + }; + yield break; + } + } + } + } + + internal class FireModeSubMenu : CustomSubInteractions + { + public FireModeSubMenu(ItemUiContext uiContext, FireModeComponent component) + : base(uiContext) + { + foreach (var fireMode in component.AvailableEFireModes) + { + Add(new CustomInteraction() + { + Caption = () => fireMode.ToString().Localized(), + Action = () => + { + Singleton.Instance.PlayUISound(EUISoundType.MenuContextMenu); + ComponentUtils.SetFireMode(component, fireMode); + }, + Enabled = () => fireMode != component.FireMode + }); + } + } + } + + internal class LightModeSubMenu : CustomSubInteractions + { + public LightModeSubMenu(ItemUiContext uiContext, LightComponent component) + : base(uiContext) + { + foreach (var lightMode in Enumerable.Range(0, component.GetModesCount())) + { + Add(new CustomInteraction() + { + Caption = () => $"Mode {lightMode + 1}", + Action = () => + { + Singleton.Instance.PlayUISound(EUISoundType.MenuContextMenu); + ComponentUtils.SetLightState(component, component.IsActive, lightMode); + }, + Enabled = () => lightMode != component.SelectedMode + }); + } + } + } + + internal static class ComponentUtils + { + public static void SetFireMode(FireModeComponent component, Weapon.EFireMode fireMode) + { + var player = GamePlayerOwner.MyPlayer; + if (player != null && player.HandsController is Player.FirearmController fc && component.Item == fc.Item) + { + if (fc.Item.MalfState.State == Weapon.EMalfunctionState.None) + { + fc.ChangeFireMode(fireMode); + } + else + { + fc.FirearmsAnimator.MisfireSlideUnknown(false); + player.GetInventoryController().ExamineMalfunction(fc.Item, false); + } + return; + } + + component.SetFireMode(fireMode); + } + + public static void SetLightState(LightComponent component, bool isActive, int lightMode) + { + var player = GamePlayerOwner.MyPlayer; + if (player != null && player.HandsController is Player.FirearmController fc && component.Item.IsChildOf(fc.Item)) + { + var state = new LightsState + { + Id = component.Item.Id, + IsActive = isActive, + LightMode = lightMode + }; + fc.SetLightsState(new[] { state }); + return; + } + + component.IsActive = isActive; + component.SelectedMode = lightMode; + + if (player != null) + { + foreach (var tcvc in player.GetComponentsInChildren()) + { + if (ReferenceEquals(tcvc.LightMod, component)) + { + tcvc.UpdateBeams(); + break; + } + } + } + } + } +} \ No newline at end of file diff --git a/ItemContextMenuExt/ItemContextMenuExt.csproj b/ItemContextMenuExt/ItemContextMenuExt.csproj new file mode 100644 index 0000000..5bcf443 --- /dev/null +++ b/ItemContextMenuExt/ItemContextMenuExt.csproj @@ -0,0 +1,37 @@ + + + + net472 + IcyClawz.ItemContextMenuExt + 1.0.1 + IcyClawz.ItemContextMenuExt + + + + + ..\Shared\Assembly-CSharp.dll + + + ..\Shared\BepInEx.dll + + + ..\Shared\Comfort.dll + + + ..\Shared\IcyClawz.CustomInteractions.dll + + + ..\Shared\ItemComponent.Types.dll + + + ..\Shared\ItemTemplate.Types.dll + + + ..\Shared\UnityEngine.dll + + + ..\Shared\UnityEngine.CoreModule.dll + + + + diff --git a/ItemContextMenuExt/Plugin.cs b/ItemContextMenuExt/Plugin.cs new file mode 100644 index 0000000..0a76a2f --- /dev/null +++ b/ItemContextMenuExt/Plugin.cs @@ -0,0 +1,14 @@ +using BepInEx; +using IcyClawz.CustomInteractions; + +namespace IcyClawz.ItemContextMenuExt +{ + [BepInPlugin("com.IcyClawz.ItemContextMenuExt", "IcyClawz.ItemContextMenuExt", "1.0.1")] + public class Plugin : BaseUnityPlugin + { + private void Awake() + { + CustomInteractionsManager.Register(new CustomInteractionsProvider()); + } + } +} diff --git a/ItemSellPrice.sln b/ItemSellPrice.sln new file mode 100644 index 0000000..9e016be --- /dev/null +++ b/ItemSellPrice.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32616.157 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{1A9E4EC8-A583-446A-8918-2608C278F194}") = "ItemSellPrice", "ItemSellPrice\ItemSellPrice.csproj", "{FA2EAB19-7A7F-452F-80B3-DEAD18E43D88}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FA2EAB19-7A7F-452F-80B3-DEAD18E43D88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FA2EAB19-7A7F-452F-80B3-DEAD18E43D88}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FA2EAB19-7A7F-452F-80B3-DEAD18E43D88}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FA2EAB19-7A7F-452F-80B3-DEAD18E43D88}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B6B3C802-40C3-4FF0-9BAA-1DAC503CA619} + EndGlobalSection +EndGlobal diff --git a/ItemSellPrice/ItemSellPrice.cs b/ItemSellPrice/ItemSellPrice.cs new file mode 100644 index 0000000..8b9b838 --- /dev/null +++ b/ItemSellPrice/ItemSellPrice.cs @@ -0,0 +1,217 @@ +using Aki.Reflection.Utils; +using Comfort.Common; +using EFT; +using EFT.InventoryLogic; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEngine; + +using CurrencyUtil = GClass2181; + +namespace IcyClawz.ItemSellPrice +{ + public static class ItemSellPrice + { + private static readonly Dictionary DisplayNames = new Dictionary() + { + { "ch", new[] { "售价({0})", "无法出售给商人" } }, + { "cz", new[] { "Prodejní cena ({0})", "Nemůže být prodán obchodníkům" } }, + { "en", new[] { "Selling price ({0})", "Cannot be sold to traders" } }, + { "es", new[] { "Precio de venta ({0})", "No puede ser vendido a los vendedores" } }, + { "es-mx", new[] { "Precio de venta ({0})", "No puede ser vendido a comerciantes" } }, + { "fr", new[] { "Prix de vente ({0})", "Ne peut pas être vendu aux marchands" } }, + { "ge", new[] { "Verkaufspreis ({0})", "Kann nicht an Händler verkauft werden" } }, + { "hu", new[] { "Eladási ár ({0})", "Kereskedőknek nem adható el" } }, + { "it", new[] { "Prezzo di vendita ({0})", "Non vendibile ai mercanti" } }, + { "jp", new[] { "販売価格({0})", "トレーダーには販売できません" } }, + { "kr", new[] { "판매 가격 ({0})", "상인에게 판매 불가" } }, + { "pl", new[] { "Cena sprzedaży ({0})", "Nie można sprzedawać handlarzom" } }, + { "po", new[] { "Preço de venda ({0})", "Não pode ser vendido a comerciantes" } }, + { "ru", new[] { "Цена продажи ({0})", "Невозможно продать торговцам" } }, + { "sk", new[] { "Predajná cena ({0})", "Nedá sa predať obchodníkom" } }, + { "tu", new[] { "Satış fiyatı ({0})", "Tüccarlara satılamaz" } } + }; + + private static readonly FieldInfo SupplyDataField = + typeof(TraderClass).GetField("supplyData_0", BindingFlags.NonPublic | BindingFlags.Instance); + + private static ISession Session => ClientAppUtils.GetMainApp().GetClientBackEndSession(); + + public static async void UpdateSupplyData(this TraderClass trader) + { + Result result = await Session.GetSupplyData(trader.Id); + if (result.Failed) + { + Debug.LogError("Failed to download supply data"); + return; + } + trader.SetSupplyData(result.Value); + } + + public static SupplyData GetSupplyData(this TraderClass trader) => + SupplyDataField.GetValue(trader) as SupplyData; + + public static void SetSupplyData(this TraderClass trader, SupplyData supplyData) => + SupplyDataField.SetValue(trader, supplyData); + + public static void AddTraderOfferAttribute(this Item item) + { + List attributes = new List + { + new ItemAttributeClass(EItemAttributeId.MoneySum) + { + Name = EItemAttributeId.MoneySum.GetName(), + DisplayNameFunc = () => GetDisplayName(item), + Base = () => GetBase(item), + StringValue = () => GetStringValue(item), + FullStringValue = () => GetFullStringValue(item), + DisplayType = () => EItemAttributeDisplayType.Compact + } + }; + attributes.AddRange(item.Attributes); + item.Attributes = attributes; + } + + private sealed class TraderOffer + { + public string Name; + public int Price; + public string Currency; + public double Course; + public int Count; + + public TraderOffer(string name, int price, string currency, double course, int count) + { + Name = name; + Price = price; + Currency = currency; + Course = course; + Count = count; + } + } + + private static TraderOffer GetTraderOffer(Item item, TraderClass trader) + { + var result = trader.GetUserItemPrice(item); + if (result == null) + { + return null; + } + return new TraderOffer( + trader.LocalizedName, + result.Value.Amount, + CurrencyUtil.GetCurrencyCharById(result.Value.CurrencyId), + trader.GetSupplyData().CurrencyCourses[result.Value.CurrencyId], + item.StackObjectsCount + ); + } + + private static List GetAllTraderOffers(Item item) + { + if (!Session.Profile.Examined(item)) + { + return null; + } + switch (item.Owner?.OwnerType) + { + case EOwnerType.RagFair: + case EOwnerType.Trader: + if (item.StackObjectsCount > 1 || item.UnlimitedCount) + { + item = item.CloneItem(); + item.StackObjectsCount = 1; + item.UnlimitedCount = false; + } + break; + } + List offers = new List(); + foreach (TraderClass trader in Session.Traders) + { + if (GetTraderOffer(item, trader) is TraderOffer offer) + { + offers.Add(offer); + } + } + offers.Sort((x, y) => (y.Price * y.Course).CompareTo(x.Price * x.Course)); + return offers; + } + + private static TraderOffer GetBestTraderOffer(Item item) + { + if (GetAllTraderOffers(item) is List offers) + { + return offers.FirstOrDefault(); + } + else + { + return null; + } + } + + public static string GetDisplayName(Item item) + { + string language = Singleton.Instance?.Game?.Settings?.Language?.GetValue(); + if (language == null || !DisplayNames.ContainsKey(language)) + { + language = "en"; + } + if (GetBestTraderOffer(item) is TraderOffer offer) + { + return string.Format(DisplayNames[language][0], offer.Name); + } + else + { + return DisplayNames[language][1]; + } + } + + public static float GetBase(Item item) + { + if (GetBestTraderOffer(item) is TraderOffer offer) + { + return offer.Price; + } + else + { + return 0.01f; + } + } + + public static string GetStringValue(Item item) + { + if (GetBestTraderOffer(item) is TraderOffer offer) + { + string value = $"{offer.Currency} {offer.Price}"; + if (offer.Count > 1) + { + value += $" ({offer.Count})"; + } + return value; + } + else + { + return string.Empty; + } + } + + public static string GetFullStringValue(Item item) + { + if (GetAllTraderOffers(item) is List offers) + { + string[] lines = new string[offers.Count]; + for (int i = 0; i < offers.Count; i++) + { + TraderOffer offer = offers[i]; + lines[i] = $"{offer.Name}: {offer.Currency} {offer.Price}"; + } + return string.Join(Environment.NewLine, lines); + } + else + { + return string.Empty; + } + } + } +} \ No newline at end of file diff --git a/ItemSellPrice/ItemSellPrice.csproj b/ItemSellPrice/ItemSellPrice.csproj new file mode 100644 index 0000000..e415dd4 --- /dev/null +++ b/ItemSellPrice/ItemSellPrice.csproj @@ -0,0 +1,34 @@ + + + + net472 + IcyClawz.ItemSellPrice + 1.0.2 + IcyClawz.ItemSellPrice + + + + + ..\Shared\Aki.Common.dll + + + ..\Shared\Aki.Reflection.dll + + + ..\Shared\Assembly-CSharp.dll + + + ..\Shared\BepInEx.dll + + + ..\Shared\Comfort.dll + + + ..\Shared\UnityEngine.dll + + + ..\Shared\UnityEngine.CoreModule.dll + + + + diff --git a/ItemSellPrice/Plugin.cs b/ItemSellPrice/Plugin.cs new file mode 100644 index 0000000..6bc6715 --- /dev/null +++ b/ItemSellPrice/Plugin.cs @@ -0,0 +1,82 @@ +using Aki.Reflection.Patching; +using BepInEx; +using EFT.InventoryLogic; +using System.Reflection; + +namespace IcyClawz.ItemSellPrice +{ + [BepInPlugin("com.IcyClawz.ItemSellPrice", "IcyClawz.ItemSellPrice", "1.0.2")] + public class Plugin : BaseUnityPlugin + { + private void Awake() + { + new TraderPatch().Enable(); + new ItemPatch().Enable(); + new AmmoPatch().Enable(); + new GrenadePatch().Enable(); + new SecureContainerPatch().Enable(); + } + } + + public class TraderPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() => + typeof(TraderClass).GetConstructors()[0]; + + [PatchPostfix] + private static void PatchPostfix(ref TraderClass __instance) + { + __instance.UpdateSupplyData(); + } + } + + public class ItemPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() => + typeof(Item).GetConstructors()[0]; + + [PatchPostfix] + private static void PatchPostfix(ref Item __instance) + { + __instance.AddTraderOfferAttribute(); + } + } + + public class AmmoPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() => + typeof(BulletClass).GetConstructors()[0]; + + [PatchPostfix] + private static void PatchPostfix(ref BulletClass __instance) + { + __instance.AddTraderOfferAttribute(); + } + } + + public class GrenadePatch : ModulePatch + { + protected override MethodBase GetTargetMethod() => + typeof(GrenadeClass).GetConstructors()[0]; + + [PatchPostfix] + private static void PatchPostfix(ref GrenadeClass __instance) + { + __instance.AddTraderOfferAttribute(); + } + } + + public class SecureContainerPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return typeof(ItemContainerClass).GetConstructors()[0]; + } + + [PatchPostfix] + private static void PatchPostfix(ref ItemContainerClass __instance) + { + __instance.AddTraderOfferAttribute(); + } + } +} diff --git a/MagazineInspector.sln b/MagazineInspector.sln new file mode 100644 index 0000000..f0e2ac5 --- /dev/null +++ b/MagazineInspector.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32616.157 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{1528224F-F248-466A-95DF-C841EA46D89E}") = "MagazineInspector", "MagazineInspector\MagazineInspector.csproj", "{F4F0DAB6-3A97-4656-A61F-945D63BAB4F8}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F4F0DAB6-3A97-4656-A61F-945D63BAB4F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F4F0DAB6-3A97-4656-A61F-945D63BAB4F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F4F0DAB6-3A97-4656-A61F-945D63BAB4F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F4F0DAB6-3A97-4656-A61F-945D63BAB4F8}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {BEB55B48-4A92-4A7E-8B4E-58FFADC9FE2C} + EndGlobalSection +EndGlobal diff --git a/MagazineInspector/MagazineInspector.cs b/MagazineInspector/MagazineInspector.cs new file mode 100644 index 0000000..e454431 --- /dev/null +++ b/MagazineInspector/MagazineInspector.cs @@ -0,0 +1,157 @@ +using Aki.Reflection.Utils; +using Comfort.Common; +using EFT; +using EFT.InventoryLogic; +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +using InGameStatus = GClass1756; + +namespace IcyClawz.MagazineInspector +{ + public static class MagazineInspector + { + private static readonly Dictionary DisplayNames = new Dictionary() + { + { "ch", "上膛弹药" }, + { "cz", "Nabité střelivo" }, + { "en", "Loaded ammo" }, + { "es", "Munición cargada" }, + { "es-mx", "Munición cargada" }, + { "fr", "Munitions chargées" }, + { "ge", "Geladene Munition" }, + { "hu", "Töltött lőszer" }, + { "it", "Munizioni caricate" }, + { "jp", "装填弾薬" }, + { "kr", "장전 탄약" }, + { "pl", "Załadowana amunicja" }, + { "po", "Munição carregada" }, + { "ru", "Заряженные боеприпасы" }, + { "sk", "Nabitá munícia" }, + { "tu", "Yüklü mühimmat" } + }; + + private static ISession Session => ClientAppUtils.GetMainApp().GetClientBackEndSession(); + + private static Profile ActiveProfile => InGameStatus.InRaid ? ClientPlayerOwner.MyPlayer.Profile : Session.Profile; + + public static void AddAmmoCountAttribute(this MagazineClass magazine) + { + ItemAttributeClass attribute = magazine.Attributes.Find(attr => attr.Id is EItemAttributeId.MaxCount); + if (attribute == null) + { + return; + } + attribute.DisplayNameFunc = GetDisplayName; + attribute.Base = () => GetBase(magazine); + attribute.StringValue = () => GetStringValue(magazine); + attribute.FullStringValue = () => GetFullStringValue(magazine); + } + + private static int? GetAmmoCount(MagazineClass magazine, Profile profile, out bool magChecked) + { + if (!InGameStatus.InRaid || magazine.Count == 0) + { + magChecked = true; + return magazine.Count; + } + magChecked = profile.CheckedMagazines.ContainsKey(magazine.Id); + if (magChecked) + { + bool equipped = profile.Inventory.Equipment.GetAllSlots().Any(slot => ReferenceEquals(slot.ContainedItem, magazine)); + if (magazine.Count >= (equipped ? magazine.MaxCount - 1 : magazine.MaxCount)) + { + return magazine.Count; + } + int skill = Mathf.Max( + profile.MagDrillsMastering, + profile.CheckedMagazineSkillLevel(magazine.Id), + magazine.CheckOverride + ); + if (skill > 1 || (skill == 1 && magazine.MaxCount <= 10)) + { + return magazine.Count; + } + } + return null; + } + + public static string GetDisplayName() + { + string language = Singleton.Instance?.Game?.Settings?.Language?.GetValue(); + if (language == null || !DisplayNames.ContainsKey(language)) + { + language = "en"; + } + return DisplayNames[language]; + } + + public static float GetBase(MagazineClass magazine) + { + if (GetAmmoCount(magazine, ActiveProfile, out _) is int ammoCount) + { + return ammoCount; + } + else + { + return 0f; + } + } + + public static string GetStringValue(MagazineClass magazine) + { + string value; + if (GetAmmoCount(magazine, ActiveProfile, out _) is int ammoCount) + { + value = ammoCount.ToString(); + } + else + { + value = "?"; + } + return $"{value}/{magazine.MaxCount}"; + } + + + public static string GetFullStringValue(MagazineClass magazine) + { + Profile profile = ActiveProfile; + int? ammoCount = GetAmmoCount(magazine, profile, out bool magChecked); + if (magChecked) + { + List cartridges = new List(magazine.Cartridges.Items); + string[] lines = new string[cartridges.Count]; + int i = cartridges.Count - 1; + foreach (Item cartridge in cartridges) + { + string count; + if (ammoCount != null) + { + count = cartridge.StackObjectsCount.ToString(); + } + else + { + count = "?"; + } + string name; + if (profile.Examined(cartridge)) + { + name = cartridge.LocalizedName(); + } + else + { + name = "Unknown item".Localized(); + } + lines[i--] = $"{count} × {name}"; + } + return string.Join(Environment.NewLine, lines); + } + else + { + return string.Empty; + } + } + } +} \ No newline at end of file diff --git a/MagazineInspector/MagazineInspector.csproj b/MagazineInspector/MagazineInspector.csproj new file mode 100644 index 0000000..3efa759 --- /dev/null +++ b/MagazineInspector/MagazineInspector.csproj @@ -0,0 +1,34 @@ + + + + net472 + IcyClawz.MagazineInspector + 1.0.1 + IcyClawz.MagazineInspector + + + + + ..\Shared\Aki.Common.dll + + + ..\Shared\Aki.Reflection.dll + + + ..\Shared\Assembly-CSharp.dll + + + ..\Shared\BepInEx.dll + + + ..\Shared\Comfort.dll + + + ..\Shared\UnityEngine.dll + + + ..\Shared\UnityEngine.CoreModule.dll + + + + diff --git a/MagazineInspector/Plugin.cs b/MagazineInspector/Plugin.cs new file mode 100644 index 0000000..60c775a --- /dev/null +++ b/MagazineInspector/Plugin.cs @@ -0,0 +1,27 @@ +using Aki.Reflection.Patching; +using BepInEx; +using System.Reflection; + +namespace IcyClawz.MagazineInspector +{ + [BepInPlugin("com.IcyClawz.MagazineInspector", "IcyClawz.MagazineInspector", "1.0.1")] + public class Plugin : BaseUnityPlugin + { + private void Awake() + { + new MagazinePatch().Enable(); + } + } + + public class MagazinePatch : ModulePatch + { + protected override MethodBase GetTargetMethod() => + typeof(MagazineClass).GetConstructors()[0]; + + [PatchPostfix] + private static void PatchPostfix(ref MagazineClass __instance) + { + __instance.AddAmmoCountAttribute(); + } + } +} diff --git a/MunitionsExpert.sln b/MunitionsExpert.sln new file mode 100644 index 0000000..ae6039c --- /dev/null +++ b/MunitionsExpert.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32616.157 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{12CC1CB2-4C8C-40A8-834F-237AB5B50B90}") = "MunitionsExpert", "MunitionsExpert\MunitionsExpert.csproj", "{F1E2DE6A-7145-4ED7-8AB0-DE2FE9E33CCA}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F1E2DE6A-7145-4ED7-8AB0-DE2FE9E33CCA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F1E2DE6A-7145-4ED7-8AB0-DE2FE9E33CCA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F1E2DE6A-7145-4ED7-8AB0-DE2FE9E33CCA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F1E2DE6A-7145-4ED7-8AB0-DE2FE9E33CCA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {BA86C600-4494-49AD-A227-017B8142B09C} + EndGlobalSection +EndGlobal diff --git a/MunitionsExpert/MunitionsExpert.cs b/MunitionsExpert/MunitionsExpert.cs new file mode 100644 index 0000000..850ced0 --- /dev/null +++ b/MunitionsExpert/MunitionsExpert.cs @@ -0,0 +1,195 @@ +using Comfort.Common; +using EFT.HandBook; +using EFT.InventoryLogic; +using EFT.UI.DragAndDrop; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using UnityEngine; +using UnityEngine.UI; + +namespace IcyClawz.MunitionsExpert +{ + internal enum EAmmoExtraAttributeId + { + Damage, + PenetrationPower, + ArmorDamage, + FragmentationChance, + RicochetChance + } + + internal static class ImageExtensions + { + public static Sprite ToSprite(this System.Drawing.Image instance) + { + byte[] data; + using (MemoryStream ms = new MemoryStream()) + { + instance.Save(ms, System.Drawing.Imaging.ImageFormat.Png); + data = ms.ToArray(); + } + Texture2D texture = new Texture2D(instance.Width, instance.Height); + ImageConversion.LoadImage(texture, data); + return Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), Vector2.zero); + } + } + + internal static class IconCache + { + private static readonly Dictionary Cache = new Dictionary(5); + + public static void Initialize() + { + Cache.Add(EAmmoExtraAttributeId.Damage, Properties.Resources.Damage.ToSprite()); + Cache.Add(EAmmoExtraAttributeId.PenetrationPower, Properties.Resources.PenetrationPower.ToSprite()); + Cache.Add(EAmmoExtraAttributeId.ArmorDamage, Properties.Resources.ArmorDamage.ToSprite()); + Cache.Add(EAmmoExtraAttributeId.FragmentationChance, Properties.Resources.FragmentationChance.ToSprite()); + Cache.Add(EAmmoExtraAttributeId.RicochetChance, Properties.Resources.RicochetChance.ToSprite()); + } + + public static Sprite Get(Enum id) => + id is EAmmoExtraAttributeId extraId ? Cache[extraId] : null; + } + + internal static class AmmoTemplateExtensions + { + private static readonly FieldInfo CachedQualitiesField = + typeof(AmmoTemplate).GetField("_cachedQualities", BindingFlags.NonPublic | BindingFlags.Instance); + + public static List GetCachedQualities(this AmmoTemplate instance) => + CachedQualitiesField.GetValue(instance) as List; + + public static void AddExtraAttributes(this AmmoTemplate instance) + { + instance.SafelyAddQualityToList(new ItemAttributeClass(EAmmoExtraAttributeId.Damage) + { + Name = EAmmoExtraAttributeId.Damage.ToString(), + DisplayNameFunc = () => "Damage", + Base = () => instance.Damage * instance.ProjectileCount, + StringValue = () => + { + int totalDamage = instance.Damage * instance.ProjectileCount; + if (instance.ProjectileCount > 1) + { + return $"{instance.ProjectileCount} {instance.Damage} = {totalDamage}"; + } + else + { + return totalDamage.ToString(); + } + }, + DisplayType = () => EItemAttributeDisplayType.Compact + }); + + instance.SafelyAddQualityToList(new ItemAttributeClass(EAmmoExtraAttributeId.PenetrationPower) + { + Name = EAmmoExtraAttributeId.PenetrationPower.ToString(), + DisplayNameFunc = () => "Penetration power", + Base = () => instance.PenetrationPower, + StringValue = () => + { + int armorClass = instance.GetPenetrationArmorClass(); + string armorClassStr = armorClass > 0 ? $"Class {armorClass}" : "Unarmored"; + return $"{armorClassStr} ({instance.PenetrationPower})"; + }, + DisplayType = () => EItemAttributeDisplayType.Compact + }); + + instance.SafelyAddQualityToList(new ItemAttributeClass(EAmmoExtraAttributeId.ArmorDamage) + { + Name = EAmmoExtraAttributeId.ArmorDamage.ToString(), + DisplayNameFunc = () => "Armor damage", + Base = () => instance.ArmorDamage, + StringValue = () => $"{instance.ArmorDamage}%", + DisplayType = () => EItemAttributeDisplayType.Compact + }); + + instance.SafelyAddQualityToList(new ItemAttributeClass(EAmmoExtraAttributeId.FragmentationChance) + { + Name = EAmmoExtraAttributeId.FragmentationChance.ToString(), + DisplayNameFunc = () => "Fragmentation chance", + Base = () => instance.FragmentationChance, + StringValue = () => $"{Math.Round(instance.FragmentationChance * 100, 1)}%", + DisplayType = () => EItemAttributeDisplayType.Compact + }); + + instance.SafelyAddQualityToList(new ItemAttributeClass(EAmmoExtraAttributeId.RicochetChance) + { + Name = EAmmoExtraAttributeId.RicochetChance.ToString(), + DisplayNameFunc = () => "Ricochet chance", + Base = () => instance.RicochetChance, + StringValue = () => $"{Math.Round(instance.RicochetChance * 100, 1)}%", + DisplayType = () => EItemAttributeDisplayType.Compact + }); + } + + public static int GetPenetrationArmorClass(this AmmoTemplate instance) + { + var armorClasses = Singleton.Instance.Armor.ArmorClass; + for (int i = armorClasses.Length - 1; i >= 0; i--) + { + if (armorClasses[i].Resistance <= instance.PenetrationPower) + { + return i; + } + } + return 0; + } + } + + internal static class ColorUtils + { + private const byte ALPHA = 38; + private static readonly Color[] ArmorClassColors = new Color[] + { + new Color32(120, 40, 135, ALPHA), // Unarmored => Violet + new Color32(0, 60, 170, ALPHA), // Class 1 => Blue + new Color32(0, 150, 150, ALPHA), // Class 2 => Cyan + new Color32(70, 140, 0, ALPHA), // Class 3 => Green + new Color32(170, 170, 0, ALPHA), // Class 4 => Yellow + new Color32(140, 70, 0, ALPHA), // Class 5 => Orange + new Color32(170, 20, 0, ALPHA) // Class 6+ => Red + }; + + public static Color GetArmorClassColor(int armorClass) => + ArmorClassColors[Mathf.Clamp(armorClass, 0, ArmorClassColors.Length - 1)]; + } + + internal static class ItemViewExtensions + { + private static readonly FieldInfo BackgroundColorField = + typeof(ItemView).GetField("BackgroundColor", BindingFlags.NonPublic | BindingFlags.Instance); + + private static void SetBackgroundColor(this ItemView instance, Color color) => + BackgroundColorField.SetValue(instance, color); + + public static void OverrideColor(this ItemView instance, Item item) + { + if (item is BulletClass bullet && bullet.PenetrationPower > 0) + { + int armorClass = bullet.AmmoTemplate.GetPenetrationArmorClass(); + instance.SetBackgroundColor(ColorUtils.GetArmorClassColor(armorClass)); + } + } + } + + internal static class EntityIconExtensions + { + private static readonly FieldInfo ColorPanelField = + typeof(EntityIcon).GetField("_colorPanel", BindingFlags.NonPublic | BindingFlags.Instance); + + private static Image GetColorPanel(this EntityIcon instance) => + ColorPanelField.GetValue(instance) as Image; + + public static void OverrideColor(this EntityIcon instance, Item item) + { + if (item is BulletClass bullet && bullet.PenetrationPower > 0) + { + int armorClass = bullet.AmmoTemplate.GetPenetrationArmorClass(); + instance.GetColorPanel().color = ColorUtils.GetArmorClassColor(armorClass); + } + } + } +} \ No newline at end of file diff --git a/MunitionsExpert/MunitionsExpert.csproj b/MunitionsExpert/MunitionsExpert.csproj new file mode 100644 index 0000000..15f62f5 --- /dev/null +++ b/MunitionsExpert/MunitionsExpert.csproj @@ -0,0 +1,55 @@ + + + + net472 + IcyClawz.MunitionsExpert + 1.0.0 + IcyClawz.MunitionsExpert + + + + + ..\Shared\Aki.Reflection.dll + + + ..\Shared\Assembly-CSharp.dll + + + ..\Shared\BepInEx.dll + + + ..\Shared\Comfort.dll + + + ..\Shared\Sirenix.Serialization.dll + + + ..\Shared\UnityEngine.dll + + + ..\Shared\UnityEngine.CoreModule.dll + + + ..\Shared\UnityEngine.ImageConversionModule.dll + + + ..\Shared\UnityEngine.UI.dll + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + diff --git a/MunitionsExpert/Plugin.cs b/MunitionsExpert/Plugin.cs new file mode 100644 index 0000000..958b3e3 --- /dev/null +++ b/MunitionsExpert/Plugin.cs @@ -0,0 +1,96 @@ +using Aki.Reflection.Patching; +using BepInEx; +using EFT.HandBook; +using EFT.InventoryLogic; +using EFT.UI; +using EFT.UI.DragAndDrop; +using System; +using System.Collections.Generic; +using System.Reflection; +using UnityEngine; + +namespace IcyClawz.MunitionsExpert +{ + [BepInPlugin("com.IcyClawz.MunitionsExpert", "IcyClawz.MunitionsExpert", "1.0.0")] + public class Plugin : BaseUnityPlugin + { + private void Awake() + { + IconCache.Initialize(); + new StaticIconsPatch().Enable(); + new AmmoTemplatePatch().Enable(); + new ItemViewPatch().Enable(); + new EntityIconPatch().Enable(); + } + } + + internal class StaticIconsPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() => + typeof(StaticIcons).GetMethod("GetAttributeIcon", BindingFlags.Public | BindingFlags.Instance); + + [PatchPrefix] + private static bool PatchPrefix(ref Sprite __result, Enum id) + { + var icon = IconCache.Get(id); + if (icon != null) + { + __result = icon; + return false; + } + return true; + } + } + + internal class AmmoTemplatePatch : ModulePatch + { + protected override MethodBase GetTargetMethod() => + typeof(AmmoTemplate).GetMethod("GetCachedReadonlyQualities", BindingFlags.Public | BindingFlags.Instance); + + [PatchPrefix] + private static bool PatchPrefix(ref List __result, AmmoTemplate __instance) + { + if (__instance.GetCachedQualities() != null) + { + __result = null; + return false; + } + return true; + } + + [PatchPostfix] + private static void PatchPostfix(ref List __result, AmmoTemplate __instance) + { + if (__result == null) + { + __result = __instance.GetCachedQualities(); + return; + } + __instance.AddExtraAttributes(); + } + } + + internal class ItemViewPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() => + typeof(ItemView).GetMethod("UpdateColor", BindingFlags.NonPublic | BindingFlags.Instance); + + [PatchPrefix] + private static void PatchPrefix(ref ItemView __instance) + { + __instance.OverrideColor(__instance.Item); + } + } + + internal class EntityIconPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() => + typeof(EntityIcon).GetMethod("Show", BindingFlags.Public | BindingFlags.Instance); + + [PatchPostfix] + private static void PatchPostfix(ref EntityIcon __instance, Item item) + { + __instance.OverrideColor(item); + } + } +} diff --git a/MunitionsExpert/Properties/Resources.Designer.cs b/MunitionsExpert/Properties/Resources.Designer.cs new file mode 100644 index 0000000..01b5d04 --- /dev/null +++ b/MunitionsExpert/Properties/Resources.Designer.cs @@ -0,0 +1,113 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace IcyClawz.MunitionsExpert.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("IcyClawz.MunitionsExpert.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap ArmorDamage { + get { + object obj = ResourceManager.GetObject("ArmorDamage", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap Damage { + get { + object obj = ResourceManager.GetObject("Damage", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap FragmentationChance { + get { + object obj = ResourceManager.GetObject("FragmentationChance", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap PenetrationPower { + get { + object obj = ResourceManager.GetObject("PenetrationPower", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap RicochetChance { + get { + object obj = ResourceManager.GetObject("RicochetChance", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + } +} diff --git a/MunitionsExpert/Properties/Resources.resx b/MunitionsExpert/Properties/Resources.resx new file mode 100644 index 0000000..4286fde --- /dev/null +++ b/MunitionsExpert/Properties/Resources.resx @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\Resources\ArmorDamage.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\Damage.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\FragmentationChance.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\PenetrationPower.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\RicochetChance.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/MunitionsExpert/Resources/ArmorDamage.png b/MunitionsExpert/Resources/ArmorDamage.png new file mode 100644 index 0000000000000000000000000000000000000000..5a05fbcfb453dbd6afbc0d10be188de7c330cb0d GIT binary patch literal 649 zcmV;40(Sk0P)Ho_gsiUxI)jQVOt$LI?;53W%b|G7M0W zTr!Lx2uRw=SS@T#OV@4J*`3{;zMpCCu+)Li+&gFPIrp6Jy%RIWSP~)e5PXg1htxvq zA-)Z67OO-{IPy*+tSXj;-Jl{$!VTv&J~Rn}H;8Rwdk6tDf&h|`c&At^sEFi2MO2a$ ztzyUU*t;j5S55eS)v5aRGp9RG{OBJUWzCU!Ot33ILca@XkK;0O8VMTsh? zhDtWIwzcinI4d5Dh4+~()tnMN;;^6^6RY`KG!R34BqASI;|RwNL8%+XL9t5&0hn8^ zux5oI_U}U$2<&+k)Mmk1u}Az855zsOBm(owF6$vMk`RbUl~SQt(8x#1eJf~>?~eE` zOx^Qp&Zmi=np$ClBuvVp=uA43IAtWFm0rOb#9t>&Haj(4C=?bo<%o&(Szzx$e(2@E zh{i_SFY;oa*oDD+o?nReq&gF8mcW&M2*Bq_b zIBJI^w8M@wS1(=cTg>OJh=_b&tXVF(m76_R?s~5KRqu`M1Uq5&zwqbh=G-rzo#~^g zVkYH#a@Bn+>z*rqlt{U{f{^a{v~?>5s`Mm3K~_>m;|$l*lDGpI((j>d_af)+W6P- j0ipv&5SrPs!HoF>)~nJ5V}2f}00000NkvXXu0mjfOZYV1 literal 0 HcmV?d00001 diff --git a/MunitionsExpert/Resources/Damage.png b/MunitionsExpert/Resources/Damage.png new file mode 100644 index 0000000000000000000000000000000000000000..ba8012acbf2de37c8fa4b39ae71f0271e1376bf3 GIT binary patch literal 764 zcmVe;j5D)-<0ZQ0h^?yPr!UL;-L?9km2EGG+64C=fCae|G)60xFAQ=!Vt50uj zxqkJ@g4_KG`aW6+`3WqcI1|#5edZv6ep^6fM@p(_29fvV1mXAXFSp!mzBWwTUDgVr z15pr!^*X)Y>|I)l-H?&FuBc>}qqh1)7mTH#xM;NsCu5yeveKa_L+c6asIZ}>P9M69|IPmb&1?N+!N#y8HMg~9t%z!mL zBWsh#vk+ZYeo#%%%t~v!(ewhtt~quWnhTOOt9D2l3t-&k&DdTMcg-t9kpv@(5e&K0_UuX_TVvP0suLbyk~ zwUS@*Nk(I0f_6f3s+dqvRBU@WGVw*Q|?z_C*rHT<~Ij!LW}VI&cx5Yk(M9M532*{44eU$&!VQFZ`fY@F~z3`*cVI ubD~BP;%Eh#Ci$q&K4w9x2f45#*S6ng9TtukTy5n500003$2bE`rTJ#r+|6wx>q<{^G1%X%u%Ki$( zZ=f_to(G88q3S;a@i!p;iEbFOLGtVz9E#<&4YqTpP1*76>68CZ13<171F@NzS$_Tf z`{xOe{TtmdMyS{LfLIcU73*6&WB&jD{|{&o7t{b@5H_>0(loQOmRvk<&i;=d-oL~a zW=ufL4K+wPA|~F=(#F<0D=BUf)DVz5_N;_~!L%FRov`0t#GF8Cf~miuxueV^eeO>67}GY*@4E6j1y*5PyVv6Votp z2}z~Kj_znaetw=+%NE~bV`t|N3<=kWPf0hCl$I7=IA`X1psV%))jkB`S3vw77L&+^ zfoRs;;xZcrB_*kc5AMG>v~SOCV>5GMph3nUgSKzpcw$0V`vIW34-AlG0*Wjys8>O* z`v(g%kiWmIT)Jr2u5DYce)#a7@z<|kZ%-XR&Xb##_|DwQMsDMpmDi!J03{76Xk>ka zCYw*77-fMf06F*7#4)z@+jD8b0r#_$@R-{=i}r7x)cTc<=3- zH}8O$4J!5x>YDG!F-gdQX2z&LR^6~RR9k!FE`4J~am3oCXG4xpfikDr=`rnauWo}rP0i>s@rx4XBmyO+Ht*PR==h#vXLcVwwd=^qy~odK8pSS z&p)|uu}#SS<8ysCMfx%|bTC9Q3q58_Z|QZpBerit>91c6U)?UN@Ws4NcG<+X`R>v) zhZZJpTf(+vcj7K9dGVJvpLPW?r?KDMbR)0ptD=0h+STHiBTwJPeO}>L`DBZu^I`vW x=gqIyU3z~&fBG)(y(^vtUDme$m+SQQHS2~<5gxIKY*|p;c)I$ztaD0e0stGWq89)F literal 0 HcmV?d00001 diff --git a/MunitionsExpert/Resources/RicochetChance.png b/MunitionsExpert/Resources/RicochetChance.png new file mode 100644 index 0000000000000000000000000000000000000000..5e0ec30832e73c9a1c06d182267191a245e5fce0 GIT binary patch literal 681 zcmV;a0#^NrP)M9@z ziD*cPU{YbB2oVs1)2kI5>4vk-t!+Ke*clrb-SohZ_ni0q|Ic~f|A+8DB+JD@F(*M~ za*5*qD2+=)XC59I+fILB0U#K{7C2x^}~|cb~aFlTHgt zl`0vDgulAZ+I*gY+g+B%=H~G?qfailJNhto8L*;!C5(EotHNZmnfD*4+gVvzqc<3; zwOZXFj2IMP|Ohix$wnd{+eIy)CDV0ibY}EI`-gxpE@>^oD7{3c^ z@i<%I_`D<&y2^oAB9qC&%S)?Iy^kka9Brq&FJ3u;yfK+baI%n2r#VNOmzCtmm0kl& zp3sSrczm4{|iwt7|LP@aZqGLv6W4r3LFO2U_0Qw*#l02&U5XZw-65k-mkyj9Uf%C|g~@f9 P00000NkvXXu0mjfrer*z literal 0 HcmV?d00001