Skip to content

Commit

Permalink
created midfix patch attribute
Browse files Browse the repository at this point in the history
  • Loading branch information
BlazingTwist committed Oct 29, 2021
1 parent 0b6e493 commit fe9c6a2
Show file tree
Hide file tree
Showing 13 changed files with 746 additions and 8 deletions.
4 changes: 3 additions & 1 deletion BTHarmonyUtils.sln.DotSettings.user
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@
<s:Boolean x:Key="/Default/AddReferences/RecentPaths/=D_003A_005CProgrammierung_005CVisualStudioProjects_005CBTHarmonyUtils_005CBepInEx_005FDependencies_005C0Harmony_002Edll/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/AddReferences/RecentPaths/=D_003A_005CProgrammierung_005CVisualStudioProjects_005CBTHarmonyUtils_005CBepInEx_005FDependencies_005CBepInEx_002Edll/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/AddReferences/RecentPaths/=D_003A_005CProgrammierung_005CVisualStudioProjects_005CBTHarmonyUtils_005CBepInEx_005FDependencies_005CBepInEx_002EHarmony_002Edll/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/AddReferences/RecentPaths/=D_003A_005CProgrammierung_005CVisualStudioProjects_005CBTHarmonyUtils_005CBepInEx_005FDependencies_005CMono_002ECecil_002Edll/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/AddReferences/RecentPaths/=D_003A_005CProgrammierung_005CVisualStudioProjects_005CBTHarmonyUtils_005CBepInEx_005FDependencies_005CMono_002ECecil_002Edll/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=fnptrtype/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Ldarg/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
4 changes: 4 additions & 0 deletions BTHarmonyUtils/BTHarmonyUtils.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@
<Compile Include="src\ILUtils\InstructionSimplifier.cs" />
<Compile Include="src\ILUtils\InstructionUtils.cs" />
<Compile Include="src\ILUtils\MethodBodyReader.cs" />
<Compile Include="src\MidFixPatch\BTHarmonyMidFix.cs" />
<Compile Include="src\MidFixPatch\MidFixCodeGenerator.cs" />
<Compile Include="src\MidFixPatch\MidFixInstructionMatcher.cs" />
<Compile Include="src\MidFixPatch\MidFixPatcher.cs" />
<Compile Include="src\PatcherUtils.cs" />
<Compile Include="src\TranspilerUtils\CodeReplacementPatch.cs" />
<Compile Include="src\TranspilerUtils\TranspilerMarkers.cs" />
Expand Down
2 changes: 1 addition & 1 deletion BTHarmonyUtils/src/ILUtils/ByteBuffer.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System;

