diff --git a/Patches/ItemPanelPatches.cs b/Patches/ItemPanelPatches.cs index ec64274..e98f603 100644 --- a/Patches/ItemPanelPatches.cs +++ b/Patches/ItemPanelPatches.cs @@ -1,5 +1,4 @@ using Aki.Reflection.Patching; -using Aki.Reflection.Utils; using EFT.InventoryLogic; using EFT.UI; using HarmonyLib; @@ -8,6 +7,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Reflection; +using System.Reflection.Emit; using System.Text.RegularExpressions; using TMPro; using UnityEngine; @@ -19,19 +19,16 @@ namespace UIFixes private static FieldInfo AttributeCompactPanelDictionaryField; private static FieldInfo AttributeCompactDropdownDictionaryField; - private static FieldInfo ItemComponentItemField; - private static FieldInfo CompactCharacteristicPanelItemAttributeField; + private static FieldInfo CompactCharacteristicPanelCompareItemAttributeField; 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) && t.GetField("Item") != null); // GClass2754 - ItemComponentItemField = AccessTools.Field(itemComponentType, "Item"); - CompactCharacteristicPanelItemAttributeField = AccessTools.Field(typeof(CompactCharacteristicPanel), "ItemAttribute"); + CompactCharacteristicPanelCompareItemAttributeField = AccessTools.Field(typeof(CompactCharacteristicPanel), "CompareItemAttribute"); new InjectButtonPatch().Enable(); new LoadModStatsPatch().Enable(); @@ -260,8 +257,14 @@ namespace UIFixes private class FormatFullValuesPatch : ModulePatch { + private static MethodInfo RoundToIntMethod; + private static MethodInfo ToStringMethod; + protected override MethodBase GetTargetMethod() { + RoundToIntMethod = AccessTools.Method(typeof(Mathf), "RoundToInt"); + ToStringMethod = AccessTools.Method(typeof(float), "ToString", [typeof(string)]); + return AccessTools.Method(typeof(CharacteristicPanel), "SetValues"); } @@ -270,19 +273,61 @@ namespace UIFixes { try { - FormatText(__instance, ___ValueText); + FormatText(__instance, ___ValueText, true); } catch (Exception ex) { Logger.LogError(ex); } } + + // This transpiler looks for where it rounds a float to an int, and skips that. Instead it calls ToString("0.0#") on it + [PatchTranspiler] + private static IEnumerable Transpile(IEnumerable instructions) + { + int skip = 0; + CodeInstruction lastInstruction = null; + CodeInstruction currentInstruction = null; + foreach (var instruction in instructions) + { + if (lastInstruction == null) + { + lastInstruction = instruction; + continue; + } + + currentInstruction = instruction; + + if (skip > 0) + { + --skip; + } + else if (currentInstruction.Calls(RoundToIntMethod)) + { + yield return new CodeInstruction(OpCodes.Ldloca_S, 17); + yield return new CodeInstruction(OpCodes.Ldstr, "0.0#"); + yield return new CodeInstruction(OpCodes.Call, ToStringMethod); + skip = 4; + } + else + { + yield return lastInstruction; + } + + lastInstruction = instruction; + } + + if (currentInstruction != null) + { + yield return currentInstruction; + } + } } // 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) + private static void FormatText(CompactCharacteristicPanel panel, TextMeshProUGUI textMesh, bool fullBar = false) { // Comparisons are shown as () // is from each attribute type's StringValue() function, so is formatted *mostly* ok @@ -301,6 +346,40 @@ namespace UIFixes string text = textMesh.text; ItemAttributeClass attribute = CompactCharacteristicPanelItemAttributeField.GetValue(panel) as ItemAttributeClass; + // Holy shit did they mess up MOA. Half of the calculation is done in the StringValue() method, so calculating delta from Base() loses all that + // Plus, they round the difference to the nearest integer (!?) + // Completely redo it + if ((EItemAttributeId)attribute.Id == EItemAttributeId.CenterOfImpact) + { + ItemAttributeClass compareAttribute = CompactCharacteristicPanelCompareItemAttributeField.GetValue(panel) as ItemAttributeClass; + if (compareAttribute != null) + { + string currentStringValue = attribute.StringValue(); + var moaMatch = Regex.Match(currentStringValue, @"^(\S+)"); + float moa; + if (float.TryParse(moaMatch.Groups[1].Value, out moa)) + { + string compareStringValue = compareAttribute.StringValue(); + moaMatch = Regex.Match(compareStringValue, @"^(\S+)"); + float compareMoa; + if (float.TryParse(moaMatch.Groups[1].Value, out compareMoa)) + { + float delta = compareMoa - moa; + string final = currentStringValue; + if (Math.Abs(delta) > 0) + { + string sign = delta > 0 ? "+" : ""; + string color = (attribute.LessIsGood && delta < 0) || (!attribute.LessIsGood && delta > 0) ? IncreasingColorHex : DecreasingColorHex; + final += " (" + sign + delta.ToString("0.0#") + ")"; + } + + textMesh.text = final; + return; + } + } + } + } + // 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) @@ -326,7 +405,7 @@ namespace UIFixes 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; @@ -350,6 +429,11 @@ namespace UIFixes { string sign = value > 0 ? "+" : ""; string color = (attribute.LessIsGood && value < 0) || (!attribute.LessIsGood && value > 0) ? IncreasingColorHex : DecreasingColorHex; + if (fullBar && Math.Abs(value) >= 1) + { + // Fullbar rounds to nearest int, but I transpiled it not to. Restore the rounding, but only if the value won't just round to 0 + value = Mathf.RoundToInt(value); + } text = Regex.Replace(text, @"\(([+-].*)\)", "(" + sign + value + ")"); } } diff --git a/Patches/SwapPatch.cs b/Patches/SwapPatch.cs index f6bf906..3b8aacb 100644 --- a/Patches/SwapPatch.cs +++ b/Patches/SwapPatch.cs @@ -3,12 +3,14 @@ using Aki.Reflection.Utils; using Comfort.Common; using EFT; using EFT.InventoryLogic; +using EFT.UI; using EFT.UI.DragAndDrop; using HarmonyLib; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; +using UnityEngine; using UnityEngine.EventSystems; namespace UIFixes @@ -64,8 +66,9 @@ namespace UIFixes new ItemViewOnDragPatch().Enable(); new GridViewCanAcceptPatch().Enable(); - new GetHightLightColorPatch().Enable(); - new SlotViewCanAcceptPatch().Enable(); + new GridGetHightLightColorPatch().Enable(); + new SlotGetHightLightColorPatch().Enable(); + new ItemContextClassCanAcceptPatch().Enable(); new CheckItemFilterPatch().Enable(); new SwapOperationRaiseEventsPatch().Enable(); new GridItemViewOnPointerEnterPatch().Enable(); @@ -115,7 +118,7 @@ namespace UIFixes } } - if (!error.EndsWith("not applicable") && !error.StartsWith("Cannot apply") && error != "InventoryError/NoPossibleActions") + if (!error.EndsWith("not applicable") && !(error.StartsWith("Cannot apply") && !error.EndsWith("modified")) && error != "InventoryError/NoPossibleActions") { return false; } @@ -182,7 +185,6 @@ namespace UIFixes return false; } - //if (itemAddressA is GClass2769 && itemAddressB is GClass2769) if (GridItemAddressType.IsInstanceOfType(itemAddressA) && GridItemAddressType.IsInstanceOfType(itemAddressB)) { LocationInGrid locationA = GridItemAddressLocationInGridField.GetValue(itemAddressA) as LocationInGrid; @@ -277,7 +279,7 @@ 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); + __result = result.Succeeded; if (result.Succeeded) { return; @@ -294,6 +296,7 @@ namespace UIFixes var result = InteractionsHandlerClass.Swap(item, itemToAddress, targetItem, targetToAddress, traderControllerClass, true); if (result.Succeeded) { + // Only save this operation result if it succeeded, otherwise we return the non-rotated result from above operation = SwapOperationToCanAcceptOperationOperator.Invoke(null, [result]); __result = true; return; @@ -347,7 +350,7 @@ namespace UIFixes } } - if (LastHoveredGridItemView != null) + if (LastHoveredGridItemView != null && LastHoveredGridItemView.ItemContext != null) { LastHoveredGridItemView.OnPointerEnter(new PointerEventData(EventSystem.current)); } @@ -370,39 +373,50 @@ namespace UIFixes // Called when dragging an item onto an equipment slot // Handles any kind of ItemAddress as the target destination (aka where the dragged item came from) - public class SlotViewCanAcceptPatch : ModulePatch + public class ItemContextClassCanAcceptPatch : ModulePatch { protected override MethodBase GetTargetMethod() { - Type type = typeof(SlotView); + Type type = typeof(ItemContextClass); return type.GetMethod("CanAccept"); } [PatchPostfix] - private static void Postfix(SlotView __instance, ItemContextClass itemContext, ItemContextAbstractClass targetItemContext, ref object operation, InventoryControllerClass ___InventoryController, ref bool __result) + private static void Postfix(ItemContextClass __instance, Slot slot, ItemContextAbstractClass targetItemContext, ref object operation, TraderControllerClass itemController, bool simulate, ref bool __result) { - if (!ValidPrerequisites(itemContext, targetItemContext, operation)) + // targetItemContext here is not the target item, it's the *parent* context, i.e. the owner of the slot + // Do a few more checks + if (slot.ContainedItem == null || __instance.Item == slot.ContainedItem || slot.ContainedItem.GetAllParentItems().Contains(__instance.Item)) { return; } - var item = itemContext.Item; - var targetItem = targetItemContext.Item; - var itemToAddress = Activator.CreateInstance(SlotItemAddressType, [__instance.Slot]) as ItemAddress; + if (!ValidPrerequisites(__instance, targetItemContext, operation)) + { + return; + } + + var item = __instance.Item; + var targetItem = slot.ContainedItem; + var itemToAddress = Activator.CreateInstance(SlotItemAddressType, [slot]) as ItemAddress; var targetToAddress = item.Parent; - var result = InteractionsHandlerClass.Swap(item, itemToAddress, targetItem, targetToAddress, ___InventoryController, true); - if (result.Succeeded) + // Repair kits again + // Don't have access to ItemView to call CanInteract, but repair kits can't go into any slot I'm aware of, so... + if (item.Template is RepairKitClass) { - operation = SwapOperationToCanAcceptOperationOperator.Invoke(null, [result]); - __result = true; + return; } + + var result = InteractionsHandlerClass.Swap(item, itemToAddress, targetItem, targetToAddress, itemController, simulate); + operation = SwapOperationToCanAcceptOperationOperator.Invoke(null, [result]); + __result = result.Succeeded; } } // The patched method here is called when iterating over all slots to highlight ones that the dragged item can interact with // Since swap has no special highlight, I just skip the patch here (minor perf savings, plus makes debugging a million times easier) - public class GetHightLightColorPatch : ModulePatch + public class GridGetHightLightColorPatch : ModulePatch { protected override MethodBase GetTargetMethod() { @@ -423,6 +437,29 @@ namespace UIFixes } } + // The patched method here is called when iterating over all slots to highlight ones that the dragged item can interact with + // Since swap has no special highlight, I just skip the patch here (minor perf savings, plus makes debugging a million times easier) + public class SlotGetHightLightColorPatch : ModulePatch + { + protected override MethodBase GetTargetMethod() + { + Type type = typeof(SlotView); + return type.GetMethod("method_2"); + } + + [PatchPrefix] + private static void Prefix() + { + InHighlight = true; + } + + [PatchPostfix] + private static void Postfix() + { + InHighlight = false; + } + } + // 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 diff --git a/UIFixes.csproj b/UIFixes.csproj index cdb871c..bccb8c2 100644 --- a/UIFixes.csproj +++ b/UIFixes.csproj @@ -61,6 +61,6 @@ - +