Initial commit

This commit is contained in:
2022-12-30 00:58:33 +01:00
commit 3f1075e67f
176 changed files with 25715 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
/*
* 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 System.Reflection;
namespace PeterHan.PLib.PatchManager {
/// <summary>
/// The commmon parent of [PLibPatch] and [PLibMethod].
/// </summary>
internal interface IPLibAnnotation {
/// <summary>
/// When this method is run.
/// </summary>
uint Runtime { get; }
/// <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>
IPatchMethodInstance CreateInstance(MethodInfo method);
}
}

View File

@@ -0,0 +1,33 @@
/*
* 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;
namespace PeterHan.PLib.PatchManager {
/// <summary>
/// Refers to a single instance of the annotation, with its annotated method.
/// </summary>
public interface IPatchMethodInstance {
/// <summary>
/// Runs the patch or method if the conditions are met. This method should check its
/// preconditions before executing the target.
/// </summary>
/// <param name="instance">The Harmony instance to use.</param>
void Run(Harmony instance);
}
}

View File

@@ -0,0 +1,115 @@
/*
* 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 run by PLib at a specific time to reduce the number
/// of patches required and allow conditional integration with other mods.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public sealed class PLibMethodAttribute : Attribute, IPLibAnnotation {
/// <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; }
public PLibMethodAttribute(uint runtime) {
Runtime = runtime;
}
/// <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 PLibMethodInstance(this, method);
}
public override string ToString() {
return "PLibMethod[RunAt={0}]".F(RunAt.ToString(Runtime));
}
}
/// <summary>
/// Refers to a single instance of the annotation, with its annotated method.
/// </summary>
internal sealed class PLibMethodInstance : IPatchMethodInstance {
/// <summary>
/// The attribute describing the method.
/// </summary>
public PLibMethodAttribute Descriptor { get; }
/// <summary>
/// The method to run.
/// </summary>
public MethodInfo Method { get; }
public PLibMethodInstance(PLibMethodAttribute attribute, MethodInfo method) {
Descriptor = attribute ?? throw new ArgumentNullException(nameof(attribute));
Method = method ?? throw new ArgumentNullException(nameof(method));
}
/// <summary>
/// Runs the method, passing the required parameters if any.
/// </summary>
/// <param name="instance">The Harmony instance to use if the method wants to
/// perform a patch.</param>
public void Run(Harmony instance) {
if (PPatchManager.CheckConditions(Descriptor.RequireAssembly, Descriptor.
RequireType, out Type requiredType)) {
// Only runs once, no meaningful savings with a delegate
var paramTypes = Method.GetParameterTypes();
int len = paramTypes.Length;
if (len <= 0)
// No parameters, static method only
Method.Invoke(null, null);
else if (paramTypes[0] == typeof(Harmony)) {
if (len == 1)
// Harmony instance parameter
Method.Invoke(null, new object[] { instance });
else if (len == 2 && paramTypes[1] == typeof(Type))
// Type parameter
Method.Invoke(null, new object[] { instance, requiredType });
} else
PUtil.LogWarning("Invalid signature for PLibMethod - must have (), " +
"(HarmonyInstance), or (HarmonyInstance, Type)");
}
}
}
}

View File

@@ -0,0 +1,312 @@
/*
* 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;
}
}
}
}
}

View File

@@ -0,0 +1,268 @@
/*
* 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.Collections.Generic;
using System.Reflection;
using PrivateRunList = System.Collections.Generic.ICollection<PeterHan.PLib.PatchManager.
IPatchMethodInstance>;
namespace PeterHan.PLib.PatchManager {
/// <summary>
/// Manages patches that PLib will conditionally apply.
/// </summary>
public sealed class PPatchManager : PForwardedComponent {
/// <summary>
/// The base flags to use when matching instance or static methods.
/// </summary>
internal const BindingFlags FLAGS = PPatchTools.BASE_FLAGS | BindingFlags.DeclaredOnly;
/// <summary>
/// The flags to use when matching instance and static methods.
/// </summary>
internal const BindingFlags FLAGS_EITHER = FLAGS | BindingFlags.Static | BindingFlags.
Instance;
/// <summary>
/// The version of this component. Uses the running PLib version.
/// </summary>
internal static readonly Version VERSION = new Version(PVersion.VERSION);
/// <summary>
/// The instantiated copy of this class.
/// </summary>
internal static PPatchManager Instance { get; private set; }
/// <summary>
/// true if the AfterModsLoad patches have been run, or false otherwise.
/// </summary>
private static volatile bool afterModsLoaded = false;
private static void Game_DestroyInstances_Postfix() {
Instance?.InvokeAllProcess(RunAt.OnEndGame, null);
}
private static void Game_OnPrefabInit_Postfix() {
Instance?.InvokeAllProcess(RunAt.OnStartGame, null);
}
private static void Initialize_Prefix() {
Instance?.InvokeAllProcess(RunAt.BeforeDbInit, null);
}
private static void Initialize_Postfix() {
Instance?.InvokeAllProcess(RunAt.AfterDbInit, null);
}
private static void Instance_Postfix() {
bool load = false;
if (Instance != null)
lock (VERSION) {
if (!afterModsLoaded)
load = afterModsLoaded = true;
}
if (load)
Instance.InvokeAllProcess(RunAt.AfterLayerableLoad, null);
}
private static void MainMenu_OnSpawn_Postfix() {
Instance?.InvokeAllProcess(RunAt.InMainMenu, null);
}
public override Version Version => VERSION;
/// <summary>
/// The Harmony instance to use for patching.
/// </summary>
private readonly Harmony harmony;
/// <summary>
/// Patches and delegates to be run at specific points in the runtime. Put the kibosh
/// on patching Db.Initialize()!
/// </summary>
private readonly IDictionary<uint, PrivateRunList> patches;
/// <summary>
/// Checks to see if the conditions for a method running are met.
/// </summary>
/// <param name="assemblyName">The assembly name that must be present, or null if none is required.</param>
/// <param name="typeName">The type full name that must be present, or null if none is required.</param>
/// <param name="requiredType">The type that was required, if typeName was not null or empty.</param>
/// <returns>true if the requirements are met, or false otherwise.</returns>
internal static bool CheckConditions(string assemblyName, string typeName,
out Type requiredType) {
bool ok = false, emptyType = string.IsNullOrEmpty(typeName);
if (string.IsNullOrEmpty(assemblyName)) {
if (emptyType) {
requiredType = null;
ok = true;
} else {
requiredType = PPatchTools.GetTypeSafe(typeName);
ok = requiredType != null;
}
} else if (emptyType) {
requiredType = null;
// Search for assembly only, by name
foreach (var candidate in AppDomain.CurrentDomain.GetAssemblies())
if (candidate.GetName().Name == assemblyName) {
ok = true;
break;
}
} else {
requiredType = PPatchTools.GetTypeSafe(typeName, assemblyName);
ok = requiredType != null;
}
return ok;
}
/// <summary>
/// Creates a patch manager to execute patches at specific times.
///
/// Create this instance in OnLoad() and use RegisterPatchClass to register a
/// patch class.
/// </summary>
/// <param name="harmony">The Harmony instance to use for patching.</param>
public PPatchManager(Harmony harmony) {
if (harmony == null) {
PUtil.LogWarning("Use the Harmony instance from OnLoad to create PPatchManager");
harmony = new Harmony("PLib.PostLoad." + Assembly.GetExecutingAssembly().
GetNameSafe());
}
this.harmony = harmony;
patches = new Dictionary<uint, PrivateRunList>(8);
InstanceData = patches;
}
/// <summary>
/// Schedules a patch method instance to be run.
/// </summary>
/// <param name="when">When to run the patch.</param>
/// <param name="instance">The patch method instance to run.</param>
/// <param name="harmony">The Harmony instance to use for patching.</param>
private void AddHandler(uint when, IPatchMethodInstance instance) {
if (!patches.TryGetValue(when, out PrivateRunList atTime))
patches.Add(when, atTime = new List<IPatchMethodInstance>(16));
atTime.Add(instance);
}
public override void Initialize(Harmony plibInstance) {
Instance = this;
// Db
plibInstance.Patch(typeof(Db), nameof(Db.Initialize), prefix: PatchMethod(nameof(
Initialize_Prefix)), postfix: PatchMethod(nameof(Initialize_Postfix)));
// Game
plibInstance.Patch(typeof(Game), "DestroyInstances", postfix: PatchMethod(nameof(
Game_DestroyInstances_Postfix)));
plibInstance.Patch(typeof(Game), "OnPrefabInit", postfix: PatchMethod(nameof(
Game_OnPrefabInit_Postfix)));
// GlobalResources
plibInstance.Patch(typeof(GlobalResources), "Instance", postfix:
PatchMethod(nameof(Instance_Postfix)));
// MainMenu
plibInstance.Patch(typeof(MainMenu), "OnSpawn", postfix: PatchMethod(
nameof(MainMenu_OnSpawn_Postfix)));
}
public override void PostInitialize(Harmony plibInstance) {
InvokeAllProcess(RunAt.AfterModsLoad, null);
}
public override void Process(uint when, object _) {
if (patches.TryGetValue(when, out PrivateRunList atTime) && atTime != null &&
atTime.Count > 0) {
string stage = RunAt.ToString(when);
#if DEBUG
PRegistry.LogPatchDebug("Executing {0:D} handler(s) from {1} for stage {2}".F(
atTime.Count, Assembly.GetExecutingAssembly().GetNameSafe() ?? "?", stage));
#endif
foreach (var patch in atTime)
try {
patch.Run(harmony);
} catch (TargetInvocationException e) {
// Use the inner exception
PUtil.LogError("Error running patches for stage " + stage + ":");
PUtil.LogException(e.GetBaseException());
} catch (Exception e) {
// Say which mod's postload crashed
PUtil.LogError("Error running patches for stage " + stage + ":");
PUtil.LogException(e);
}
}
}
/// <summary>
/// Registers a single patch to be run by Patch Manager. Obviously, the patch must be
/// registered before the time that it is used.
/// </summary>
/// <param name="when">The time when the method should be run.</param>
/// <param name="patch">The patch to execute.</param>
public void RegisterPatch(uint when, IPatchMethodInstance patch) {
RegisterForForwarding();
if (patch == null)
throw new ArgumentNullException(nameof(patch));
if (when == RunAt.Immediately)
// Now now now!
patch.Run(harmony);
else
AddHandler(when, patch);
}
/// <summary>
/// Registers a class containing methods for [PLibPatch] and [PLibMethod] handlers.
/// All methods, public and private, of the type will be searched for annotations.
/// However, nested and derived types will not be searched, nor will inherited methods.
///
/// This method cannot be used to register a class from another mod, as the annotations
/// on those methods would have a different assembly qualified name and would thus
/// not be recognized.
/// </summary>
/// <param name="type">The type to register.</param>
/// <param name="harmony">The Harmony instance to use for immediate patches. Use
/// the instance provided from UserMod2.OnLoad().</param>
public void RegisterPatchClass(Type type) {
int count = 0;
if (type == null)
throw new ArgumentNullException(nameof(type));
RegisterForForwarding();
foreach (var method in type.GetMethods(FLAGS | BindingFlags.Static))
foreach (var attrib in method.GetCustomAttributes(true))
if (attrib is IPLibAnnotation pm) {
var when = pm.Runtime;
var instance = pm.CreateInstance(method);
if (when == RunAt.Immediately)
// Now now now!
instance.Run(harmony);
else
AddHandler(pm.Runtime, instance);
count++;
}
if (count > 0)
PRegistry.LogPatchDebug("Registered {0:D} handler(s) for {1}".F(count,
Assembly.GetCallingAssembly().GetNameSafe() ?? "?"));
else
PRegistry.LogPatchWarning("RegisterPatchClass could not find any handlers!");
}
}
}

View File

@@ -0,0 +1,93 @@
/*
* 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.
*/
namespace PeterHan.PLib.PatchManager {
/// <summary>
/// Describes when a PLibPatch or PLibMethod should be invoked.
///
/// Due to a bug in ILRepack an enum type in PLib cannot be used as a parameter for a
/// custom attribute. ILmerge does not have this bug.
/// </summary>
public static class RunAt {
/// <summary>
/// Runs the method/patch now.
///
/// Note that mods may load in any order and thus not all mods may be initialized at
/// this time.
/// </summary>
public const uint Immediately = 0U;
/// <summary>
/// Runs after all mods load, but before most other aspects of the game (including
/// Assets, Db, and so forth) are initialized. This will run before any other mod
/// has their UserMod2.AfterModsLoad executed. All PLib components will be initialized
/// by this point.
/// </summary>
public const uint AfterModsLoad = 1U;
/// <summary>
/// Runs immediately before Db.Initialize.
/// </summary>
public const uint BeforeDbInit = 2U;
/// <summary>
/// Runs immediately after Db.Initialize.
/// </summary>
public const uint AfterDbInit = 3U;
/// <summary>
/// Runs when the main menu has loaded.
/// </summary>
public const uint InMainMenu = 4U;
/// <summary>
/// Runs when Game.OnPrefabInit has completed.
/// </summary>
public const uint OnStartGame = 5U;
/// <summary>
/// Runs when Game.DestroyInstances is executed.
/// </summary>
public const uint OnEndGame = 6U;
/// <summary>
/// Runs after all mod data (including layerable files like world gen and codex/
/// elements) are loaded. This comes after all UserMod2.AfterModsLoad handlers execute.
/// All PLib components will be initialized by this point.
/// </summary>
public const uint AfterLayerableLoad = 7U;
/// <summary>
/// The string equivalents of each constant for debugging.
/// </summary>
private static readonly string[] STRING_VALUES = new string[] {
nameof(Immediately), nameof(AfterModsLoad), nameof(BeforeDbInit),
nameof(AfterDbInit), nameof(InMainMenu), nameof(OnStartGame), nameof(OnEndGame),
nameof(AfterLayerableLoad)
};
/// <summary>
/// Gets a human readable representation of a run time constant.
/// </summary>
/// <param name="runtime">The time when the patch should be run.</param>
public static string ToString(uint runtime) {
return (runtime < STRING_VALUES.Length) ? STRING_VALUES[runtime] : runtime.
ToString();
}
}
}