/* * 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 { /// /// Represents a method that will be patched by PLib at a specific time to allow /// conditional integration with other mods. /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] public sealed class PLibPatchAttribute : Attribute, IPLibAnnotation { /// /// The required argument types. If null, any matching method name is patched, or an /// exception thrown if more than one matches. /// public Type[] ArgumentTypes { get; set; } /// /// If this flag is set, the patch will emit only at DEBUG level if the target method /// is not found or matches ambiguously. /// public bool IgnoreOnFail { get; set; } /// /// The name of the method to patch. /// public string MethodName { get; } /// /// The type of patch to apply through Harmony. /// public HarmonyPatchType PatchType { get; set; } /// /// 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...) /// public string RequireAssembly { get; set; } /// /// 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. /// public string RequireType { get; set; } /// /// When this method is run. /// public uint Runtime { get; } /// /// The type to patch. If null, the patcher will try to use the required type from the /// RequireType parameter. /// public Type TargetType { get; } /// /// 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. /// /// When to apply the patch. /// The type to patch. /// The method name to patch. 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)); } /// /// Patches a concrete type and overloaded method. /// /// Passing null as the method name will attempt to patch a constructor. /// /// When to apply the patch. /// The type to patch. /// The method name to patch. /// The types of the overload to patch. 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)); } /// /// 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. /// /// When to apply the patch. /// The method name to patch. public PLibPatchAttribute(uint runtime, string method) { ArgumentTypes = null; IgnoreOnFail = false; MethodName = method; PatchType = HarmonyPatchType.All; Runtime = runtime; TargetType = null; } /// /// 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. /// /// When to apply the patch. /// The method name to patch. /// The types of the overload to patch. public PLibPatchAttribute(uint runtime, string method, params Type[] argTypes) { ArgumentTypes = argTypes; IgnoreOnFail = false; MethodName = method; PatchType = HarmonyPatchType.All; Runtime = runtime; TargetType = null; } /// /// Creates a new patch method instance. /// /// The method that was attributed. /// An instance that can execute this patch. 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); } } /// /// Refers to a single instance of the annotation, with its annotated method. /// internal sealed class PLibPatchInstance : IPatchMethodInstance { /// /// The attribute describing the method. /// public PLibPatchAttribute Descriptor { get; } /// /// The method to run. /// public MethodInfo Method { get; } public PLibPatchInstance(PLibPatchAttribute attribute, MethodInfo method) { Descriptor = attribute ?? throw new ArgumentNullException(nameof(attribute)); Method = method ?? throw new ArgumentNullException(nameof(method)); } /// /// Calculates the patch type to perform. /// /// The type of Harmony patch to use for this method. 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; } /// /// Gets the specified instance constructor. /// /// The type to be constructed. /// The target constructor. /// If no parameter types were specified, /// and multiple declared constructors exist. 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; } /// /// Calculates the target method to patch. /// /// The type to use if no type was specified. /// The method to patch. /// If no parameter types were specified, /// and multiple options match the method name. /// If the target method was not found. 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; } /// /// Logs a message at debug level if Ignore On Patch Fail is enabled. /// /// The exception thrown during patching. /// true to suppress the exception, or false to rethrow it. 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; } /// /// Applies the patch. /// /// The Harmony instance to use. /// If the /// If no parameter types were specified, /// and multiple options match the method name. 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; } } } } }