From 1809f1343dac3f3599912ccc0ecd21495fc40b84 Mon Sep 17 00:00:00 2001 From: Tyfon <29051038+tyfon7@users.noreply.github.com> Date: Thu, 2 May 2024 11:19:02 -0700 Subject: [PATCH] Color full panel bars, swap containers, fix black text, fix non-delta parens --- Patches/ContainerStackPatch.cs | 2 +- Patches/ItemPanelPatches.cs | 173 ++++++++++++++++++++------------- Patches/SwapPatch.cs | 82 ++++++++++++++-- Settings.cs | 24 +++++ UIFixes.Test/UnitTests.cs | 9 +- UIFixes.csproj | 5 +- 6 files changed, 214 insertions(+), 81 deletions(-) diff --git a/Patches/ContainerStackPatch.cs b/Patches/ContainerStackPatch.cs index 20f827a..ab98260 100644 --- a/Patches/ContainerStackPatch.cs +++ b/Patches/ContainerStackPatch.cs @@ -8,7 +8,7 @@ using System.Reflection; namespace UIFixes { - internal class ContainerStackPatch : ModulePatch + public class ContainerStackPatch : ModulePatch { private static Type MergeableItemType; diff --git a/Patches/ItemPanelPatches.cs b/Patches/ItemPanelPatches.cs index d217a46..97fdce9 100644 --- a/Patches/ItemPanelPatches.cs +++ b/Patches/ItemPanelPatches.cs @@ -21,18 +21,23 @@ namespace UIFixes private static FieldInfo ItemComponentItemField; + private static FieldInfo CompactCharacteristicPanelItemAttributeField; + public static void Enable() { AttributeCompactPanelDictionaryField = AccessTools.GetDeclaredFields(typeof(ItemSpecificationPanel)).First(f => typeof(IEnumerable>).IsAssignableFrom(f.FieldType)); AttributeCompactDropdownDictionaryField = AccessTools.GetDeclaredFields(typeof(ItemSpecificationPanel)).First(f => typeof(IEnumerable>).IsAssignableFrom(f.FieldType)); - Type ItemComponentType = PatchConstants.EftTypes.First(t => typeof(IItemComponent).IsAssignableFrom(t) && AccessTools.Field(t, "Item") != null); - ItemComponentItemField = AccessTools.Field(ItemComponentType, "Item"); + Type itemComponentType = PatchConstants.EftTypes.First(t => typeof(IItemComponent).IsAssignableFrom(t) && t.GetField("Item") != null); // GClass2754 + ItemComponentItemField = AccessTools.Field(itemComponentType, "Item"); + + CompactCharacteristicPanelItemAttributeField = AccessTools.Field(typeof(CompactCharacteristicPanel), "ItemAttribute"); new InjectButtonPatch().Enable(); new LoadModStatsPatch().Enable(); new CompareModPatch().Enable(); - new FormatValuesPatch().Enable(); + new FormatCompactValuesPatch().Enable(); + new FormatFullValuesPatch().Enable(); } private class LoadModStatsPatch : ModulePatch @@ -238,105 +243,133 @@ namespace UIFixes } } - private class FormatValuesPatch : ModulePatch + private class FormatCompactValuesPatch : ModulePatch { - // These fields are percents, but have been manually multipied by 100 already - private static EItemAttributeId[] NonPercentPercents = [EItemAttributeId.ChangeMovementSpeed, EItemAttributeId.ChangeTurningSpeed, EItemAttributeId.Ergonomics]; - - private static FieldInfo ItemAttributeField; - private static FieldInfo IncreasingColorField; - private static FieldInfo DecreasingColorField; - protected override MethodBase GetTargetMethod() { - ItemAttributeField = AccessTools.Field(typeof(CompactCharacteristicPanel), "ItemAttribute"); - IncreasingColorField = AccessTools.Field(typeof(CompactCharacteristicPanel), "_increasingColor"); - DecreasingColorField = AccessTools.Field(typeof(CompactCharacteristicPanel), "_decreasingColor"); - return AccessTools.Method(typeof(CompactCharacteristicPanel), "SetValues"); } [PatchPostfix] private static void Postfix(CompactCharacteristicPanel __instance, TextMeshProUGUI ___ValueText) { - // Comparisons are shown as () - // is from each attribute type's StringValue() function, so is formatted *mostly* ok - // is just naively formatted with ToString("F2"), so I have to figure out what it is and fix that - // This method is a gnarly pile of regex and replacements, blame BSG + try + { + FormatText(__instance, ___ValueText); + } + catch (Exception ex) + { + Logger.LogError(ex); + } + } + } - Color increasingColor = (Color)IncreasingColorField.GetValue(__instance); - string increasingColorHex = "#" + ColorUtility.ToHtmlStringRGB(increasingColor); + private class FormatFullValuesPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + return AccessTools.Method(typeof(CharacteristicPanel), "SetValues"); + } - Color decreasingColor = (Color)DecreasingColorField.GetValue(__instance); - string decreasingColorHex = "#" + ColorUtility.ToHtmlStringRGB(decreasingColor); + [PatchPostfix] + private static void Postfix(CharacteristicPanel __instance, TextMeshProUGUI ___ValueText) + { + try + { + FormatText(__instance, ___ValueText); + } + catch (Exception ex) + { + Logger.LogError(ex); + } + } + } - string text = ___ValueText.text; - ItemAttributeClass attribute = ItemAttributeField.GetValue(__instance) as ItemAttributeClass; + // These fields are percents, but have been manually multipied by 100 already + private static readonly EItemAttributeId[] NonPercentPercents = [EItemAttributeId.ChangeMovementSpeed, EItemAttributeId.ChangeTurningSpeed, EItemAttributeId.Ergonomics]; - // Some percents are formatted with ToString("P1"), which puts a space before the %. These are percents from 0-1, so the value need to be converted - var match = Regex.Match(text, @" %\((.*)\)"); + private static void FormatText(CompactCharacteristicPanel panel, TextMeshProUGUI textMesh) + { + // Comparisons are shown as () + // is from each attribute type's StringValue() function, so is formatted *mostly* ok + // is just naively formatted with ToString("F2"), so I have to figure out what it is and fix that + // This method is a gnarly pile of regex and replacements, blame BSG + if (!Settings.StyleItemPanel.Value) + { + return; + } + + // These come from CompactCharacteristicPanel._increasingColor and _decreasingColor, which are hardcoded. Hardcoding here too because + // CharacteristicPanel doesn't define and you get clear + const string IncreasingColorHex = "#5EC1FF"; + const string DecreasingColorHex = "#C40000"; + + string text = textMesh.text; + ItemAttributeClass attribute = CompactCharacteristicPanelItemAttributeField.GetValue(panel) as ItemAttributeClass; + + // Some percents are formatted with ToString("P1"), which puts a space before the %. These are percents from 0-1, so the value need to be converted + var match = Regex.Match(text, @" %\(([+-].*)\)"); + if (match.Success) + { + float value; + // If this fails to parse, I don't know what it is, leave it be + if (float.TryParse(match.Groups[1].Value, out value)) + { + string sign = value > 0 ? "+" : ""; + string color = (attribute.LessIsGood && value < 0) || (!attribute.LessIsGood && value > 0) ? IncreasingColorHex : DecreasingColorHex; + + // Except some that have a space weren't actually formatted with P1 and are 0-100 with a manually added " %" + if (NonPercentPercents.Contains((EItemAttributeId)attribute.Id)) + { + text = Regex.Replace(text, @"%\([+-].*\)", "%(" + sign + value + "%)"); + } + else + { + text = Regex.Replace(text, @"%\([+-].*\)", "%(" + sign + value.ToString("P1") + ")"); + } + } + } + else + { + // Others are rendered as num + "%", so there's no space before the %. These are percents but are from 0-100, not 0-1. + match = Regex.Match(text, @"(\S)%\(([+-].*)\)"); if (match.Success) { float value; // If this fails to parse, I don't know what it is, leave it be - if (float.TryParse(match.Groups[1].Value, out value)) + if (float.TryParse(match.Groups[2].Value, out value)) { string sign = value > 0 ? "+" : ""; - string color = (attribute.LessIsGood && value < 0) || (!attribute.LessIsGood && value > 0) ? increasingColorHex : decreasingColorHex; - - // Except some that have a space weren't actually formatted with P1 and are 0-100 with a manually added " %" - if (NonPercentPercents.Contains((EItemAttributeId)attribute.Id)) - { - text = Regex.Replace(text, @"%\(.*\)", "%(" + sign + value + "%)"); - } - else - { - text = Regex.Replace(text, @"%\(.*\)", "%(" + sign + value.ToString("P1") + ")"); - } + string color = (attribute.LessIsGood && value < 0) || (!attribute.LessIsGood && value > 0) ? IncreasingColorHex : DecreasingColorHex; + text = Regex.Replace(text, @"(\S)%\(([+-].*)\)", match.Groups[1].Value + "%(" + sign + value + "%)"); } } else { - // Others are rendered as num + "%", so there's no space before the %. These are percents but are from 0-100, not 0-1. - match = Regex.Match(text, @"(\S)%\((.*)\)"); + // Finally the ones that aren't percents + match = Regex.Match(text, @"\(([+-].*)\)"); if (match.Success) { float value; // If this fails to parse, I don't know what it is, leave it be - if (float.TryParse(match.Groups[2].Value, out value)) + if (float.TryParse(match.Groups[1].Value, out value)) { string sign = value > 0 ? "+" : ""; - string color = (attribute.LessIsGood && value < 0) || (!attribute.LessIsGood && value > 0) ? increasingColorHex : decreasingColorHex; - text = Regex.Replace(text, @"(\S)%\((.*)\)", match.Groups[1].Value + "%(" + sign + value + "%)"); - } - } - else - { - // Finally the ones that aren't percents - match = Regex.Match(text, @"\((.*)\)"); - if (match.Success) - { - float value; - // If this fails to parse, I don't know what it is, leave it be - if (float.TryParse(match.Groups[1].Value, out value)) - { - string sign = value > 0 ? "+" : ""; - string color = (attribute.LessIsGood && value < 0) || (!attribute.LessIsGood && value > 0) ? increasingColorHex : decreasingColorHex; - text = Regex.Replace(text, @"\((.*)\)", "(" + sign + value + ")"); - } + string color = (attribute.LessIsGood && value < 0) || (!attribute.LessIsGood && value > 0) ? IncreasingColorHex : DecreasingColorHex; + text = Regex.Replace(text, @"\(([+-].*)\)", "(" + sign + value + ")"); } } } - - // Remove trailing 0s - text = RemoveTrailingZeros(text); - - // Fix spacing - text = text.Replace(" %", "%"); - text = text.Replace("(", " ("); - - ___ValueText.text = text; } + + // Remove trailing 0s + text = RemoveTrailingZeros(text); + + // Fix spacing + text = text.Replace(" %", "%"); + text = text.Replace("(", " ("); + + textMesh.text = text; } private static List GetDeepAttributes(Item item, out bool changed) @@ -421,7 +454,7 @@ namespace UIFixes // b) a dot and some trailing 0 // And all that is replaced to the original integer, and the significantDigits (if they exist) // If neither matches this doesn't match and does nothing - return Regex.Replace(input, @"(?\d)((?\.[0-9]*[^0])0*\b)?(\.0+\b)?", "${integer}${significantDecimals}"); + return Regex.Replace(input, @"(?\d)((?\.[0-9]*[1-9])0*\b)?(\.0+\b)?", "${integer}${significantDecimals}"); } } } diff --git a/Patches/SwapPatch.cs b/Patches/SwapPatch.cs index 93ae0d3..af2da0d 100644 --- a/Patches/SwapPatch.cs +++ b/Patches/SwapPatch.cs @@ -1,7 +1,8 @@ using Aki.Reflection.Patching; using Aki.Reflection.Utils; +using Comfort.Common; +using EFT; using EFT.InventoryLogic; -using EFT.UI; using EFT.UI.DragAndDrop; using HarmonyLib; using System; @@ -35,20 +36,24 @@ namespace UIFixes // Whether we're being called from the "check every slot" loop private static bool InHighlight = false; + // The most recent CheckItemFilter result + private static string LastCheckItemFilterId; + private static bool LastCheckItemFilterResult; + public static void Enable() { - GridItemAddressType = PatchConstants.EftTypes.First(t => typeof(ItemAddress).IsAssignableFrom(t) && AccessTools.Property(t, "Grid") != null); + GridItemAddressType = PatchConstants.EftTypes.First(t => typeof(ItemAddress).IsAssignableFrom(t) && t.GetProperty("Grid") != null); // GClass2769 GridItemAddressLocationInGridField = AccessTools.Field(GridItemAddressType, "LocationInGrid"); GridItemAddressGridProperty = AccessTools.Property(GridItemAddressType, "Grid"); - SlotItemAddressType = PatchConstants.EftTypes.First(t => typeof(ItemAddress).IsAssignableFrom(t) && AccessTools.Field(t, "Slot") != null); + SlotItemAddressType = PatchConstants.EftTypes.First(t => typeof(ItemAddress).IsAssignableFrom(t) && t.GetField("Slot") != null); // GClass2767 SlotItemAddressSlotField = AccessTools.Field(SlotItemAddressType, "Slot"); - CanAcceptOperationType = AccessTools.Method(typeof(GridView), "CanAccept").GetParameters()[2].ParameterType.GetElementType(); // parameter is a ref type, get underlying type + CanAcceptOperationType = AccessTools.Method(typeof(GridView), "CanAccept").GetParameters()[2].ParameterType.GetElementType(); // GStruct413, parameter is a ref type, get underlying type CanAcceptOperationSucceededProperty = AccessTools.Property(CanAcceptOperationType, "Succeeded"); CanAcceptOperationErrorProperty = AccessTools.Property(CanAcceptOperationType, "Error"); - SwapOperationType = AccessTools.Method(typeof(InteractionsHandlerClass), "Swap").ReturnType; + SwapOperationType = AccessTools.Method(typeof(InteractionsHandlerClass), "Swap").ReturnType; // GStruct414 SwapOperationToCanAcceptOperationOperator = SwapOperationType.GetMethods().First(m => m.Name == "op_Implicit" && m.ReturnType == CanAcceptOperationType); GridViewNonInteractableField = AccessTools.Field(typeof(GridView), "_nonInteractable"); @@ -57,6 +62,12 @@ namespace UIFixes new GridViewCanAcceptPatch().Enable(); new GetHightLightColorPatch().Enable(); new SlotViewCanAcceptPatch().Enable(); + new CheckItemFilterPatch().Enable(); + } + private static bool InRaid() + { + bool? inRaid = Singleton.Instance?.InRaid; + return inRaid.HasValue && inRaid.Value; } private static bool ValidPrerequisites(ItemContextClass itemContext, ItemContextAbstractClass targetItemContext, object operation) @@ -75,6 +86,7 @@ namespace UIFixes { return false; } + // Check if the source container is a non-interactable GridView. Specifically for StashSearch, but may exist in other scenarios? if (SourceContainer != null && SourceContainer is GridView && (bool)GridViewNonInteractableField.GetValue(SourceContainer)) { @@ -82,6 +94,21 @@ namespace UIFixes } string error = CanAcceptOperationErrorProperty.GetValue(operation).ToString(); + if (Settings.SwapImpossibleContainers.Value && !InRaid() && error.StartsWith("No free room")) + { + // Check if it isn't allowed in that container, if so try to swap + if (LastCheckItemFilterId == itemContext.Item.Id && !LastCheckItemFilterResult) + { + return true; + } + + // Check if it would ever fit no matter what, if not try to swap + if (!CouldEverFit(itemContext, targetItemContext)) + { + return true; + } + } + if (!error.StartsWith("Cannot add") && !error.StartsWith("Cannot apply") && error != "InventoryError/NoPossibleActions") { return false; @@ -90,6 +117,30 @@ namespace UIFixes return true; } + private static bool CouldEverFit(ItemContextClass itemContext, ItemContextAbstractClass containerItemContext) + { + Item item = itemContext.Item; + LootItemClass container = containerItemContext.Item as LootItemClass; + if (container == null) + { + return false; + } + + var size = item.CalculateCellSize(); + var rotatedSize = item.CalculateRotatedSize(itemContext.ItemRotation == ItemRotation.Horizontal ? ItemRotation.Vertical : ItemRotation.Horizontal); + + foreach (StashGridClass grid in container.Grids) + { + if (size.X <= grid.GridWidth.Value && size.Y <= grid.GridHeight.Value || + rotatedSize.X <= grid.GridWidth.Value && rotatedSize.Y <= grid.GridHeight.Value) + { + return true; + } + } + + return false; + } + public class ItemViewOnDragPatch : ModulePatch { protected override MethodBase GetTargetMethod() @@ -219,10 +270,10 @@ namespace UIFixes { // Try original rotations var result = InteractionsHandlerClass.Swap(item, itemToAddress, targetItem, targetToAddress, traderControllerClass, true); + operation = SwapOperationToCanAcceptOperationOperator.Invoke(null, [result]); + __result = (bool)CanAcceptOperationSucceededProperty.GetValue(operation); if (result.Succeeded) { - operation = SwapOperationToCanAcceptOperationOperator.Invoke(null, [result]); - __result = true; return; } } @@ -301,5 +352,22 @@ namespace UIFixes } } + // CanApply, when dealing with containers, eventually calls down into FindPlaceForItem, which calls CheckItemFilter. For reasons, + // if an item fails the filters, it returns the error "no space", instead of "no action". Try to detect this, so we can swap. + public class CheckItemFilterPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + Type type = PatchConstants.EftTypes.First(t => t.GetMethod("CheckItemFilter", BindingFlags.Public | BindingFlags.Static) != null); // GClass2510 + return AccessTools.Method(type, "CheckItemFilter"); + } + + [PatchPostfix] + private static void Postfix(Item item, ref bool __result) + { + LastCheckItemFilterId = item.Id; + LastCheckItemFilterResult = __result; + } + } } } diff --git a/Settings.cs b/Settings.cs index 72308a3..04b1d34 100644 --- a/Settings.cs +++ b/Settings.cs @@ -25,6 +25,7 @@ namespace UIFixes private const string InputSection = "2. Input"; private const string InventorySection = "3. Inventory"; private const string InRaidSection = "4. In Raid"; + private const string AdvancedSection = "5. Advanced"; // General public static ConfigEntry ShowPresetConfirmations { get; set; } @@ -38,6 +39,7 @@ namespace UIFixes // Inventory public static ConfigEntry SwapItems { get; set; } + public static ConfigEntry SwapImpossibleContainers { get; set; } public static ConfigEntry MergeFIRMoney { get; set; } public static ConfigEntry MergeFIRAmmo { get; set; } public static ConfigEntry MergeFIROther { get; set; } @@ -45,6 +47,9 @@ namespace UIFixes // In Raid public static ConfigEntry RemoveDisabledActions { get; set; } + // Advanced + public static ConfigEntry StyleItemPanel { get; set; } + public static void Init(ConfigFile config) { var configEntries = new List(); @@ -115,6 +120,15 @@ namespace UIFixes null, new ConfigurationManagerAttributes { }))); + configEntries.Add(SwapImpossibleContainers = config.Bind( + InventorySection, + "Swap with Incompatible Containers", + false, + new ConfigDescription( + "Enable swapping items with containers that could never fit that item due to size or filter restrictions. Disabled in raid to avoid costly mistakes.", + null, + new ConfigurationManagerAttributes { }))); + configEntries.Add(MergeFIRMoney = config.Bind( InventorySection, "Autostack Money with FiR Money", @@ -152,6 +166,16 @@ namespace UIFixes null, new ConfigurationManagerAttributes { }))); + // Advanced + configEntries.Add(StyleItemPanel = config.Bind( + AdvancedSection, + "Style Item Panel", + true, + new ConfigDescription( + "Clean up and colorize item stats", + null, + new ConfigurationManagerAttributes { IsAdvanced = true }))); + RecalcOrder(configEntries); } private static void RecalcOrder(List configEntries) diff --git a/UIFixes.Test/UnitTests.cs b/UIFixes.Test/UnitTests.cs index bfc64cb..d84247f 100644 --- a/UIFixes.Test/UnitTests.cs +++ b/UIFixes.Test/UnitTests.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + namespace UIFixes.Test { [TestClass] @@ -21,13 +23,16 @@ namespace UIFixes.Test { "del. 2sec", "del. 2sec" }, { "Hello.world", "Hello.world" }, { "2Fast20Furious0", "2Fast20Furious0" }, - { "1.0.2", "1.0.2" } + { "2.720(+0.57)", "2.72(+0.57)" }, + { "2.720(+0.64)", "2.72(+0.64)" }, + { "Class 3 (34)", "Class 3 (34)" }, + { "$1234 (30)", "$1234 (30)" } }; foreach (var testCase in testCases) { string result = ItemPanelPatches.RemoveTrailingZeros(testCase.Key); - Assert.AreEqual(result, testCase.Value); + Assert.AreEqual(testCase.Value, result); } } } diff --git a/UIFixes.csproj b/UIFixes.csproj index f57cbe7..919c3f4 100644 --- a/UIFixes.csproj +++ b/UIFixes.csproj @@ -4,7 +4,7 @@ net471 Tyfon.UIFixes SPT UI Fixes - 1.3.2 + 1.3.3 true latest @@ -25,6 +25,9 @@ $(PathToSPT)\EscapeFromTarkov_Data\Managed\Assembly-CSharp.dll + + $(PathToSPT)\EscapeFromTarkov_Data\Managed\Comfort.dll + $(PathToSPT)\EscapeFromTarkov_Data\Managed\ItemComponent.Types.dll