313 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			313 lines
		
	
	
		
			12 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 PeterHan.PLib.Core;
 | |
| using System;
 | |
| using System.Reflection;
 | |
| 
 | |
| namespace PeterHan.PLib.PatchManager {
 | |
| 	/// <summary>
 | |
| 	/// Represents a method that will be patched by PLib at a specific time to allow
 | |
| 	/// conditional integration with other mods.
 | |
| 	/// </summary>
 | |
| 	[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
 | |
| 	public sealed class PLibPatchAttribute : Attribute, IPLibAnnotation {
 | |
| 		/// <summary>
 | |
| 		/// The required argument types. If null, any matching method name is patched, or an
 | |
| 		/// exception thrown if more than one matches.
 | |
| 		/// </summary>
 | |
| 		public Type[] ArgumentTypes { get; set; }
 | |
| 
 | |
| 		/// <summary>
 | |
| 		/// If this flag is set, the patch will emit only at DEBUG level if the target method
 | |
| 		/// is not found or matches ambiguously.
 | |
| 		/// </summary>
 | |
| 		public bool IgnoreOnFail { get; set; }
 | |
| 
 | |
| 		/// <summary>
 | |
| 		/// The name of the method to patch.
 | |
| 		/// </summary>
 | |
| 		public string MethodName { get; }
 | |
| 
 | |
| 		/// <summary>
 | |
| 		/// The type of patch to apply through Harmony.
 | |
| 		/// </summary>
 | |
| 		public HarmonyPatchType PatchType { get; set; }
 | |
| 
 | |
| 		/// <summary>
 | |
| 		/// Requires the specified assembly to be loaded for this method to run. If RequireType
 | |
| 		/// is null or empty, no particular types need to be defined in the assembly. The
 | |
| 		/// assembly name is required, but the version is optional (strong named assemblies
 | |
| 		/// can never load in ONI, since neither Unity nor Klei types are strong named...)
 | |
| 		/// </summary>
 | |
| 		public string RequireAssembly { get; set; }
 | |
| 
 | |
| 		/// <summary>
 | |
| 		/// Requires the specified type full name (not assembly qualified name) to exist for
 | |
| 		/// this method to run. If RequireAssembly is null or empty, a type in any assembly
 | |
| 		/// will satisfy the requirement.
 | |
| 		/// </summary>
 | |
| 		public string RequireType { get; set; }
 | |
| 
 | |
| 		/// <summary>
 | |
| 		/// When this method is run.
 | |
| 		/// </summary>
 | |
| 		public uint Runtime { get; }
 | |
| 		
 | |
| 		/// <summary>
 | |
| 		/// The type to patch. If null, the patcher will try to use the required type from the
 | |
| 		/// RequireType parameter.
 | |
| 		/// </summary>
 | |
| 		public Type TargetType { get; }
 | |
| 
 | |
| 		/// <summary>
 | |
| 		/// Patches a concrete type and method.
 | |
| 		/// 
 | |
| 		/// Passing null as the method name will attempt to patch a constructor. Only one
 | |
| 		/// declared constructor may be present, or the call will fail at patch time.
 | |
| 		/// </summary>
 | |
| 		/// <param name="runtime">When to apply the patch.</param>
 | |
| 		/// <param name="target">The type to patch.</param>
 | |
| 		/// <param name="method">The method name to patch.</param>
 | |
| 		public PLibPatchAttribute(uint runtime, Type target, string method) {
 | |
| 			ArgumentTypes = null;
 | |
| 			IgnoreOnFail = false;
 | |
| 			MethodName = method;
 | |
| 			PatchType = HarmonyPatchType.All;
 | |
| 			Runtime = runtime;
 | |
| 			TargetType = target ?? throw new ArgumentNullException(nameof(target));
 | |
| 		}
 | |
| 
 | |
| 		/// <summary>
 | |
| 		/// Patches a concrete type and overloaded method.
 | |
| 		/// 
 | |
| 		/// Passing null as the method name will attempt to patch a constructor.
 | |
| 		/// </summary>
 | |
| 		/// <param name="runtime">When to apply the patch.</param>
 | |
| 		/// <param name="target">The type to patch.</param>
 | |
| 		/// <param name="method">The method name to patch.</param>
 | |
| 		/// <param name="argTypes">The types of the overload to patch.</param>
 | |
| 		public PLibPatchAttribute(uint runtime, Type target, string method,
 | |
| 				params Type[] argTypes) {
 | |
| 			ArgumentTypes = argTypes;
 | |
| 			IgnoreOnFail = false;
 | |
| 			MethodName = method;
 | |
| 			PatchType = HarmonyPatchType.All;
 | |
| 			Runtime = runtime;
 | |
| 			TargetType = target ?? throw new ArgumentNullException(nameof(target));
 | |
| 		}
 | |
| 
 | |
| 		/// <summary>
 | |
| 		/// Patches a method only if a specified type is available. Use optional parameters to
 | |
| 		/// specify the type to patch using RequireType / RequireAssembly.
 | |
| 		/// 
 | |
| 		/// Passing null as the method name will attempt to patch a constructor. Only one
 | |
| 		/// declared constructor may be present, or the call will fail at patch time.
 | |
| 		/// </summary>
 | |
| 		/// <param name="runtime">When to apply the patch.</param>
 | |
| 		/// <param name="method">The method name to patch.</param>
 | |
| 		public PLibPatchAttribute(uint runtime, string method) {
 | |
| 			ArgumentTypes = null;
 | |
| 			IgnoreOnFail = false;
 | |
| 			MethodName = method;
 | |
| 			PatchType = HarmonyPatchType.All;
 | |
| 			Runtime = runtime;
 | |
| 			TargetType = null;
 | |
| 		}
 | |
| 
 | |
| 		/// <summary>
 | |
| 		/// Patches an overloaded method only if a specified type is available. Use optional
 | |
| 		/// parameters to specify the type to patch using RequireType / RequireAssembly.
 | |
| 		/// 
 | |
| 		/// Passing null as the method name will attempt to patch a constructor.
 | |
| 		/// </summary>
 | |
| 		/// <param name="runtime">When to apply the patch.</param>
 | |
| 		/// <param name="method">The method name to patch.</param>
 | |
| 		/// <param name="argTypes">The types of the overload to patch.</param>
 | |
| 		public PLibPatchAttribute(uint runtime, string method, params Type[] argTypes) {
 | |
| 			ArgumentTypes = argTypes;
 | |
| 			IgnoreOnFail = false;
 | |
| 			MethodName = method;
 | |
| 			PatchType = HarmonyPatchType.All;
 | |
| 			Runtime = runtime;
 | |
| 			TargetType = null;
 | |
| 		}
 | |
| 
 | |
| 		/// <summary>
 | |
| 		/// Creates a new patch method instance.
 | |
| 		/// </summary>
 | |
| 		/// <param name="method">The method that was attributed.</param>
 | |
| 		/// <returns>An instance that can execute this patch.</returns>
 | |
| 		public IPatchMethodInstance CreateInstance(MethodInfo method) {
 | |
| 			return new PLibPatchInstance(this, method);
 | |
| 		}
 | |
| 
 | |
| 		public override string ToString() {
 | |
| 			return "PLibPatch[RunAt={0},PatchType={1},MethodName={2}]".F(RunAt.ToString(
 | |
| 				Runtime), PatchType, MethodName);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/// <summary>
 | |
| 	/// Refers to a single instance of the annotation, with its annotated method.
 | |
| 	/// </summary>
 | |
| 	internal sealed class PLibPatchInstance : IPatchMethodInstance {
 | |
| 		/// <summary>
 | |
| 		/// The attribute describing the method.
 | |
| 		/// </summary>
 | |
| 		public PLibPatchAttribute Descriptor { get; }
 | |
| 
 | |
| 		/// <summary>
 | |
| 		/// The method to run.
 | |
| 		/// </summary>
 | |
| 		public MethodInfo Method { get; }
 | |
| 
 | |
| 		public PLibPatchInstance(PLibPatchAttribute attribute, MethodInfo method) {
 | |
| 			Descriptor = attribute ?? throw new ArgumentNullException(nameof(attribute));
 | |
| 			Method = method ?? throw new ArgumentNullException(nameof(method));
 | |
| 		}
 | |
| 
 | |
| 		/// <summary>
 | |
| 		/// Calculates the patch type to perform.
 | |
| 		/// </summary>
 | |
| 		/// <returns>The type of Harmony patch to use for this method.</returns>
 | |
| 		private HarmonyPatchType GetPatchType() {
 | |
| 			var patchType = Descriptor.PatchType;
 | |
| 			if (patchType == HarmonyPatchType.All) {
 | |
| 				// Auto-determine the patch type based on name, if possible
 | |
| 				string patchName = Method.Name;
 | |
| 				foreach (var value in Enum.GetValues(typeof(HarmonyPatchType)))
 | |
| 					if (value is HarmonyPatchType eval && eval != patchType && patchName.
 | |
| 							EndsWith(eval.ToString(), StringComparison.Ordinal)) {
 | |
| 						patchType = eval;
 | |
| 						break;
 | |
| 					}
 | |
| 			}
 | |
| 			return patchType;
 | |
| 		}
 | |
| 
 | |
| 		/// <summary>
 | |
| 		/// Gets the specified instance constructor.
 | |
| 		/// </summary>
 | |
| 		/// <param name="targetType">The type to be constructed.</param>
 | |
| 		/// <returns>The target constructor.</returns>
 | |
| 		/// <exception cref="AmbiguousMatchException">If no parameter types were specified,
 | |
| 		/// and multiple declared constructors exist.</exception>
 | |
| 		private MethodBase GetTargetConstructor(Type targetType, Type[] argumentTypes) {
 | |
| 			MethodBase constructor;
 | |
| 			if (argumentTypes == null) {
 | |
| 				var cons = targetType.GetConstructors(PPatchManager.FLAGS | BindingFlags.
 | |
| 					Instance);
 | |
| 				if (cons == null || cons.Length != 1)
 | |
| 					throw new InvalidOperationException("No constructor for {0} found".F(
 | |
| 						targetType.FullName));
 | |
| 				constructor = cons[0];
 | |
| 			} else
 | |
| 				constructor = targetType.GetConstructor(PPatchManager.FLAGS | BindingFlags.
 | |
| 					Instance, null, argumentTypes, null);
 | |
| 			return constructor;
 | |
| 		}
 | |
| 
 | |
| 		/// <summary>
 | |
| 		/// Calculates the target method to patch.
 | |
| 		/// </summary>
 | |
| 		/// <param name="requiredType">The type to use if no type was specified.</param>
 | |
| 		/// <returns>The method to patch.</returns>
 | |
| 		/// <exception cref="AmbiguousMatchException">If no parameter types were specified,
 | |
| 		/// and multiple options match the method name.</exception>
 | |
| 		/// <exception cref="InvalidOperationException">If the target method was not found.</exception>
 | |
| 		private MethodBase GetTargetMethod(Type requiredType) {
 | |
| 			var targetType = Descriptor.TargetType;
 | |
| 			var argumentTypes = Descriptor.ArgumentTypes;
 | |
| 			string name = Descriptor.MethodName;
 | |
| 			MethodBase method;
 | |
| 			if (targetType == null)
 | |
| 				targetType = requiredType;
 | |
| 			// Only allow non-inherited members, patching inherited members gets spooky on
 | |
| 			// Mac OS and Linux
 | |
| 			if (targetType == null)
 | |
| 				throw new InvalidOperationException("No type specified to patch");
 | |
| 			if (string.IsNullOrEmpty(name) || name == ".ctor")
 | |
| 				// Constructor
 | |
| 				method = GetTargetConstructor(targetType, argumentTypes);
 | |
| 			else
 | |
| 				// Method
 | |
| 				method = (argumentTypes == null) ? targetType.GetMethod(name, PPatchManager.
 | |
| 					FLAGS_EITHER) : targetType.GetMethod(name, PPatchManager.FLAGS_EITHER,
 | |
| 					null, argumentTypes, null);
 | |
| 			if (method == null)
 | |
| 				throw new InvalidOperationException("Method {0}.{1} not found".F(targetType.
 | |
| 					FullName, name));
 | |
| 			return method;
 | |
| 		}
 | |
| 
 | |
| 		/// <summary>
 | |
| 		/// Logs a message at debug level if Ignore On Patch Fail is enabled.
 | |
| 		/// </summary>
 | |
| 		/// <param name="e">The exception thrown during patching.</param>
 | |
| 		/// <returns>true to suppress the exception, or false to rethrow it.</returns>
 | |
| 		private bool LogIgnoreOnFail(Exception e) {
 | |
| 			bool ignore = Descriptor.IgnoreOnFail;
 | |
| 			if (ignore)
 | |
| 				PUtil.LogDebug("Patch for {0} not applied: {1}".F(Descriptor.
 | |
| 					MethodName, e.Message));
 | |
| 			return ignore;
 | |
| 		}
 | |
| 
 | |
| 		/// <summary>
 | |
| 		/// Applies the patch.
 | |
| 		/// </summary>
 | |
| 		/// <param name="instance">The Harmony instance to use.</param>
 | |
| 		/// <exception cref="InvalidOperationException">If the </exception>
 | |
| 		/// <exception cref="AmbiguousMatchException">If no parameter types were specified,
 | |
| 		/// and multiple options match the method name.</exception>
 | |
| 		public void Run(Harmony instance) {
 | |
| 			if (PPatchManager.CheckConditions(Descriptor.RequireAssembly, Descriptor.
 | |
| 					RequireType, out Type requiredType)) {
 | |
| 				var dest = new HarmonyMethod(Method);
 | |
| 				if (instance == null)
 | |
| 					throw new ArgumentNullException(nameof(instance));
 | |
| 				try {
 | |
| 					var method = GetTargetMethod(requiredType);
 | |
| 					switch (GetPatchType()) {
 | |
| 					case HarmonyPatchType.Postfix:
 | |
| 						instance.Patch(method, postfix: dest);
 | |
| 						break;
 | |
| 					case HarmonyPatchType.Prefix:
 | |
| 						instance.Patch(method, prefix: dest);
 | |
| 						break;
 | |
| 					case HarmonyPatchType.Transpiler:
 | |
| 						instance.Patch(method, transpiler: dest);
 | |
| 						break;
 | |
| 					default:
 | |
| 						throw new ArgumentOutOfRangeException(nameof(HarmonyPatchType));
 | |
| 					}
 | |
| 				} catch (AmbiguousMatchException e) {
 | |
| 					// Multi catch or filtering is not available in this version of C#
 | |
| 					if (!LogIgnoreOnFail(e))
 | |
| 						throw;
 | |
| 				} catch (InvalidOperationException e) {
 | |
| 					if (!LogIgnoreOnFail(e))
 | |
| 						throw;
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 |