oni-priority-ux/mod/PLibCore/PPatchTools.cs

1029 lines
42 KiB
C#

/*
* Copyright 2022 Peter Han
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
* and associated documentation files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
using HarmonyLib;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using TranspiledMethod = System.Collections.Generic.IEnumerable<HarmonyLib.CodeInstruction>;
namespace PeterHan.PLib.Core {
/// <summary>
/// Contains tools to aid with patching.
/// </summary>
public static class PPatchTools {
/// <summary>
/// The base binding flags for all reflection methods.
/// </summary>
public const BindingFlags BASE_FLAGS = BindingFlags.Public | BindingFlags.NonPublic;
/// <summary>
/// Passed to GetMethodSafe to match any method arguments.
/// </summary>
public static Type[] AnyArguments {
get {
return new Type[] { null };
}
}
/// <summary>
/// A placeholder flag to ReplaceMethodCallSafe to remove the method call.
/// </summary>
public static readonly MethodInfo RemoveCall = typeof(PPatchTools).GetMethodSafe(
nameof(RemoveMethodCallPrivate), true);
/// <summary>
/// Creates a delegate for a private instance method. This delegate is over ten times
/// faster than reflection, so useful if called frequently on the same object.
/// </summary>
/// <typeparam name="T">A delegate type which matches the method signature.</typeparam>
/// <param name="type">The declaring type of the target method.</param>
/// <param name="method">The target method name.</param>
/// <param name="caller">The object on which to call the method.</param>
/// <param name="argumentTypes">The types of the target method arguments, or PPatchTools.
/// AnyArguments (not recommended, type safety is good) to match any method with
/// that name.</param>
/// <returns>A delegate which calls this method, or null if the method could not be
/// found or did not match the types.</returns>
public static T CreateDelegate<T>(this Type type, string method, object caller,
params Type[] argumentTypes) where T : Delegate {
var del = default(T);
if (type == null)
throw new ArgumentNullException(nameof(type));
if (string.IsNullOrEmpty(method))
throw new ArgumentNullException(nameof(method));
var reflectMethod = GetMethodSafe(type, method, false, argumentTypes);
// MethodInfo.CreateDelegate is @since .NET 5.0 :(
if (reflectMethod != null)
del = Delegate.CreateDelegate(typeof(T), caller, reflectMethod, false) as T;
return del;
}
/// <summary>
/// Creates a delegate for a private instance method. This delegate is over ten times
/// faster than reflection, so useful if called frequently on the same object.
/// </summary>
/// <typeparam name="T">A delegate type which matches the method signature.</typeparam>
/// <param name="type">The declaring type of the target method.</param>
/// <param name="method">The target method.</param>
/// <param name="caller">The object on which to call the method.</param>
/// <returns>A delegate which calls this method, or null if the method was null or did
/// not match the delegate type.</returns>
public static T CreateDelegate<T>(this MethodInfo method, object caller)
where T : Delegate {
var del = default(T);
if (method != null)
del = Delegate.CreateDelegate(typeof(T), caller, method, false) as T;
return del;
}
/// <summary>
/// Creates a delegate for a private instance property getter. This delegate is over
/// ten times faster than reflection, so useful if called frequently on the same object.
///
/// This method does not work on indexed properties.
/// </summary>
/// <typeparam name="T">The property's type.</typeparam>
/// <param name="type">The declaring type of the target property.</param>
/// <param name="property">The target property name.</param>
/// <param name="caller">The object on which to call the property getter.</param>
/// <returns>A delegate which calls this property's getter, or null if the property
/// could not be found or did not match the type.</returns>
public static Func<T> CreateGetDelegate<T>(this Type type, string property,
object caller) {
Func<T> del = null;
if (type == null)
throw new ArgumentNullException(nameof(type));
if (string.IsNullOrEmpty(property))
throw new ArgumentNullException(nameof(property));
var reflectMethod = GetPropertySafe<T>(type, property, false)?.GetGetMethod(true);
if (reflectMethod != null)
del = Delegate.CreateDelegate(typeof(Func<T>), caller, reflectMethod, false)
as Func<T>;
return del;
}
/// <summary>
/// Creates a delegate for a private instance property getter. This delegate is over
/// ten times faster than reflection, so useful if called frequently on the same object.
///
/// This method does not work on indexed properties.
/// </summary>
/// <typeparam name="T">The property's type.</typeparam>
/// <param name="property">The target property.</param>
/// <param name="caller">The object on which to call the property getter.</param>
/// <returns>A delegate which calls this property's getter, or null if the property
/// was null or did not match the type.</returns>
public static Func<T> CreateGetDelegate<T>(this PropertyInfo property, object caller) {
Func<T> del = null;
var reflectMethod = property?.GetGetMethod(true);
if (reflectMethod != null && typeof(T).IsAssignableFrom(property.PropertyType))
del = Delegate.CreateDelegate(typeof(Func<T>), caller, reflectMethod, false)
as Func<T>;
return del;
}
/// <summary>
/// Creates a delegate for a private instance property setter. This delegate is over
/// ten times faster than reflection, so useful if called frequently on the same object.
///
/// This method does not work on indexed properties.
/// </summary>
/// <typeparam name="T">The property's type.</typeparam>
/// <param name="type">The declaring type of the target property.</param>
/// <param name="property">The target property name.</param>
/// <param name="caller">The object on which to call the property setter.</param>
/// <returns>A delegate which calls this property's setter, or null if the property
/// could not be found or did not match the type.</returns>
public static Action<T> CreateSetDelegate<T>(this Type type, string property,
object caller) {
Action<T> del = null;
if (type == null)
throw new ArgumentNullException(nameof(type));
if (string.IsNullOrEmpty(property))
throw new ArgumentNullException(nameof(property));
var reflectMethod = GetPropertySafe<T>(type, property, false)?.GetSetMethod(true);
if (reflectMethod != null)
del = Delegate.CreateDelegate(typeof(Action<T>), caller, reflectMethod, false)
as Action<T>;
return del;
}
/// <summary>
/// Creates a delegate for a private instance property setter. This delegate is over
/// ten times faster than reflection, so useful if called frequently on the same object.
///
/// This method does not work on indexed properties.
/// </summary>
/// <typeparam name="T">The property's type.</typeparam>
/// <param name="property">The target property.</param>
/// <param name="caller">The object on which to call the property setter.</param>
/// <returns>A delegate which calls this property's setter, or null if the property
/// was null or did not match the type.</returns>
public static Action<T> CreateSetDelegate<T>(this PropertyInfo property,
object caller) {
Action<T> del = null;
var reflectMethod = property?.GetSetMethod(true);
if (reflectMethod != null && property.PropertyType.IsAssignableFrom(typeof(T)))
del = Delegate.CreateDelegate(typeof(Action<T>), caller, reflectMethod, false)
as Action<T>;
return del;
}
/// <summary>
/// Creates a delegate for a private static method. This delegate is over ten times
/// faster than reflection, so useful if called frequently.
/// </summary>
/// <typeparam name="T">A delegate type which matches the method signature.</typeparam>
/// <param name="type">The declaring type of the target method.</param>
/// <param name="method">The target method name.</param>
/// <param name="argumentTypes">The types of the target method arguments, or PPatchTools.
/// AnyArguments (not recommended, type safety is good) to match any static method with
/// that name.</param>
/// <returns>A delegate which calls this method, or null if the method could not be
/// found or did not match the types.</returns>
public static T CreateStaticDelegate<T>(this Type type, string method,
params Type[] argumentTypes) where T : Delegate {
var del = default(T);
if (type == null)
throw new ArgumentNullException(nameof(type));
if (string.IsNullOrEmpty(method))
throw new ArgumentNullException(nameof(method));
var reflectMethod = GetMethodSafe(type, method, true, argumentTypes);
if (reflectMethod != null)
del = Delegate.CreateDelegate(typeof(T), reflectMethod, false) as T;
return del;
}
/// <summary>
/// Replaces method calls in a transpiled method.
/// </summary>
/// <param name="method">The method to patch.</param>
/// <param name="translation">A mapping from the old method calls to replace, to the
/// new method calls to use instead.</param>
/// <returns>A transpiled version of that method that replaces or removes all calls
/// to the specified methods.</returns>
private static TranspiledMethod DoReplaceMethodCalls(TranspiledMethod method,
IDictionary<MethodInfo, MethodInfo> translation) {
var remove = RemoveCall;
int replaced = 0;
foreach (var instruction in method) {
var opcode = instruction.opcode;
if ((opcode == OpCodes.Call || opcode == OpCodes.Calli || opcode == OpCodes.
Callvirt) && instruction.operand is MethodInfo target && translation.
TryGetValue(target, out MethodInfo newMethod)) {
if (newMethod != null && newMethod != remove) {
// Replace with new method
instruction.opcode = newMethod.IsStatic ? OpCodes.Call :
OpCodes.Callvirt;
instruction.operand = newMethod;
yield return instruction;
} else {
// Pop "this" if needed
int n = target.GetParameters().Length;
if (!target.IsStatic) n++;
// Pop the arguments off the stack
instruction.opcode = (n == 0) ? OpCodes.Nop : OpCodes.Pop;
instruction.operand = null;
yield return instruction;
for (int i = 0; i < n - 1; i++)
yield return new CodeInstruction(OpCodes.Pop);
}
replaced++;
} else
yield return instruction;
}
#if DEBUG
if (replaced == 0) {
if (translation.Count == 1) {
// Diagnose the method that could not be replaced
var items = new KeyValuePair<MethodInfo, MethodInfo>[1];
translation.CopyTo(items, 0);
MethodInfo from = items[0].Key, to = items[0].Value;
PUtil.LogWarning("No method calls replaced: {0}.{1} to {2}.{3}".F(
from.DeclaringType.FullName, from.Name, to.DeclaringType.FullName,
to.Name));
} else
PUtil.LogWarning("No method calls replaced (multiple replacements)");
}
#endif
}
/// <summary>
/// Dumps the IL body of the method to the debug log.
///
/// Only to be used for debugging purposes.
/// </summary>
/// <param name="opcodes">The IL instructions to log.</param>
public static void DumpMethodBody(TranspiledMethod opcodes) {
var result = new StringBuilder(1024);
result.AppendLine("METHOD BODY:");
foreach (var instr in opcodes) {
foreach (var block in instr.blocks) {
var type = block.blockType;
if (type == ExceptionBlockType.EndExceptionBlock)
result.AppendLine("}");
else {
if (type != ExceptionBlockType.BeginExceptionBlock)
result.Append("} ");
result.Append(block.blockType);
result.AppendLine(" {");
}
}
foreach (var label in instr.labels) {
// Label hashcodes are just easy integers
result.Append(label.GetHashCode());
result.Append(": ");
}
result.Append('\t');
result.Append(instr.opcode);
var operand = instr.operand;
if (operand != null) {
result.Append('\t');
if (operand is Label)
result.Append(operand.GetHashCode());
else if (operand is MethodBase method)
FormatMethodCall(result, method);
else
result.Append(FormatArgument(operand));
}
result.AppendLine();
}
PUtil.LogDebug(result.ToString());
}
/// <summary>
/// This method was taken directly from Harmony (https://github.com/pardeike/Harmony)
/// which is also available under the MIT License.
/// </summary>
/// <param name="argument">The argument to format.</param>
/// <returns>The IL argument in string form.</returns>
private static string FormatArgument(object argument) {
if (argument is null) return "NULL";
if (argument is MethodBase method)
return method.FullDescription();
if (argument is FieldInfo field)
return $"{field.FieldType.FullDescription()} {field.DeclaringType.FullDescription()}::{field.Name}";
if (argument is Label label)
return $"Label{label.GetHashCode()}";
if (argument is Label[] labels) {
int n = labels.Length;
string[] labelCodes = new string[n];
for (int i = 0; i < n; i++)
labelCodes[i] = labels[i].GetHashCode().ToString();
return $"Labels{labelCodes.Join(",")}";
}
if (argument is LocalBuilder lb)
return $"{lb.LocalIndex} ({lb.LocalType})";
if (argument is string sval)
return sval.ToString().ToLiteral();
return argument.ToString().Trim();
}
/// <summary>
/// Formats a method call for logging.
/// </summary>
/// <param name="result">The location where the log is stored.</param>
/// <param name="method">The method that is called.</param>
private static void FormatMethodCall(StringBuilder result, MethodBase method) {
bool first = true;
// The default representation leaves off the class name!
if (method is MethodInfo hasRet) {
result.Append(hasRet.ReturnType.Name);
result.Append(' ');
}
result.Append(method.DeclaringType.Name);
result.Append('.');
result.Append(method.Name);
result.Append('(');
// Put the default value in there too
foreach (var pr in method.GetParameters()) {
string paramName = pr.Name;
if (!first)
result.Append(", ");
result.Append(pr.ParameterType.Name);
if (!string.IsNullOrEmpty(paramName)) {
result.Append(' ');
result.Append(paramName);
}
if (pr.IsOptional) {
result.Append(" = ");
result.Append(pr.DefaultValue);
}
first = false;
}
result.Append(')');
}
/// <summary>
/// Retrieves a field using reflection, or returns null if it does not exist.
/// </summary>
/// <param name="type">The base type.</param>
/// <param name="fieldName">The field name.</param>
/// <param name="isStatic">true to find static fields, or false to find instance
/// fields.</param>
/// <returns>The field, or null if no such field could be found.</returns>
public static FieldInfo GetFieldSafe(this Type type, string fieldName,
bool isStatic) {
FieldInfo field = null;
if (type != null)
try {
var flag = isStatic ? BindingFlags.Static : BindingFlags.Instance;
field = type.GetField(fieldName, BASE_FLAGS | flag);
} catch (AmbiguousMatchException e) {
PUtil.LogException(e);
}
return field;
}
/// <summary>
/// Creates a store instruction to the same local as the specified load instruction.
/// </summary>
/// <param name="load">The initial load instruction.</param>
/// <returns>The counterbalancing store instruction.</returns>
public static CodeInstruction GetMatchingStoreInstruction(CodeInstruction load) {
CodeInstruction instr;
var opcode = load.opcode;
if (opcode == OpCodes.Ldloc)
instr = new CodeInstruction(OpCodes.Stloc, load.operand);
else if (opcode == OpCodes.Ldloc_S)
instr = new CodeInstruction(OpCodes.Stloc_S, load.operand);
else if (opcode == OpCodes.Ldloc_0)
instr = new CodeInstruction(OpCodes.Stloc_0);
else if (opcode == OpCodes.Ldloc_1)
instr = new CodeInstruction(OpCodes.Stloc_1);
else if (opcode == OpCodes.Ldloc_2)
instr = new CodeInstruction(OpCodes.Stloc_2);
else if (opcode == OpCodes.Ldloc_3)
instr = new CodeInstruction(OpCodes.Stloc_3);
else
instr = new CodeInstruction(OpCodes.Pop);
return instr;
}
/// <summary>
/// Retrieves a method using reflection, or returns null if it does not exist.
/// </summary>
/// <param name="type">The base type.</param>
/// <param name="methodName">The method name.</param>
/// <param name="isStatic">true to find static methods, or false to find instance
/// methods.</param>
/// <param name="arguments">The method argument types. If null is provided, any
/// argument types are matched, whereas no arguments match only void methods.</param>
/// <returns>The method, or null if no such method could be found.</returns>
public static MethodInfo GetMethodSafe(this Type type, string methodName,
bool isStatic, params Type[] arguments) {
MethodInfo method = null;
if (type != null && arguments != null)
try {
var flag = isStatic ? BindingFlags.Static : BindingFlags.Instance;
if (arguments.Length == 1 && arguments[0] == null)
// AnyArguments
method = type.GetMethod(methodName, BASE_FLAGS | flag);
else
method = type.GetMethod(methodName, BASE_FLAGS | flag, null, arguments,
new ParameterModifier[arguments.Length]);
} catch (AmbiguousMatchException e) {
PUtil.LogException(e);
}
return method;
}
/// <summary>
/// Retrieves a property using reflection, or returns null if it does not exist.
/// </summary>
/// <param name="type">The base type.</param>
/// <param name="propName">The property name.</param>
/// <param name="isStatic">true to find static properties, or false to find instance
/// properties.</param>
/// <typeparam name="T">The property field type.</typeparam>
/// <returns>The property, or null if no such property could be found.</returns>
public static PropertyInfo GetPropertySafe<T>(this Type type, string propName,
bool isStatic) {
PropertyInfo field = null;
if (type != null)
try {
var flag = isStatic ? BindingFlags.Static : BindingFlags.Instance;
field = type.GetProperty(propName, BASE_FLAGS | flag, null, typeof(T),
Type.EmptyTypes, null);
} catch (AmbiguousMatchException e) {
PUtil.LogException(e);
}
return field;
}
/// <summary>
/// Retrieves an indexed property using reflection, or returns null if it does not
/// exist.
/// </summary>
/// <param name="type">The base type.</param>
/// <param name="propName">The property name.</param>
/// <param name="isStatic">true to find static properties, or false to find instance
/// properties.</param>
/// <param name="arguments">The property indexer's arguments.</param>
/// <typeparam name="T">The property field type.</typeparam>
/// <returns>The property, or null if no such property could be found.</returns>
public static PropertyInfo GetPropertyIndexedSafe<T>(this Type type, string propName,
bool isStatic, params Type[] arguments) {
PropertyInfo field = null;
if (type != null && arguments != null)
try {
var flag = isStatic ? BindingFlags.Static : BindingFlags.Instance;
field = type.GetProperty(propName, BASE_FLAGS | flag, null, typeof(T),
arguments, new ParameterModifier[arguments.Length]);
} catch (AmbiguousMatchException e) {
PUtil.LogException(e);
}
return field;
}
/// <summary>
/// Retrieves a type using its full name (including namespace). However, the assembly
/// name is optional, as this method searches all assemblies in the current
/// AppDomain if it is null or empty.
/// </summary>
/// <param name="name">The type name to retrieve.</param>
/// <param name="assemblyName">If specified, the name of the assembly that contains
/// the type. No other assembly name will be searched if this parameter is not null
/// or empty. The assembly name might not match the DLL name, use a decompiler to
/// make sure.</param>
/// <returns>The type, or null if the type is not found or cannot be loaded.</returns>
public static Type GetTypeSafe(string name, string assemblyName = null) {
Type type = null;
if (string.IsNullOrEmpty(assemblyName))
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) {
try {
type = assembly.GetType(name, false);
} catch (System.IO.IOException) {
// The common parent of exceptions when the type requires another type
// that cannot be loaded
} catch (BadImageFormatException) { }
if (type != null) break;
}
else {
try {
type = Type.GetType(name + ", " + assemblyName, false);
} catch (TargetInvocationException e) {
PUtil.LogWarning("Unable to load type {0} from assembly {1}:".F(name,
assemblyName));
PUtil.LogExcWarn(e);
} catch (ArgumentException e) {
// A generic type is loaded with bad arguments
PUtil.LogWarning("Unable to load type {0} from assembly {1}:".F(name,
assemblyName));
PUtil.LogExcWarn(e);
} catch (System.IO.IOException) {
// The common parent of exceptions when the type requires another type that
// cannot be loaded
} catch (BadImageFormatException) { }
}
return type;
}
/// <summary>
/// Checks to see if a patch with the specified method name (the method used in the
/// patch class) and type is defined.
/// </summary>
/// <param name="instance">The Harmony instance to query for patches. Unused.</param>
/// <param name="target">The target method to search for patches.</param>
/// <param name="type">The patch type to look up.</param>
/// <param name="name">The patch method name to look up (name as declared by patch owner).</param>
/// <returns>true if such a patch was found, or false otherwise</returns>
public static bool HasPatchWithMethodName(Harmony instance, MethodBase target,
HarmonyPatchType type, string name) {
bool found = false;
if (target == null)
throw new ArgumentNullException(nameof(target));
if (string.IsNullOrEmpty(name))
throw new ArgumentNullException(nameof(name));
var patches = Harmony.GetPatchInfo(target);
if (patches != null) {
ICollection<Patch> patchList;
switch (type) {
case HarmonyPatchType.Prefix:
patchList = patches.Prefixes;
break;
case HarmonyPatchType.Postfix:
patchList = patches.Postfixes;
break;
case HarmonyPatchType.Transpiler:
patchList = patches.Transpilers;
break;
case HarmonyPatchType.All:
default:
// All
if (patches.Transpilers != null)
found = HasPatchWithMethodName(patches.Transpilers, name);
if (patches.Prefixes != null)
found = found || HasPatchWithMethodName(patches.Prefixes, name);
patchList = patches.Postfixes;
break;
}
if (patchList != null)
found = found || HasPatchWithMethodName(patchList, name);
}
return found;
}
/// <summary>
/// Checks to see if the patch list has a method with the specified name.
/// </summary>
/// <param name="patchList">The patch list to search.</param>
/// <param name="name">The declaring method name to look up.</param>
/// <returns>true if a patch matches that name, or false otherwise</returns>
private static bool HasPatchWithMethodName(IEnumerable<Patch> patchList, string name) {
bool found = false;
foreach (var patch in patchList)
if (patch.PatchMethod.Name == name) {
found = true;
break;
}
return found;
}
/// <summary>
/// Checks to see if an instruction opcode is a branch instruction.
/// </summary>
/// <param name="opcode">The opcode to check.</param>
/// <returns>true if it is a branch, or false otherwise.</returns>
public static bool IsConditionalBranchInstruction(this OpCode opcode) {
return PTranspilerTools.IsConditionalBranchInstruction(opcode);
}
/// <summary>
/// Adds a logger to all unhandled exceptions.
/// </summary>
[Obsolete("Do not use this method in production code. Make sure to remove it in release builds, or disable it with #if DEBUG.")]
public static void LogAllExceptions() {
PUtil.LogWarning("PLib in mod " + Assembly.GetCallingAssembly().GetName()?.Name +
" is logging ALL unhandled exceptions!");
PTranspilerTools.LogAllExceptions();
}
/// <summary>
/// Adds a logger to all failed assertions. The assertions will still fail, but a stack
/// trace will be printed for each failed assertion.
/// </summary>
[Obsolete("Do not use this method in production code. Make sure to remove it in release builds, or disable it with #if DEBUG.")]
public static void LogAllFailedAsserts() {
PUtil.LogWarning("PLib in mod " + Assembly.GetCallingAssembly().GetName()?.Name +
" is logging ALL failed assertions!");
PTranspilerTools.LogAllFailedAsserts();
}
/// <summary>
/// Transpiles a method to replace instances of one constant value with another.
/// </summary>
/// <param name="method">The method to patch.</param>
/// <param name="oldValue">The old constant to remove.</param>
/// <param name="newValue">The new constant to replace.</param>
/// <param name="all">true to replace all instances, or false to replace the first
/// instance (default).</param>
/// <returns>A transpiled version of that method which replaces instances of the first
/// constant with that of the second.</returns>
public static TranspiledMethod ReplaceConstant(TranspiledMethod method,
double oldValue, double newValue, bool all = false) {
if (method == null)
throw new ArgumentNullException(nameof(method));
int replaced = 0;
foreach (var inst in method) {
var instruction = inst;
var opcode = instruction.opcode;
if ((opcode == OpCodes.Ldc_R8 && (instruction.operand is double dval) &&
dval == oldValue)) {
// Replace instruction if first instance, or all to be replaced
if (all || replaced == 0)
instruction.operand = newValue;
replaced++;
}
yield return instruction;
}
}
/// <summary>
/// Transpiles a method to replace instances of one constant value with another.
/// </summary>
/// <param name="method">The method to patch.</param>
/// <param name="oldValue">The old constant to remove.</param>
/// <param name="newValue">The new constant to replace.</param>
/// <param name="all">true to replace all instances, or false to replace the first
/// instance (default).</param>
/// <returns>A transpiled version of that method which replaces instances of the first
/// constant with that of the second.</returns>
public static TranspiledMethod ReplaceConstant(TranspiledMethod method, float oldValue,
float newValue, bool all = false) {
if (method == null)
throw new ArgumentNullException(nameof(method));
int replaced = 0;
foreach (var inst in method) {
var instruction = inst;
var opcode = instruction.opcode;
if ((opcode == OpCodes.Ldc_R4 && (instruction.operand is float fval) &&
fval == oldValue)) {
// Replace instruction if first instance, or all to be replaced
if (all || replaced == 0)
instruction.operand = newValue;
replaced++;
}
yield return instruction;
}
}
/// <summary>
/// Transpiles a method to replace instances of one constant value with another.
///
/// Note that values of type byte, short, char, and bool are also represented with "i4"
/// constants which can be targeted by this method.
/// </summary>
/// <param name="method">The method to patch.</param>
/// <param name="oldValue">The old constant to remove.</param>
/// <param name="newValue">The new constant to replace.</param>
/// <param name="all">true to replace all instances, or false to replace the first
/// instance (default).</param>
/// <returns>A transpiled version of that method which replaces instances of the first
/// constant with that of the second.</returns>
public static TranspiledMethod ReplaceConstant(TranspiledMethod method, int oldValue,
int newValue, bool all = false) {
int replaced = 0;
bool quickCode = oldValue >= -1 && oldValue <= 8;
var qc = OpCodes.Nop;
if (method == null)
throw new ArgumentNullException(nameof(method));
// Quick test for the opcode on the shorthand forms
if (quickCode)
qc = PTranspilerTools.LOAD_INT[oldValue + 1];
foreach (var inst in method) {
var instruction = inst;
var opcode = instruction.opcode;
object operand = instruction.operand;
if ((opcode == OpCodes.Ldc_I4 && (operand is int ival) && ival == oldValue) ||
(opcode == OpCodes.Ldc_I4_S && (operand is byte bval) && bval ==
oldValue) || (quickCode && qc == opcode)) {
// Replace instruction if first instance, or all to be replaced
if (all || replaced == 0)
PTranspilerTools.ModifyLoadI4(instruction, newValue);
replaced++;
}
yield return instruction;
}
}
/// <summary>
/// Transpiles a method to replace instances of one constant value with another.
/// </summary>
/// <param name="method">The method to patch.</param>
/// <param name="oldValue">The old constant to remove.</param>
/// <param name="newValue">The new constant to replace.</param>
/// <param name="all">true to replace all instances, or false to replace the first
/// instance (default).</param>
/// <returns>A transpiled version of that method which replaces instances of the first
/// constant with that of the second.</returns>
public static TranspiledMethod ReplaceConstant(TranspiledMethod method, long oldValue,
long newValue, bool all = false) {
if (method == null)
throw new ArgumentNullException(nameof(method));
int replaced = 0;
foreach (var inst in method) {
var instruction = inst;
var opcode = instruction.opcode;
if ((opcode == OpCodes.Ldc_I8 && (instruction.operand is long lval) &&
lval == oldValue)) {
// Replace instruction if first instance, or all to be replaced
if (all || replaced == 0)
instruction.operand = newValue;
replaced++;
}
yield return instruction;
}
}
/// <summary>
/// Transpiles a method to remove all calls to the specified victim method.
/// </summary>
/// <param name="method">The method to patch.</param>
/// <param name="victim">The old method calls to remove.</param>
/// <returns>A transpiled version of that method that removes all calls to method.</returns>
/// <exception cref="ArgumentException">If the method being removed had a return value
/// (with what would it be replaced?).</exception>
public static TranspiledMethod RemoveMethodCall(TranspiledMethod method,
MethodInfo victim) {
return ReplaceMethodCallSafe(method, new Dictionary<MethodInfo, MethodInfo>() {
{ victim, RemoveCall }
});
}
/// <summary>
/// A placeholder method for signaling call removal. Not actually called.
/// </summary>
private static void RemoveMethodCallPrivate() { }
/// <summary>
/// Transpiles a method to replace all calls to the specified victim method with
/// another method, altering the call type if necessary. The argument types and return
/// type must match exactly, including in/out/ref parameters.
///
/// If replacing an instance method call with a static method, the first argument
/// will receive the "this" which the old method would have received.
///
/// If newMethod is null, the calls will all be removed silently instead. This will
/// fail if the method call being removed had a return type (what would it be replaced
/// with?); in those cases, declare an empty method with the same signature and
/// replace it instead.
/// </summary>
/// <param name="method">The method to patch.</param>
/// <param name="victim">The old method calls to remove.</param>
/// <param name="newMethod">The new method to replace, or null to delete the calls.</param>
/// <returns>A transpiled version of that method that replaces or removes all calls
/// to method.</returns>
/// <exception cref="ArgumentException">If the new method's argument types do not
/// exactly match the old method's argument types.</exception>
[Obsolete("This method is unsafe. Use the RemoveMethodCall or ReplaceMethodCallSafe versions instead.")]
public static TranspiledMethod ReplaceMethodCall(TranspiledMethod method,
MethodInfo victim, MethodInfo newMethod = null) {
if (newMethod == null)
newMethod = RemoveCall;
return ReplaceMethodCallSafe(method, new Dictionary<MethodInfo, MethodInfo>() {
{ victim, newMethod }
});
}
/// <summary>
/// Transpiles a method to replace all calls to the specified victim method with
/// another method, altering the call type if necessary. The argument types and return
/// type must match exactly, including in/out/ref parameters.
///
/// If replacing an instance method call with a static method, the first argument
/// will receive the "this" which the old method would have received.
/// </summary>
/// <param name="method">The method to patch.</param>
/// <param name="victim">The old method calls to remove.</param>
/// <param name="newMethod">The new method to replace.</param>
/// <returns>A transpiled version of that method that replaces all calls to method.</returns>
/// <exception cref="ArgumentException">If the new method's argument types do not
/// exactly match the old method's argument types.</exception>
public static TranspiledMethod ReplaceMethodCallSafe(TranspiledMethod method,
MethodInfo victim, MethodInfo newMethod) {
if (newMethod == null)
throw new ArgumentNullException(nameof(newMethod));
return ReplaceMethodCallSafe(method, new Dictionary<MethodInfo, MethodInfo>() {
{ victim, newMethod }
});
}
/// <summary>
/// Transpiles a method to replace calls to the specified victim methods with
/// replacement methods, altering the call type if necessary.
///
/// Each key to value pair must meet the criteria defined in
/// ReplaceMethodCall(TranspiledMethod, MethodInfo, MethodInfo).
/// </summary>
/// <param name="method">The method to patch.</param>
/// <param name="translation">A mapping from the old method calls to replace, to the
/// new method calls to use instead.</param>
/// <returns>A transpiled version of that method that replaces or removes all calls
/// to the specified methods.</returns>
/// <exception cref="ArgumentException">If any of the new methods' argument types do
/// not exactly match the old methods' argument types.</exception>
[Obsolete("This method is unsafe. Use ReplaceMethodCallSafe instead.")]
public static TranspiledMethod ReplaceMethodCall(TranspiledMethod method,
IDictionary<MethodInfo, MethodInfo> translation) {
if (method == null)
throw new ArgumentNullException(nameof(method));
if (translation == null)
throw new ArgumentNullException(nameof(translation));
// Sanity check arguments
foreach (var pair in translation) {
var victim = pair.Key;
var newMethod = pair.Value;
if (victim == null)
throw new ArgumentNullException(nameof(victim));
if (newMethod != null)
PTranspilerTools.CompareMethodParams(victim, victim.GetParameterTypes(),
newMethod);
else if (victim.ReturnType != typeof(void))
throw new ArgumentException("Cannot remove method {0} with a return value".
F(victim.Name));
}
return DoReplaceMethodCalls(method, translation);
}
/// <summary>
/// Transpiles a method to replace calls to the specified victim methods with
/// replacement methods, altering the call type if necessary.
///
/// Each key to value pair must meet the criteria defined in
/// ReplaceMethodCallSafe(TranspiledMethod, MethodInfo, MethodInfo).
/// </summary>
/// <param name="method">The method to patch.</param>
/// <param name="translation">A mapping from the old method calls to replace, to the
/// new method calls to use instead.</param>
/// <returns>A transpiled version of that method that replaces or removes all calls
/// to the specified methods.</returns>
/// <exception cref="ArgumentException">If any of the new methods' argument types do
/// not exactly match the old methods' argument types.</exception>
public static TranspiledMethod ReplaceMethodCallSafe(TranspiledMethod method,
IDictionary<MethodInfo, MethodInfo> translation) {
if (method == null)
throw new ArgumentNullException(nameof(method));
if (translation == null)
throw new ArgumentNullException(nameof(translation));
// Sanity check arguments
var remove = RemoveCall;
foreach (var pair in translation) {
var victim = pair.Key;
var newMethod = pair.Value;
if (victim == null)
throw new ArgumentNullException(nameof(victim));
if (newMethod == null)
throw new ArgumentNullException(nameof(newMethod));
if (newMethod == remove) {
if (victim.ReturnType != typeof(void))
throw new ArgumentException("Cannot remove method {0} with a return value".
F(victim.Name));
} else
PTranspilerTools.CompareMethodParams(victim, victim.GetParameterTypes(),
newMethod);
}
return DoReplaceMethodCalls(method, translation);
}
/// <summary>
/// Attempts to read a field value from an object of a type not in this assembly.
///
/// If this operation is expected to be performed more than once on the same object,
/// use a delegate. If the type of the object is known, use Detours.
/// </summary>
/// <typeparam name="T">The type of the value to read.</typeparam>
/// <param name="source">The source object.</param>
/// <param name="name">The field name.</param>
/// <param name="value">The location where the field value will be stored.</param>
/// <returns>true if the field was read, or false if the field was not found or
/// has the wrong type.</returns>
public static bool TryGetFieldValue<T>(object source, string name, out T value) {
bool ok = false;
if (source != null && !string.IsNullOrEmpty(name)) {
var type = source.GetType();
var field = type.GetFieldSafe(name, false);
if (field != null && field.GetValue(source) is T newValue) {
ok = true;
value = newValue;
} else
value = default;
} else
value = default;
return ok;
}
/// <summary>
/// Attempts to read a property value from an object of a type not in this assembly.
///
/// If this operation is expected to be performed more than once on the same object,
/// use a delegate. If the type of the object is known, use Detours.
/// </summary>
/// <typeparam name="T">The type of the value to read.</typeparam>
/// <param name="source">The source object.</param>
/// <param name="name">The property name.</param>
/// <param name="value">The location where the property value will be stored.</param>
/// <returns>true if the property was read, or false if the property was not found or
/// has the wrong type.</returns>
public static bool TryGetPropertyValue<T>(object source, string name, out T value) {
bool ok = false;
if (source != null && !string.IsNullOrEmpty(name)) {
var type = source.GetType();
var prop = type.GetPropertySafe<T>(name, false);
ParameterInfo[] indexes;
if (prop != null && ((indexes = prop.GetIndexParameters()) == null || indexes.
Length < 1) && prop.GetValue(source, null) is T newValue) {
ok = true;
value = newValue;
} else
value = default;
} else
value = default;
return ok;
}
/// <summary>
/// Transpiles a method to wrap it with a try/catch that logs and rethrows all
/// exceptions.
/// </summary>
/// <param name="method">The method body to patch.</param>
/// <param name="generator">The IL generator to make labels.</param>
/// <returns>A transpiled version of that method that is wrapped with an error
/// logger.</returns>
public static TranspiledMethod WrapWithErrorLogger(TranspiledMethod method,
ILGenerator generator) {
var logger = typeof(PUtil).GetMethodSafe(nameof(PUtil.LogException), true,
typeof(Exception));
var ee = method.GetEnumerator();
// Emit all but the last instruction
if (ee.MoveNext()) {
CodeInstruction last;
bool hasNext, isFirst = true;
var endMethod = generator.DefineLabel();
do {
last = ee.Current;
if (isFirst)
last.blocks.Add(new ExceptionBlock(ExceptionBlockType.
BeginExceptionBlock, null));
hasNext = ee.MoveNext();
isFirst = false;
if (hasNext)
yield return last;
} while (hasNext);
CodeInstruction startHandler;
// Preserves the labels "ret" might have had
last.opcode = OpCodes.Nop;
last.operand = null;
yield return last;
// Add a "leave"
yield return new CodeInstruction(OpCodes.Leave, endMethod);
// The exception is already on the stack
if (logger != null)
startHandler = new CodeInstruction(OpCodes.Call, logger);
else
startHandler = new CodeInstruction(OpCodes.Pop);
startHandler.blocks.Add(new ExceptionBlock(ExceptionBlockType.BeginCatchBlock,
typeof(Exception)));
yield return startHandler;
// Rethrow exception
yield return new CodeInstruction(OpCodes.Rethrow);
// End catch block
var endCatch = new CodeInstruction(OpCodes.Leave, endMethod);
endCatch.blocks.Add(new ExceptionBlock(ExceptionBlockType.EndExceptionBlock,
null));
yield return endCatch;
// Actual new ret
var ret = new CodeInstruction(OpCodes.Ret);
ret.labels.Add(endMethod);
yield return ret;
} // Otherwise, there were no instructions to wrap
}
}
}