/*
* 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 PeterHan.PLib.Core;
using System;
using System.Reflection;
using System.Reflection.Emit;
namespace PeterHan.PLib.Detours {
///
/// Efficiently detours around many changes in the game by creating detour methods and
/// accessors which are resilient against many types of source compatible but binary
/// incompatible changes.
///
public static class PDetours {
///
/// Creates a dynamic detour method of the specified delegate type to wrap a base game
/// method with the same name as the delegate type. The dynamic method will
/// automatically adapt if optional parameters are added, filling in their default
/// values.
///
/// The delegate type to be used to call the detour.
/// The target type.
/// The detour that will call the method with the name of the delegate type.
/// If the delegate does not match any valid target method.
public static D Detour(this Type type) where D : Delegate {
return Detour(type, typeof(D).Name);
}
///
/// Creates a dynamic detour method of the specified delegate type to wrap a base game
/// method with the specified name. The dynamic method will automatically adapt if
/// optional parameters are added, filling in their default values.
///
/// The delegate type to be used to call the detour.
/// The target type.
/// The method name.
/// The detour that will call that method.
/// If the delegate does not match any valid target method.
public static D Detour(this Type type, string name) where D : Delegate {
if (type == null)
throw new ArgumentNullException(nameof(type));
if (string.IsNullOrEmpty(name))
throw new ArgumentNullException(nameof(name));
var methods = type.GetMethods(PPatchTools.BASE_FLAGS | BindingFlags.Static |
BindingFlags.Instance);
// Determine delegate return type
var expected = DelegateInfo.Create(typeof(D));
MethodInfo bestMatch = null;
int bestParamCount = int.MaxValue;
foreach (var method in methods)
if (method.Name == name) {
try {
var result = ValidateDelegate(expected, method, method.ReturnType);
int n = result.Length;
// Choose overload with fewest parameters to substitute
if (n < bestParamCount) {
bestParamCount = n;
bestMatch = method;
}
} catch (DetourException) {
// Keep looking
}
}
if (bestMatch == null)
throw new DetourException("No match found for {1}.{0}".F(name, type.FullName));
return Detour(bestMatch);
}
///
/// Creates a dynamic detour method of the specified delegate type to wrap a base game
/// constructor. The dynamic method will automatically adapt if optional parameters
/// are added, filling in their default values.
///
/// The delegate type to be used to call the detour.
/// The target type.
/// The detour that will call that type's constructor.
/// If the delegate does not match any valid target method.
public static D DetourConstructor(this Type type) where D : Delegate {
if (type == null)
throw new ArgumentNullException(nameof(type));
var constructors = type.GetConstructors(PPatchTools.BASE_FLAGS | BindingFlags.
Instance);
// Determine delegate return type
var expected = DelegateInfo.Create(typeof(D));
ConstructorInfo bestMatch = null;
int bestParamCount = int.MaxValue;
foreach (var constructor in constructors)
try {
var result = ValidateDelegate(expected, constructor, type);
int n = result.Length;
// Choose overload with fewest parameters to substitute
if (n < bestParamCount) {
bestParamCount = n;
bestMatch = constructor;
}
} catch (DetourException) {
// Keep looking
}
if (bestMatch == null)
throw new DetourException("No match found for {0} constructor".F(type.
FullName));
return Detour(bestMatch);
}
///
/// Creates a dynamic detour method of the specified delegate type to wrap a base game
/// method with the specified name. The dynamic method will automatically adapt if
/// optional parameters are added, filling in their default values.
///
/// This overload creates a lazy detour that only performs the expensive reflection
/// when it is first used.
///
/// The delegate type to be used to call the detour.
/// The target type.
/// The method name.
/// The detour that will call that method.
/// If the delegate does not match any valid target method.
public static DetouredMethod DetourLazy(this Type type, string name)
where D : Delegate {
if (type == null)
throw new ArgumentNullException(nameof(type));
if (string.IsNullOrEmpty(name))
throw new ArgumentNullException(nameof(name));
return new DetouredMethod(type, name);
}
///
/// Creates a dynamic detour method of the specified delegate type to wrap a base game
/// method with the specified name. The dynamic method will automatically adapt if
/// optional parameters are added, filling in their default values.
///
/// The delegate type to be used to call the detour.
/// The target method to be called.
/// The detour that will call that method.
/// If the delegate does not match the target.
public static D Detour(this MethodInfo target) where D : Delegate {
if (target == null)
throw new ArgumentNullException(nameof(target));
if (target.ContainsGenericParameters)
throw new ArgumentException("Generic types must have all parameters defined");
var expected = DelegateInfo.Create(typeof(D));
var parentType = target.DeclaringType;
var expectedParamTypes = expected.parameterTypes;
var actualParams = ValidateDelegate(expected, target, target.ReturnType);
int offset = target.IsStatic ? 0 : 1;
if (parentType == null)
throw new ArgumentException("Method is not declared by an actual type");
// Method will be "declared" in the type of the target, as we are detouring around
// a method of that type
var caller = new DynamicMethod(target.Name + "_Detour", expected.returnType,
expectedParamTypes, parentType, true);
var generator = caller.GetILGenerator();
LoadParameters(generator, actualParams, expectedParamTypes, offset);
if (parentType.IsValueType || target.IsStatic)
generator.Emit(OpCodes.Call, target);
else
generator.Emit(OpCodes.Callvirt, target);
generator.Emit(OpCodes.Ret);
FinishDynamicMethod(caller, actualParams, expectedParamTypes, offset);
#if DEBUG
PUtil.LogDebug("Created delegate {0} for method {1}.{2} with parameters [{3}]".
F(caller.Name, parentType.FullName, target.Name, actualParams.Join(",")));
#endif
return caller.CreateDelegate(typeof(D)) as D;
}
///
/// Creates a dynamic detour method of the specified delegate type to wrap a base game
/// constructor. The dynamic method will automatically adapt if optional parameters
/// are added, filling in their default values.
///
/// The delegate type to be used to call the detour.
/// The target constructor to be called.
/// The detour that will call that constructor.
/// If the delegate does not match the target.
public static D Detour(this ConstructorInfo target) where D : Delegate {
if (target == null)
throw new ArgumentNullException(nameof(target));
if (target.ContainsGenericParameters)
throw new ArgumentException("Generic types must have all parameters defined");
if (target.IsStatic)
throw new ArgumentException("Static constructors cannot be called manually");
var expected = DelegateInfo.Create(typeof(D));
var parentType = target.DeclaringType;
var expectedParamTypes = expected.parameterTypes;
var actualParams = ValidateDelegate(expected, target, parentType);
if (parentType == null)
throw new ArgumentException("Method is not declared by an actual type");
// Method will be "declared" in the type of the target, as we are detouring around
// a constructor of that type
var caller = new DynamicMethod("Constructor_Detour", expected.returnType,
expectedParamTypes, parentType, true);
var generator = caller.GetILGenerator();
LoadParameters(generator, actualParams, expectedParamTypes, 0);
generator.Emit(OpCodes.Newobj, target);
generator.Emit(OpCodes.Ret);
FinishDynamicMethod(caller, actualParams, expectedParamTypes, 0);
#if DEBUG
PUtil.LogDebug("Created delegate {0} for constructor {1} with parameters [{2}]".
F(caller.Name, parentType.FullName, actualParams.Join(",")));
#endif
return caller.CreateDelegate(typeof(D)) as D;
}
///
/// Creates dynamic detour methods to wrap a base game field or property with the
/// specified name. The detour will still work even if the field is converted to a
/// source compatible property and vice versa.
///
/// The type of the parent class.
/// The type of the field or property element.
/// The name of the field or property to be accessed.
/// A detour element that wraps the field or property with common getter and
/// setter delegates which will work on both types.
public static IDetouredField
DetourField
(string name) {
if (string.IsNullOrEmpty(name))
throw new ArgumentNullException(nameof(name));
var pt = typeof(P);
var field = pt.GetField(name, PPatchTools.BASE_FLAGS | BindingFlags.
Static | BindingFlags.Instance);
IDetouredField
d;
if (field == null) {
try {
var property = pt.GetProperty(name, PPatchTools.BASE_FLAGS |
BindingFlags.Static | BindingFlags.Instance);
if (property == null)
throw new DetourException("Unable to find {0} on type {1}".
F(name, typeof(P).FullName));
d = DetourProperty
(property);
} catch (AmbiguousMatchException) {
throw new DetourException("Unable to find {0} on type {1}".
F(name, typeof(P).FullName));
}
} else {
if (pt.IsValueType || (pt.IsByRef && (pt.GetElementType()?.IsValueType ??
false)))
throw new ArgumentException("For accessing struct fields, use DetourStructField");
d = DetourField
(field);
}
return d;
}
///
/// Creates dynamic detour methods to wrap a base game field or property with the
/// specified name. The detour will still work even if the field is converted to a
/// source compatible property and vice versa.
///
/// This overload creates a lazy detour that only performs the expensive reflection
/// when it is first used.
///
/// The type of the parent class.
/// The type of the field or property element.
/// The name of the field or property to be accessed.
/// A detour element that wraps the field or property with common getter and
/// setter delegates which will work on both types.
public static IDetouredField
DetourFieldLazy
(string name) {
if (string.IsNullOrEmpty(name))
throw new ArgumentNullException(nameof(name));
return new LazyDetouredField
(typeof(P), name);
}
///
/// Creates dynamic detour methods to wrap a base game field with the specified name.
///
/// The type of the parent class.
/// The type of the field element.
/// The field which will be accessed.
/// A detour element that wraps the field with a common interface matching
/// that of a detoured property.
private static IDetouredField
DetourField
(FieldInfo target) {
if (target == null)
throw new ArgumentNullException(nameof(target));
var parentType = target.DeclaringType;
string name = target.Name;
if (parentType != typeof(P))
throw new ArgumentException("Parent type does not match delegate to be created");
var getter = new DynamicMethod(name + "_Detour_Get", typeof(T), new[] {
typeof(P)
}, true);
var generator = getter.GetILGenerator();
// Getter will load the first argument and use ldfld/ldsfld
if (target.IsStatic)
generator.Emit(OpCodes.Ldsfld, target);
else {
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Ldfld, target);
}
generator.Emit(OpCodes.Ret);
DynamicMethod setter;
if (target.IsInitOnly)
// Handle readonly fields
setter = null;
else {
setter = new DynamicMethod(name + "_Detour_Set", null, new[] {
typeof(P), typeof(T)
}, true);
generator = setter.GetILGenerator();
// Setter will load both arguments and use stfld/stsfld (argument 1 is ignored
// for static fields)
if (target.IsStatic) {
generator.Emit(OpCodes.Ldarg_1);
generator.Emit(OpCodes.Stsfld, target);
} else {
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Ldarg_1);
generator.Emit(OpCodes.Stfld, target);
}
generator.Emit(OpCodes.Ret);
}
#if DEBUG
PUtil.LogDebug("Created delegate for field {0}.{1} with type {2}".
F(parentType.FullName, target.Name, typeof(T).FullName));
#endif
return new DetouredField
(name, getter.CreateDelegate(typeof(Func
)) as
Func
, setter?.CreateDelegate(typeof(Action
)) as Action
);
}
///
/// Creates dynamic detour methods to wrap a base game property with the specified name.
///
/// The type of the parent class.
/// The type of the property element.
/// The property which will be accessed.
/// A detour element that wraps the property with a common interface matching
/// that of a detoured field.
/// If the property has indexers.
private static IDetouredField
DetourProperty
(PropertyInfo target) {
if (target == null)
throw new ArgumentNullException(nameof(target));
var parentType = target.DeclaringType;
string name = target.Name;
if (parentType != typeof(P))
throw new ArgumentException("Parent type does not match delegate to be created");
var indexes = target.GetIndexParameters();
if (indexes != null && indexes.Length > 0)
throw new DetourException("Cannot detour on properties with index arguments");
DynamicMethod getter, setter;
var getMethod = target.GetGetMethod(true);
if (target.CanRead && getMethod != null) {
getter = new DynamicMethod(name + "_Detour_Get", typeof(T), new[] {
typeof(P)
}, true);
var generator = getter.GetILGenerator();
// Getter will load the first argument and call the property getter
if (!getMethod.IsStatic)
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Call, getMethod);
generator.Emit(OpCodes.Ret);
} else
getter = null;
var setMethod = target.GetSetMethod(true);
if (target.CanWrite && setMethod != null) {
setter = new DynamicMethod(name + "_Detour_Set", null, new[] {
typeof(P), typeof(T)
}, true);
var generator = setter.GetILGenerator();
// Setter will load both arguments and call property setter (argument 1 is
// ignored for static properties)
if (!setMethod.IsStatic)
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Ldarg_1);
generator.Emit(OpCodes.Call, setMethod);
generator.Emit(OpCodes.Ret);
} else
setter = null;
#if DEBUG
PUtil.LogDebug("Created delegate for property {0}.{1} with type {2}".
F(parentType.FullName, target.Name, typeof(T).FullName));
#endif
return new DetouredField
(name, getter?.CreateDelegate(typeof(Func
)) as
Func
, setter?.CreateDelegate(typeof(Action
)) as Action
);
}
///
/// Creates dynamic detour methods to wrap a base game struct field with the specified
/// name. For static struct fields, use the regular DetourField.
///
/// The type of the field element.
/// The struct type which will be accessed.
/// The name of the struct field to be accessed.
/// A detour element that wraps the field with a common interface matching
/// that of a detoured property.
public static IDetouredField