diff --git a/MasterList.json b/MasterList.json index e9417497..1fccf75d 100644 --- a/MasterList.json +++ b/MasterList.json @@ -270,7 +270,7 @@ { "Name": "Container Quick Loot", "Author": "CactusPie", - "SupportedVersion": "1.9.8767.26420", + "SupportedVersion": "1.9.8766.40295", "ModVersion": "1.3.1", "PortVersion": "1.0.0", "Description": "Looting an item will now place it in a matching container with a @loot tag, such as a dogtag case or a document case", @@ -280,5 +280,19 @@ "CactusPie.ContainerQuickLoot.dll" ], "ConfigFiles": [] + }, + { + "Name": "Modding Stats Helper" + "Author": "wara", + "SupportedVersion": "1.9.8767.26420", + "ModVersion": "1.0.2", + "PortVersion": "1.0.0", + "Description": "Adds a tooltip when hovering parts in the weapon building screen that shows a quick overview of relevant stats and comparisons.", + "ModUrl": "https://hub.sp-tarkov.com/files/file/1442-modding-stats-helper/", + "RequiresFiles": false, + "PluginFiles": [ + "Wara-ModdingStatsHelper.dll" + ], + "ConfigFiles": [] } -] \ No newline at end of file +] diff --git a/ModdingStatsHelper-SIT/Globals.cs b/ModdingStatsHelper-SIT/Globals.cs new file mode 100644 index 00000000..370b3cba --- /dev/null +++ b/ModdingStatsHelper-SIT/Globals.cs @@ -0,0 +1,62 @@ +using EFT.InventoryLogic; +using EFT.UI; +using System.Collections.Generic; + +namespace ShowMeTheStats +{ + public static class Globals + { + public static bool isWeaponModding = false; + + public static Item mod = null; + + public static List allSlots = new List(); + + public static SimpleTooltip simpleTooltip = null; + + //public static Slot slotType = null; + + public static Item dropDownCurrentItem = null; + + public static bool isKeyPressed = false; + + //some stats are not very interesting to see and will clog up the ui more than anything, so we blacklist them + public static string[] statBlacklist = { + EItemAttributeId.CompatibleWith.ToString(), + EItemAttributeId.Weight.ToString(), + EItemAttributeId.Size.ToString(), + //EItemAttributeId.Caliber.ToString(), + //EItemAttributeId.BulletSpeed.ToString(), + EItemAttributeId.RaidModdable.ToString(), + EItemAttributeId.OpticCrate.ToString(), + //EItemAttributeId.EffectiveDist.ToString(), + //EItemAttributeId.EffectiveDistance.ToString(), + //EItemAttributeId.Velocity.ToString(), + EItemAttributeId.SightingRange.ToString(), + //EItemAttributeId.AmmoCaliber.ToString(), + EItemAttributeId.SingleFireRate.ToString(), + EItemAttributeId.FireRate.ToString(), + EItemAttributeId.DurabilityBurn.ToString(), + EItemAttributeId.HeatFactor.ToString(), + EItemAttributeId.CoolFactor.ToString(), + + EItemAttributeId.MalfFeedChance.ToString(), + EItemAttributeId.MalfMisfireChance.ToString(), + EItemAttributeId.LoadUnloadSpeed.ToString(), + EItemAttributeId.CheckTimeSpeed.ToString(), + "AutoROF", + "SemiROF", + }; + + public static void ClearAllGlobals() + { + isWeaponModding = false; + mod = null; + allSlots.Clear(); + simpleTooltip = null; + //slotType = null; + dropDownCurrentItem = null; + isKeyPressed = false; + } + } +} diff --git a/ModdingStatsHelper-SIT/LICENSE b/ModdingStatsHelper-SIT/LICENSE new file mode 100644 index 00000000..5bb0f40d --- /dev/null +++ b/ModdingStatsHelper-SIT/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 warawanaidene + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ModdingStatsHelper-SIT/Plugin.cs b/ModdingStatsHelper-SIT/Plugin.cs new file mode 100644 index 00000000..37c6f5cd --- /dev/null +++ b/ModdingStatsHelper-SIT/Plugin.cs @@ -0,0 +1,44 @@ +using BepInEx; +using ShowMeTheStats; +using UnityEngine; + +namespace Plugin +{ + [BepInPlugin("com.moddingstatshelper.aki", "Modding Stats Helper", "1.0")] + public class Plugin : BaseUnityPlugin + { + void Awake() + { + new ItemShowTooltipPatch().Enable(); + new ShowTooltipPatch().Enable(); + new WeaponUpdatePatch().Enable(); + new DropDownSlotContextPatch().Enable(); + new SlotViewPatch().Enable(); + new ScreenTypePatch().Enable(); + + new DropDownSlotContextClosePatch().Enable(); + + } + + + + void Update() + { + if (Globals.isWeaponModding) + { + bool isKeyDown = Input.GetKey(KeyCode.LeftControl); + + if (isKeyDown && !Globals.isKeyPressed) + { + Globals.isKeyPressed = true; + Globals.simpleTooltip.Show("", null, 0.1f, null); + } + else if (!isKeyDown && Globals.isKeyPressed) + { + Globals.isKeyPressed = false; + Globals.simpleTooltip.Show("", null, 0.1f, null); + } + } + } + } +} diff --git a/ModdingStatsHelper-SIT/ShowMeTheStats.csproj b/ModdingStatsHelper-SIT/ShowMeTheStats.csproj new file mode 100644 index 00000000..ad16656a --- /dev/null +++ b/ModdingStatsHelper-SIT/ShowMeTheStats.csproj @@ -0,0 +1,61 @@ + + + + net472 + Wara-ModdingStatsHelper + + + + False + + + + + dependencies\Aki.Common.dll + + + dependencies\Aki.Reflection.dll + + + dependencies\Assembly-CSharp.dll + + + dependencies\BepInEx.dll + + + dependencies\BepInEx.Harmony.dll + + + dependencies\Comfort.dll + + + dependencies\Newtonsoft.Json.dll + + + dependencies\Sirenix.Serialization.dll + + + + dependencies\Unity.TextMeshPro.dll + + + dependencies\UnityEngine.dll + + + dependencies\UnityEngine.CoreModule.dll + + + dependencies\UnityEngine.AudioModule.dll + + + dependencies\UnityEngine.InputModule.dll + + + dependencies\UnityEngine.InputLegacyModule.dll + + + dependencies\System.Runtime.dll + + + + \ No newline at end of file diff --git a/ModdingStatsHelper-SIT/ShowMeTheStats.sln b/ModdingStatsHelper-SIT/ShowMeTheStats.sln new file mode 100644 index 00000000..f219c7b4 --- /dev/null +++ b/ModdingStatsHelper-SIT/ShowMeTheStats.sln @@ -0,0 +1,30 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33723.286 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ShowMeTheStats", "ShowMeTheStats.csproj", "{06EE05FF-852D-47D1-882B-E8D3044A382E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4CC091D9-5745-4098-AC8B-1CB99EE9A1DA}" + ProjectSection(SolutionItems) = preProject + Complete Gameplay Overhaul.sln = Complete Gameplay Overhaul.sln + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {06EE05FF-852D-47D1-882B-E8D3044A382E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {06EE05FF-852D-47D1-882B-E8D3044A382E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {06EE05FF-852D-47D1-882B-E8D3044A382E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {06EE05FF-852D-47D1-882B-E8D3044A382E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {DF3EE3DB-0374-47BD-BC99-2ED85C439AC8} + EndGlobalSection +EndGlobal diff --git a/ModdingStatsHelper-SIT/ShowMeTheStatsPatches.cs b/ModdingStatsHelper-SIT/ShowMeTheStatsPatches.cs new file mode 100644 index 00000000..dc74a248 --- /dev/null +++ b/ModdingStatsHelper-SIT/ShowMeTheStatsPatches.cs @@ -0,0 +1,302 @@ +using Aki.Reflection.Patching; +using EFT.InventoryLogic; +using System.Reflection; +using EFT.UI; +using EFT.UI.Screens; +using System.Collections.Generic; +using static ShowMeTheStats.Utils; +using EFT.UI.DragAndDrop; +using System; +using System.Linq; +using EFT.UI.WeaponModding; + +namespace ShowMeTheStats +{ + public class ItemShowTooltipPatch : ModulePatch + { + // we set the item we are hovering in the globals + protected override MethodBase GetTargetMethod() + { + return typeof(GridItemView).GetMethod("ShowTooltip", BindingFlags.Instance | BindingFlags.NonPublic); + } + + [PatchPrefix] + static void Prefix(GridItemView __instance) + { + if (Globals.isWeaponModding) + { + Globals.mod = __instance.Item; + } + } + } + + + public class ShowTooltipPatch : ModulePatch + { + // the spaghetti starts here. + + protected override MethodBase GetTargetMethod() + { + return typeof(SimpleTooltip).GetMethod("Show", BindingFlags.Instance | BindingFlags.Public); + } + + [PatchPrefix] + static void Prefix(ref string text, ref float delay, SimpleTooltip __instance) + { + if (Globals.isWeaponModding) + { + // checks for a bug + if (text.Contains("EQUIPPED") || text.Contains("STASH")) + { + return; + } + if (Globals.mod.Attributes != null) + { + delay = 0.1f; + + Globals.simpleTooltip = __instance; + + string firstString = ""; + string finalString = ""; + bool isSameStats = true; + + bool hoveringSlottedMod = Globals.allSlots.Any(a => a.ContainedItem == Globals.mod); + + if (Globals.isKeyPressed && !hoveringSlottedMod) + { + firstString = "STATS → COMPARISON (CTRL)
"; + } + else if (!hoveringSlottedMod && Globals.dropDownCurrentItem != null) + { + firstString = "COMPARISON → STATS (CTRL)
"; + } + + // IF WE ARE NOT COMPARING + if (hoveringSlottedMod || Globals.isKeyPressed || Globals.dropDownCurrentItem == null) + { + List attributes = GetAllAttributesNotInBlacklist(Globals.mod.Attributes); + + foreach (var attribute in attributes) + { + if (attribute.Base() != 0) + { + string stringColor = "#ffffff"; + string stringValue = attribute.StringValue(); + + string stringDisplayname = AlignTextToWidth(attribute.DisplayName.Trim() + ":"); + + stringValue = AddOperatorToStringValue(attribute.StringValue(), attribute.Base(), false); + stringColor = GetValueColor(attribute.Base(), attribute.LessIsGood, attribute.LabelVariations, false); + if (!stringValue.Contains("MOA")) // MOA is annoying to deal with + { + string attributeLine = $"{stringDisplayname}{stringValue}
"; + + + finalString += attributeLine; + isSameStats = false; + } + } + } + + } + // IF WE ARE COMPARING + else if (!hoveringSlottedMod) + { + List replacingAttributes = GetAllAttributesNotInBlacklist(Globals.mod.Attributes); + List slottedAttributes = GetAllAttributesNotInBlacklist(Globals.dropDownCurrentItem.Attributes); + + List replacingAttributesDisplayed = new List(); + + foreach (var slottedAttribute in slottedAttributes) + { + if (slottedAttribute.Base() != 0) + { + //if (slottedAttribute.Id.ToString() == EItemAttributeId.MalfMisfireChance.ToString()) + //{ + // break; + //} + + string stringDisplayname = AlignTextToWidth(slottedAttribute.DisplayName.Trim() + ":"); + ItemAttribute replacingAttribute = replacingAttributes.Where(a => a.Id.ToString() == slottedAttribute.Id.ToString()).SingleOrDefault(); + + if (replacingAttribute != null && replacingAttribute.Base() != 0) + { + // check if there's a difference in comparison or same stats + float substractedBases = slottedAttribute.Base() - replacingAttribute.Base(); + bool isZero = Math.Abs(substractedBases) < float.Epsilon; + if (!isZero) + { + // we do the substract stuff here (this is the wrong way to do it. I should use Base(), but w/e.) + + string substractedAttributeStringValue = SubstractStringValue(slottedAttribute.StringValue(), replacingAttribute.StringValue()); + + substractedAttributeStringValue = SpaghettiLastStringValueOperatorCheck(substractedAttributeStringValue, substractedBases); + string stringColor = GetValueColor(substractedBases, slottedAttribute.LessIsGood, slottedAttribute.LabelVariations, true); + + if (!substractedAttributeStringValue.Contains("MOA")) // MOA is annoying to deal with + { + string attributeLine = $"{stringDisplayname}{substractedAttributeStringValue}
"; + + finalString += attributeLine; + isSameStats = false; + } + + } + } + else + { + string stringValue = AddOperatorToStringValue(slottedAttribute.StringValue(), slottedAttribute.Base(), false); + string stringColor = GetValueColor(slottedAttribute.Base(), slottedAttribute.LessIsGood, slottedAttribute.LabelVariations, true); + // should use reverse bool on AddOperatorToStringValue, but there was a bug IIRC, so I use this patchy method instead + stringValue = ReverseOperator(stringValue); + + if (!stringValue.Contains("MOA")) // MOA is annoying to deal with + { + string attributeLine = $"{stringDisplayname}{stringValue}
"; + + finalString += attributeLine; + isSameStats = false; + } + + } + } + replacingAttributesDisplayed.Add(slottedAttribute.Id.ToString()); + } + // for attributes that are not compared, just added or removed by changing the part. + foreach (var attribute in replacingAttributes) + { + if (!replacingAttributesDisplayed.Contains(attribute.Id.ToString())) + { + string stringDisplayname = AlignTextToWidth(attribute.DisplayName.Trim() + ":"); + string stringValue = AddOperatorToStringValue(attribute.StringValue(), attribute.Base(), false); + string stringColor = GetValueColor(attribute.Base(), attribute.LessIsGood, attribute.LabelVariations, false); + + if (!stringValue.Contains("MOA")) // MOA is annoying to deal with + { + string attributeLine = $"{stringDisplayname}{stringValue}
"; + + finalString += attributeLine; + isSameStats = false; + } + } + } + } + + + if (finalString != "" || firstString != "") + { + if (firstString == "") + { + firstString = "STATS
"; + } + if (isSameStats && firstString.Contains("COMPARISON") && !Globals.isKeyPressed) + { + finalString += "SAME STATS
"; + } + + text = firstString + finalString; + } + } + } + } + } + + public class WeaponUpdatePatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return typeof(EditBuildScreen).GetMethod("WeaponUpdate", BindingFlags.Instance | BindingFlags.NonPublic); + } + + [PatchPrefix] + static void Prefix() + { + Globals.allSlots.Clear(); + } + } + + public class DropDownSlotContextPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return typeof(DropDownMenu).GetMethod("Show", BindingFlags.Instance | BindingFlags.Public); + } + + [PatchPrefix] + static void Prefix(ModdingScreenSlotView slotView) + { + FieldInfo fieldInfo = typeof(ModdingScreenSlotView).GetField("slot_0", BindingFlags.Instance | BindingFlags.NonPublic); + if (fieldInfo != null) + { + Slot slot_0 = (Slot)fieldInfo.GetValue(slotView); + if (slot_0.ContainedItem != null) + { + Globals.dropDownCurrentItem = slot_0.ContainedItem; + //Globals.slotType = slot_0; + } + } + } + } + + public class DropDownSlotContextClosePatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return typeof(DropDownMenu).GetMethod("Close", BindingFlags.Instance | BindingFlags.Public); + } + + [PatchPrefix] + static void Prefix() + { + if (Globals.isWeaponModding) + { + Globals.dropDownCurrentItem = null; + } + } + } + + public class SlotViewPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return typeof(ModdingScreenSlotView).GetMethod("Show", BindingFlags.Instance | BindingFlags.Public); + } + + [PatchPrefix] + static void Prefix(Slot slot) + { + if (slot.ContainedItem != null) + { + Globals.allSlots.Add(slot); + } + } + } + + public class ScreenTypePatch : ModulePatch + { + + protected override MethodBase GetTargetMethod() + { + return typeof(MenuTaskBar).GetMethod("OnScreenChanged", BindingFlags.Instance | BindingFlags.Public); + } + + [PatchPrefix] + static void Prefix(EEftScreenType eftScreenType) + { + if (eftScreenType == EEftScreenType.EditBuild || eftScreenType == EEftScreenType.WeaponModding) + { + Globals.isWeaponModding = true; + return; + } + + if (Globals.isWeaponModding) + { + if (eftScreenType != EEftScreenType.EditBuild || eftScreenType != EEftScreenType.WeaponModding) + { + Globals.ClearAllGlobals(); + } + } + } + } + +} diff --git a/ModdingStatsHelper-SIT/Utils.cs b/ModdingStatsHelper-SIT/Utils.cs new file mode 100644 index 00000000..f01834dd --- /dev/null +++ b/ModdingStatsHelper-SIT/Utils.cs @@ -0,0 +1,204 @@ +using EFT.InventoryLogic; +using System.Collections.Generic; +using System.Linq; + +namespace ShowMeTheStats +{ + public static class Utils + { + + public static string AlignTextToWidth(string text) + { + int spaces = 20; + + int currentLength = text.Length; + + int newLength = spaces - currentLength; + + if (currentLength < spaces) + { + for (int i = 0; i < newLength; i++) + { + text += " "; + } + } + + return text; + } + + public static string[] getDigitsFromStringValues(string attributeStringValue) + { + string extractedDigits = ""; + string extractedOperator = ""; + string extractedType = ""; + + foreach (char c in attributeStringValue) + { + if (char.IsDigit(c) || c == '.') + { + extractedDigits += c.ToString(); + } + else if (c == '-' || c == '+') + { + extractedOperator = c.ToString(); + } + else if (c == '%' || c == 'M' || c == 'O' || c == 'A') // lazy ahh MOA extraction + { + extractedType += c.ToString(); + } + } + + if (extractedType == "MOA") + { + extractedType = " " + extractedType; + } + + string[] final = { extractedOperator, extractedDigits, extractedType }; + + return final; + } + + public static string SubstractStringValue(string slottedAttributeStringValue, string replacingAttributeStringValue) + { + //[0] = operator + //[1] = float + //[2] = "%" string + + string[] slottingAttributeExtracted = getDigitsFromStringValues(slottedAttributeStringValue); + string[] replacingAttributeExtracted = getDigitsFromStringValues(replacingAttributeStringValue); + + string subtracted = (float.Parse(replacingAttributeExtracted[0] + replacingAttributeExtracted[1]) - float.Parse(slottingAttributeExtracted[0] + slottingAttributeExtracted[1])).ToString(); //"F1" + + return subtracted + replacingAttributeExtracted[2]; + } + + public static string GetValueColor(float numBase, bool LessIsGood, EItemAttributeLabelVariations labelVariation, bool reversed) + { + string blueColor = "#54c1ff"; + string redColor = "#c40000"; + + string textColor = ""; + if (labelVariation == EItemAttributeLabelVariations.Colored) + { + if (numBase < 0f) + { + if (LessIsGood) + { + textColor = blueColor; + } + else if (!LessIsGood) + { + textColor = redColor; + } + } + else if (numBase > 0f) + { + if (LessIsGood) + { + textColor = redColor; + } + else if (!LessIsGood) + { + textColor = blueColor; + } + } + + if (reversed) + { + if (textColor == blueColor) + { + textColor = redColor; + } + else if (textColor == redColor) + { + textColor = blueColor; + } + } + } + else + { + textColor = "#ffffff"; // white + } + + return textColor; + } + + public static string ReverseOperator(string stringValue) + { + if (stringValue.Contains("-")) + { + stringValue = stringValue.Replace("-", "+"); + } + else if (stringValue.Contains("+")) + { + stringValue = stringValue.Replace("+", "-"); + } + + return stringValue; + } + + public static string SpaghettiLastStringValueOperatorCheck(string attributeStringValue, float attributeBase) + { + if (attributeStringValue.Trim().ToUpper().Contains("LOUDNESS") || attributeStringValue.Trim().ToUpper().Contains("MOA") || attributeStringValue.Trim().ToUpper().Contains("MAX COUNT")) + { + if (attributeStringValue.Contains("+")) + { + attributeStringValue = attributeStringValue.Replace("+", ""); + } + return attributeStringValue; + } + else if (!attributeStringValue.Contains("+") && !attributeStringValue.Contains("-")) + { + return "+" + attributeStringValue; + } + + + return attributeStringValue; + } + + public static string AddOperatorToStringValue(string attributeStringValue, float attributeBase, bool reversed) + { + if (attributeBase < 0f) + { + if (!attributeStringValue.Contains("-")) + { + if (!reversed) + { + attributeStringValue = "-" + attributeStringValue; + } + else if (reversed) + { + attributeStringValue = "+" + attributeStringValue; + } + } + } + else + { + if (!reversed) + { + attributeStringValue = "+" + attributeStringValue; + + } + else if (reversed) + { + attributeStringValue = "-" + attributeStringValue; + } + } + + return attributeStringValue; + } + + public static List GetAllAttributesNotInBlacklist(List attributes) + { + List attributesResult = new List(); + foreach (var attribute in attributes) + { + if (!Globals.statBlacklist.Any(x => x == attribute.Id.ToString())) + { + attributesResult.Add(attribute); + } + } + return attributesResult; + } + } +}