269 lines
9.7 KiB
C#
269 lines
9.7 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.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!");
|
|
}
|
|
}
|
|
}
|