Fix bsg item panel bugs, mouse over after swap, compare deltas when mod swapping

This commit is contained in:
Tyfon
2024-05-05 19:06:45 -07:00
parent 5ef75eb674
commit b4fabe75f7
3 changed files with 149 additions and 28 deletions

View File

@@ -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<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) && 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<CodeInstruction> Transpile(IEnumerable<CodeInstruction> 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 <value>(<changed>)
// <value> 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 += " <color=" + color + ">(" + sign + delta.ToString("0.0#") + ")</color>";
}
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 <changed> value need to be converted
var match = Regex.Match(text, @" %\(([+-].*)\)");
if (match.Success)
@@ -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, @"\(([+-].*)\)", "<color=" + color + ">(" + sign + value + ")</color>");
}
}

View File

@@ -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

View File

@@ -61,6 +61,6 @@
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="if $(ConfigurationName) == Debug (&#xD;&#xA; xcopy /F /Y &quot;$(TargetPath)&quot; &quot;$(ProjectDir)\$(PathToSPT)\BepInEx\plugins\$(TargetName).dll&quot;&#xD;&#xA; xcopy /F /Y &quot;$(ProjectDir)$(OutDir)$(TargetName).pdb&quot; &quot;$(ProjectDir)\$(PathToSPT)\BepInEx\plugins\$(TargetName).pdb&quot;&#xD;&#xA;) &#xD;&#xA;if $(ConfigurationName) == Release (&#xD;&#xA; xcopy /F /Y &quot;$(TargetPath)&quot; &quot;$(ProjectDir)\$(PathToSPT)\BepInEx\plugins\$(TargetName).dll&quot;&#xD;&#xA;)&#xD;&#xA;if $(Configurationname) == Dist (&#xD;&#xA; mkdir &quot;$(ProjectDir)\dist\BepInDex\plugins&quot;&#xD;&#xA; xcopy /F /Y &quot;$(TargetPath)&quot; &quot;$(ProjectDir)\dist\BepInEx\plugins\$(TargetName).dll&quot;&#xD;&#xA; 7z a -t7z Tyfon-UIFixes-$(Version).7z $(ProjectDir)\dist\BepInEx&#xD;&#xA; move /Y Tyfon-UIFixes-$(Version).7z dist\&#xD;&#xA;)" />
<Exec Command="if $(ConfigurationName) == Debug (&#xD;&#xA; xcopy /F /Y &quot;$(TargetPath)&quot; &quot;$(ProjectDir)\$(PathToSPT)\BepInEx\plugins\&quot;&#xD;&#xA; xcopy /F /Y &quot;$(ProjectDir)$(OutDir)$(TargetName).pdb&quot; &quot;$(ProjectDir)\$(PathToSPT)\BepInEx\plugins\&quot;&#xD;&#xA;) &#xD;&#xA;if $(ConfigurationName) == Release (&#xD;&#xA; xcopy /F /Y &quot;$(TargetPath)&quot; &quot;$(ProjectDir)\$(PathToSPT)\BepInEx\plugins\&quot;&#xD;&#xA;)&#xD;&#xA;if $(Configurationname) == Dist (&#xD;&#xA; mkdir &quot;$(ProjectDir)\dist\BepInDex\plugins&quot;&#xD;&#xA; xcopy /F /Y &quot;$(TargetPath)&quot; &quot;$(ProjectDir)\dist\BepInEx\plugins\&quot;&#xD;&#xA; 7z a -t7z Tyfon-UIFixes-$(Version).7z $(ProjectDir)\dist\BepInEx&#xD;&#xA; move /Y Tyfon-UIFixes-$(Version).7z dist\&#xD;&#xA;)" />
</Target>
</Project>