namespace BTHarmonyUtils {
namespace BTHarmonyUtils.ILUtils {
/// <summary>
/// Want to read values from an array of Bytes?
/// Use this thing.
Expand Down
2 changes: 1 addition & 1 deletion BTHarmonyUtils/src/ILUtils/InlineSignatureParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
using HarmonyLib;
using Mono.Cecil;

namespace BTHarmonyUtils {
namespace BTHarmonyUtils.ILUtils {
internal static class InlineSignatureParser {
// Based on https://github.com/pardeike/Harmony/blob/381b72c48a9929c55e3469d2bd50b3176a6d5f89/Harmony/Internal/InlineSignatureParser.cs
// ... which is based on https://github.com/MonoMod/MonoMod.Common/blob/fb7fed148af165905ee0f2db1bb4c78a0137fb89/Utils/ReflectionHelper.ParseCallSite.cs
Expand Down
6 changes: 5 additions & 1 deletion BTHarmonyUtils/src/ILUtils/InstructionSimplifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
using System.Reflection.Emit;
using HarmonyLib;

namespace BTHarmonyUtils {
namespace BTHarmonyUtils.ILUtils {
/// <summary>
/// A class that houses all the logic for Simplifying Code-Instructions
/// </summary>
Expand Down Expand Up @@ -139,6 +139,10 @@ public static Tuple<OpCode, object> SimplifyForComparison(CodeInstruction instru
if (opcode == OpCodes.Blt || opcode == OpCodes.Blt_Un || opcode == OpCodes.Blt_Un_S) {
return new Tuple<OpCode, object>(OpCodes.Blt_S, instruction.operand);
}

if (opcode == OpCodes.Callvirt || opcode == OpCodes.Calli) {
return new Tuple<OpCode, object>(OpCodes.Call, instruction.operand);
}

return new Tuple<OpCode, object>(instruction.opcode, instruction.operand);
}
Expand Down
2 changes: 1 addition & 1 deletion BTHarmonyUtils/src/ILUtils/InstructionUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
using System.Reflection.Emit;
using HarmonyLib;

namespace BTHarmonyUtils {
namespace BTHarmonyUtils.ILUtils {
/// <summary>
/// A Utility class centered around CodeInstructions
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion BTHarmonyUtils/src/ILUtils/MethodBodyReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
using HarmonyLib;
using JetBrains.Annotations;

namespace BTHarmonyUtils {
namespace BTHarmonyUtils.ILUtils {
/// <summary>
/// A class for reading CodeInstructions from Methods
/// </summary>
Expand Down
49 changes: 49 additions & 0 deletions BTHarmonyUtils/src/MidFixPatch/BTHarmonyMidFix.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System;
using System.Reflection;
using HarmonyLib;
using JetBrains.Annotations;

namespace BTHarmonyUtils.MidFixPatch {
/// <summary>
/// Marks a HarmonyPatch method as a MidFix (called in the middle of a method, rather than the beginning or end)
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
[PublicAPI]
public class BTHarmonyMidFix : Attribute {
public Type instructionMatcherDeclaringType { get; }
public string instructionMatcherMethodName { get; }
public int priority { get; }

/// <param name="instructionMatcherMethodName">name of the method that provides the instruction matcher for this midFix</param>
/// <param name="priority">if you are using multiple midFix patches for the same original method, you can specify their priority, higher priorities execute first</param>
public BTHarmonyMidFix(string instructionMatcherMethodName, int priority = 1) {
this.instructionMatcherMethodName = instructionMatcherMethodName;
this.priority = priority;
}

/// <param name="instructionMatcherDeclaringType">type containing the instruction matcher method - only required if in a different class</param>
/// <param name="instructionMatcherMethodName">name of the method that provides the instruction matcher for this midFix</param>
/// <param name="priority">if you are using multiple midFix patches for the same original method, you can specify their priority, higher priorities execute first</param>
public BTHarmonyMidFix(Type instructionMatcherDeclaringType, string instructionMatcherMethodName, int priority = 1) {
this.instructionMatcherDeclaringType = instructionMatcherDeclaringType;
this.instructionMatcherMethodName = instructionMatcherMethodName;
this.priority = priority;
}

/// <summary>
/// Resolves the method that provides the MidFixInstructionMatcher for this patch
/// </summary>
public MethodInfo ResolveInstructionMatcherMethod(MethodInfo annotatedMethod) {
if (instructionMatcherDeclaringType == null && annotatedMethod == null) {
throw new ArgumentNullException(nameof(annotatedMethod));
}
Type declaringType = instructionMatcherDeclaringType ?? annotatedMethod.DeclaringType;
if (string.IsNullOrEmpty(instructionMatcherMethodName)) {
throw new ArgumentNullException(
nameof(instructionMatcherMethodName),
$"{annotatedMethod.FullDescription()} is annotated with {nameof(BTHarmonyMidFix)}, but no instructionMatcher was provided");
}
return AccessTools.DeclaredMethod(declaringType, instructionMatcherMethodName) ?? AccessTools.Method(declaringType, instructionMatcherMethodName);
}
}
}
244 changes: 244 additions & 0 deletions BTHarmonyUtils/src/MidFixPatch/MidFixCodeGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text.RegularExpressions;
using BepInEx.Logging;
using HarmonyLib;
using JetBrains.Annotations;

namespace BTHarmonyUtils.MidFixPatch {
/// <summary>
/// Class for generating IL-Code for MidFixes
/// </summary>
public static class MidFixCodeGenerator {
private static readonly ManualLogSource logger = Logger.CreateLogSource($"BTHarmonyUtils:{nameof(MidFixCodeGenerator)}");

/// <summary>
/// Returns the codeInstructions for calling a midFix pattern
/// </summary>
/// <param name="midFixPatch">method to call</param>
/// /// <param name="originalMethod">method that is being patched</param>
/// /// <param name="generator">ilGenerator from harmony</param>
/// <returns>codeInstructions</returns>
public static List<CodeInstruction> GetCodeInstructions(MethodInfo midFixPatch, MethodInfo originalMethod, ILGenerator generator) {
List<CodeInstruction> result = new List<CodeInstruction>();

Type originalClassType = originalMethod.DeclaringType;
Type originalReturnType = originalMethod.ReturnType;

Dictionary<string, ParameterInfo> originalParamDict = originalMethod.GetParameters().ToDictionary(param => param.Name, param => param);

LocalBuilder resultLocal = null;

foreach (ParameterInfo parameter in midFixPatch.GetParameters()) {
string parameterName = parameter.Name;
Type parameterType = parameter.ParameterType;
Type parameterElementType = parameterType.IsByRef ? parameterType.GetElementType() : parameterType;
Debug.Assert(parameterElementType != null, nameof(parameterElementType) + " != null (internal error)");

if (parameterName == "__instance") {
if (!parameterElementType.IsAssignableFrom(originalClassType)) {
LogErrorTypeMismatch(parameterName, midFixPatch, parameterElementType, originalClassType);
return null;
}
if (parameterType.IsByRef) {
logger.LogError($"MidFix patch '{midFixPatch.FullDescription()}' parameter '__instance' may not be by ref!");
return null;
}
if (originalMethod.IsStatic) {
logger.LogError($"MidFix patch '{midFixPatch.FullDescription()}' cannot receive '__instance' parameter,"
+ $" because patched method '{originalMethod.FullDescription()}' is static");
return null;
}
result.Add(new CodeInstruction(OpCodes.Ldarg_0));
} else if (parameterName == "__result") {
if (originalReturnType == typeof(void)) {
logger.LogError($"MidFix patch '{midFixPatch.FullDescription()}' cannot receive '__result' parameter,"
+ $" because patched method '{originalMethod.FullDescription()}' has no return value (void)");
return null;
}
if (originalReturnType.IsByRef) {
logger.LogError($"MidFix patch '{midFixPatch.FullDescription()}' may not alter the result of a method returning a by-ref result ({originalMethod.FullDescription()})");
return null;
}
if (!parameterType.IsByRef) {
logger.LogError($"MidFix patch '{midFixPatch.FullDescription()}' must declare the '__result' parameter with the 'ref' or 'out' keyword");
return null;
}
if (!parameterElementType.IsAssignableFrom(originalReturnType)) {
LogErrorTypeMismatch(parameterName, midFixPatch, parameterElementType, originalReturnType);
return null;
}
resultLocal = generator.DeclareLocal(originalReturnType);
result.Add(new CodeInstruction(OpCodes.Ldloca_S, resultLocal));
} else if (parameterName == "__originalMethod") {
result.Add(new CodeInstruction(OpCodes.Ldtoken, originalMethod));
} else if (parameterName.StartsWith("___")) {
string fieldName = parameterName.Substring("___".Length);
FieldInfo fieldInfo;
if (fieldName.All(char.IsDigit)) {
fieldInfo = AccessTools.DeclaredField(originalClassType, int.Parse(fieldName));
if (fieldInfo == null) {
logger.LogError($"MidFix patch '{midFixPatch.FullDescription()}' failed, did not find field at index '{fieldName}' in class '{originalClassType.FullDescription()}'");
return null;
}
} else {
fieldInfo = AccessTools.Field(originalClassType, fieldName);
if (fieldInfo == null) {
logger.LogError($"MidFix patch '{midFixPatch.FullDescription()}' failed, did not find field with name '{fieldName}' in class '{originalClassType.FullDescription()}'");
return null;
}
}
if (!parameterElementType.IsAssignableFrom(fieldInfo.FieldType)) {
LogErrorTypeMismatch(parameterName, midFixPatch, parameterElementType, fieldInfo.FieldType);
return null;
}
if (fieldInfo.IsStatic) {
result.Add(new CodeInstruction(parameterType.IsByRef ? OpCodes.Ldsflda : OpCodes.Ldsfld, fieldInfo));
} else {
result.Add(new CodeInstruction(OpCodes.Ldarg_0));
result.Add(new CodeInstruction(parameterType.IsByRef ? OpCodes.Ldflda : OpCodes.Ldfld, fieldInfo));
}
} else if (Regex.IsMatch(parameterName, "__local[0-9]+")) {
int localIndex = int.Parse(parameterName.Substring("__".Length));
IList<LocalVariableInfo> localVariableInfos = originalMethod.GetMethodBody().LocalVariables;
LocalVariableInfo targetLocalVariable = localVariableInfos.FirstOrDefault(info => info.LocalIndex == localIndex);
if (targetLocalVariable == null) {
logger.LogError($"MidFix patch '{midFixPatch.FullDescription()}' failed, did not find local variable at index '{localIndex}' in method '{originalMethod.FullDescription()}'");
return null;
}
if (!parameterElementType.IsAssignableFrom(targetLocalVariable.LocalType)) {
LogErrorTypeMismatch(parameterName, midFixPatch, parameterElementType, targetLocalVariable.LocalType);
return null;
}
result.Add(new CodeInstruction(parameterType.IsByRef ? OpCodes.Ldloca_S : OpCodes.Ldloc_S, localIndex));
} else {
// targeting method parameter, either via '__Index' or 'paramName'
ParameterInfo targetParameter;
if (Regex.IsMatch(parameterName, "__[0-9]+")) {
int paramIndex = int.Parse(parameterName.Substring("__".Length));
targetParameter = originalParamDict.Values.FirstOrDefault(paramInfo => paramInfo.Position == paramIndex);
if (targetParameter == null) {
logger.LogError($"MidFix patch '{midFixPatch.FullDescription()}' failed, did not find parameter at index '{paramIndex}' in method '{originalMethod.FullDescription()}'");
return null;
}
} else {
if (!originalParamDict.TryGetValue(parameterName, out targetParameter)) {
logger.LogError(
$"MidFix patch '{midFixPatch.FullDescription()}' failed, did not find parameter with name '{parameterName}' in method '{originalMethod.FullDescription()}'");
return null;
}
}

Type targetParameterType = targetParameter.ParameterType;
Type targetParameterElementType = targetParameterType.IsByRef ? targetParameterType.GetElementType() : targetParameterType;
Debug.Assert(targetParameterElementType != null, nameof(targetParameterElementType) + " != null (internal error)");

if (!parameterElementType.IsAssignableFrom(targetParameterElementType)) {
LogErrorTypeMismatch(parameterName, midFixPatch, parameterElementType, targetParameterElementType);
return null;
}

bool originalParamIsOut = targetParameterType.IsByRef || targetParameter.IsOut;
bool patchParamIsOut = parameterType.IsByRef || parameter.IsOut;

if (targetParameterElementType.IsValueType != parameterElementType.IsValueType) {
logger.LogError($"MidFix patch '{midFixPatch.FullDescription()}' parameter '{parameterName}' is {(parameterElementType.IsValueType ? "valueType" : "objectType")}"
+ $", but original '{originalMethod.FullDescription()}' is not. Boxing/Unboxing is not supported by BTHarmonyUtils.");
return null;
}

int actualParamIndex = targetParameter.Position + (originalMethod.IsStatic ? 0 : 1);
if (originalParamIsOut == patchParamIsOut) {
result.Add(new CodeInstruction(OpCodes.Ldarg, actualParamIndex));
} else if (!originalParamIsOut) {
// !originalParamIsOut && patchParamIsOut
result.Add(new CodeInstruction(OpCodes.Ldarga, actualParamIndex));
} else {
// originalParamIsOut && !patchParamIsOut
result.Add(new CodeInstruction(OpCodes.Ldarg, actualParamIndex));
result.Add(targetParameterElementType.IsValueType
? new CodeInstruction(OpCodes.Ldobj, targetParameterElementType)
: new CodeInstruction(GetIndOpcode(parameterType)));
}
}
}

result.Add(new CodeInstruction(OpCodes.Call, midFixPatch));

Type returnType = midFixPatch.ReturnType;
if (returnType == typeof(void)) {
if (resultLocal != null) {
logger.LogWarning($"MidFix patch '{midFixPatch.FullDescription()}' __result will be ignored, because return type is void");
}
} else if (returnType == typeof(bool)) {
Label midFixContinueLabel = generator.DefineLabel();
result.Add(new CodeInstruction(OpCodes.Brtrue, midFixContinueLabel));
if (resultLocal != null) {
result.Add(new CodeInstruction(OpCodes.Ldloc_S, resultLocal));
}
result.Add(new CodeInstruction(OpCodes.Ret));
CodeInstruction midFixContinueInstruction = new CodeInstruction(OpCodes.Nop);
midFixContinueInstruction.labels.Add(midFixContinueLabel);
result.Add(midFixContinueInstruction);
} else {
logger.LogWarning($"MidFix patch '{midFixPatch.FullDescription()}' does not return a recognized type, will be treated as returning void");
result.Add(new CodeInstruction(OpCodes.Pop));
if (resultLocal != null) {
logger.LogWarning($"MidFix patch '{midFixPatch.FullDescription()}' __result will be ignored, because return type is void");
}
}

return result;
}

private static void LogErrorTypeMismatch(string parameterName, MethodInfo patchMethod, Type patchType, Type originalType) {
logger.LogError($"MidFix parameter '{parameterName}' for patch: {patchMethod.FullDescription()} is of type '{patchType.FullDescription()}'"
+ $" and cannot be assigned from original type '{originalType.FullDescription()}' - patch will be skipped.");
}

/// <summary>
/// straight copy from Harmony GetIndOpcode
/// </summary>
[PublicAPI]
public static OpCode GetIndOpcode(Type type) {
if (type.IsEnum) {
return OpCodes.Ldind_I4;
}
if (type == typeof(float)) {
return OpCodes.Ldind_R4;
}
if (type == typeof(double)) {
return OpCodes.Ldind_R8;
}
if (type == typeof(byte)) {
return OpCodes.Ldind_U1;
}
if (type == typeof(ushort)) {
return OpCodes.Ldind_U2;
}
if (type == typeof(uint)) {
return OpCodes.Ldind_U4;
}
if (type == typeof(ulong)) {
return OpCodes.Ldind_I8;
}
if (type == typeof(sbyte)) {
return OpCodes.Ldind_I1;
}
if (type == typeof(short)) {
return OpCodes.Ldind_I2;
}
if (type == typeof(int)) {
return OpCodes.Ldind_I4;
}
if (type == typeof(long)) {
return OpCodes.Ldind_I8;
}
return OpCodes.Ldind_Ref;
}
}
}
Loading

0 comments on commit fe9c6a2

Please sign in to comment.