Color full panel bars, swap containers, fix black text, fix non-delta parens

This commit is contained in:
Tyfon
2024-05-02 11:19:02 -07:00
parent 107d598b7e
commit 1809f1343d
6 changed files with 214 additions and 81 deletions

View File

@@ -8,7 +8,7 @@ using System.Reflection;
namespace UIFixes
{
internal class ContainerStackPatch : ModulePatch
public class ContainerStackPatch : ModulePatch
{
private static Type MergeableItemType;

View File

@@ -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<KeyValuePair<ItemAttributeClass, CompactCharacteristicPanel>>).IsAssignableFrom(f.FieldType));
AttributeCompactDropdownDictionaryField = AccessTools.GetDeclaredFields(typeof(ItemSpecificationPanel)).First(f => typeof(IEnumerable<KeyValuePair<ItemAttributeClass, CompactCharacteristicDropdownPanel>>).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,43 +243,72 @@ 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)
{
try
{
FormatText(__instance, ___ValueText);
}
catch (Exception ex)
{
Logger.LogError(ex);
}
}
}
private class FormatFullValuesPatch : ModulePatch
{
protected override MethodBase GetTargetMethod()
{
return AccessTools.Method(typeof(CharacteristicPanel), "SetValues");
}
[PatchPostfix]
private static void Postfix(CharacteristicPanel __instance, TextMeshProUGUI ___ValueText)
{
try
{
FormatText(__instance, ___ValueText);
}
catch (Exception ex)
{
Logger.LogError(ex);
}
}
}
// These fields are percents, but have been manually multipied by 100 already
private static readonly EItemAttributeId[] NonPercentPercents = [EItemAttributeId.ChangeMovementSpeed, EItemAttributeId.ChangeTurningSpeed, EItemAttributeId.Ergonomics];
private static void FormatText(CompactCharacteristicPanel panel, TextMeshProUGUI textMesh)
{
// Comparisons are shown as <value>(<changed>)
// <value> is from each attribute type's StringValue() function, so is formatted *mostly* ok
// <changed> 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;
}
Color increasingColor = (Color)IncreasingColorField.GetValue(__instance);
string increasingColorHex = "#" + ColorUtility.ToHtmlStringRGB(increasingColor);
// 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";
Color decreasingColor = (Color)DecreasingColorField.GetValue(__instance);
string decreasingColorHex = "#" + ColorUtility.ToHtmlStringRGB(decreasingColor);
string text = ___ValueText.text;
ItemAttributeClass attribute = ItemAttributeField.GetValue(__instance) as ItemAttributeClass;
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 <changed> value need to be converted
var match = Regex.Match(text, @" %\((.*)\)");
var match = Regex.Match(text, @" %\(([+-].*)\)");
if (match.Success)
{
float value;
@@ -282,23 +316,23 @@ namespace UIFixes
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;
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, @"%\(.*\)", "%<color=" + color + ">(" + sign + value + "%)</color>");
text = Regex.Replace(text, @"%\([+-].*\)", "%<color=" + color + ">(" + sign + value + "%)</color>");
}
else
{
text = Regex.Replace(text, @"%\(.*\)", "%<color=" + color + ">(" + sign + value.ToString("P1") + ")</color>");
text = Regex.Replace(text, @"%\([+-].*\)", "%<color=" + color + ">(" + sign + value.ToString("P1") + ")</color>");
}
}
}
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)%\((.*)\)");
match = Regex.Match(text, @"(\S)%\(([+-].*)\)");
if (match.Success)
{
float value;
@@ -306,14 +340,14 @@ namespace UIFixes
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;
text = Regex.Replace(text, @"(\S)%\((.*)\)", match.Groups[1].Value + "%<color=" + color + ">(" + sign + value + "%)</color>");
string color = (attribute.LessIsGood && value < 0) || (!attribute.LessIsGood && value > 0) ? IncreasingColorHex : DecreasingColorHex;
text = Regex.Replace(text, @"(\S)%\(([+-].*)\)", match.Groups[1].Value + "%<color=" + color + ">(" + sign + value + "%)</color>");
}
}
else
{
// Finally the ones that aren't percents
match = Regex.Match(text, @"\((.*)\)");
match = Regex.Match(text, @"\(([+-].*)\)");
if (match.Success)
{
float value;
@@ -321,8 +355,8 @@ namespace UIFixes
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, @"\((.*)\)", "<color=" + color + ">(" + sign + value + ")</color>");
string color = (attribute.LessIsGood && value < 0) || (!attribute.LessIsGood && value > 0) ? IncreasingColorHex : DecreasingColorHex;
text = Regex.Replace(text, @"\(([+-].*)\)", "<color=" + color + ">(" + sign + value + ")</color>");
}
}
}
@@ -335,8 +369,7 @@ namespace UIFixes
text = text.Replace(" %", "%");
text = text.Replace("(", " (");
___ValueText.text = text;
}
textMesh.text = text;
}
private static List<ItemAttributeClass> 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, @"(?<integer>\d)((?<significantDecimals>\.[0-9]*[^0])0*\b)?(\.0+\b)?", "${integer}${significantDecimals}");
return Regex.Replace(input, @"(?<integer>\d)((?<significantDecimals>\.[0-9]*[1-9])0*\b)?(\.0+\b)?", "${integer}${significantDecimals}");
}
}
}

View File

@@ -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<GClass2797>
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<AbstractGame>.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;
}
}
}
}

View File

@@ -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<WeaponPresetConfirmationOption> ShowPresetConfirmations { get; set; }
@@ -38,6 +39,7 @@ namespace UIFixes
// Inventory
public static ConfigEntry<bool> SwapItems { get; set; }
public static ConfigEntry<bool> SwapImpossibleContainers { get; set; }
public static ConfigEntry<bool> MergeFIRMoney { get; set; }
public static ConfigEntry<bool> MergeFIRAmmo { get; set; }
public static ConfigEntry<bool> MergeFIROther { get; set; }
@@ -45,6 +47,9 @@ namespace UIFixes
// In Raid
public static ConfigEntry<bool> RemoveDisabledActions { get; set; }
// Advanced
public static ConfigEntry<bool> StyleItemPanel { get; set; }
public static void Init(ConfigFile config)
{
var configEntries = new List<ConfigEntryBase>();
@@ -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<ConfigEntryBase> configEntries)

View File

@@ -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<color=#FFFFFF>(+0.64)</color>", "2.72<color=#FFFFFF>(+0.64)</color>" },
{ "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);
}
}
}

View File

@@ -4,7 +4,7 @@
<TargetFramework>net471</TargetFramework>
<AssemblyName>Tyfon.UIFixes</AssemblyName>
<Description>SPT UI Fixes</Description>
<Version>1.3.2</Version>
<Version>1.3.3</Version>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>latest</LangVersion>
</PropertyGroup>
@@ -25,6 +25,9 @@
<Reference Include="Assembly-CSharp">
<HintPath>$(PathToSPT)\EscapeFromTarkov_Data\Managed\Assembly-CSharp.dll</HintPath>
</Reference>
<Reference Include="Comfort">
<HintPath>$(PathToSPT)\EscapeFromTarkov_Data\Managed\Comfort.dll</HintPath>
</Reference>
<Reference Include="ItemComponent.Types">
<HintPath>$(PathToSPT)\EscapeFromTarkov_Data\Managed\ItemComponent.Types.dll</HintPath>
</Reference>