/* * 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; namespace PeterHan.PLib.PatchManager { /// /// Manages patches that PLib will conditionally apply. /// public sealed class PPatchManager : PForwardedComponent { /// /// The base flags to use when matching instance or static methods. /// internal const BindingFlags FLAGS = PPatchTools.BASE_FLAGS | BindingFlags.DeclaredOnly; /// /// The flags to use when matching instance and static methods. /// internal const BindingFlags FLAGS_EITHER = FLAGS | BindingFlags.Static | BindingFlags. Instance; /// /// The version of this component. Uses the running PLib version. /// internal static readonly Version VERSION = new Version(PVersion.VERSION); /// /// The instantiated copy of this class. /// internal static PPatchManager Instance { get; private set; } /// /// true if the AfterModsLoad patches have been run, or false otherwise. /// 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; /// /// The Harmony instance to use for patching. /// private readonly Harmony harmony; /// /// Patches and delegates to be run at specific points in the runtime. Put the kibosh /// on patching Db.Initialize()! /// private readonly IDictionary patches; /// /// Checks to see if the conditions for a method running are met. /// /// The assembly name that must be present, or null if none is required. /// The type full name that must be present, or null if none is required. /// The type that was required, if typeName was not null or empty. /// true if the requirements are met, or false otherwise. 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; } /// /// Creates a patch manager to execute patches at specific times. /// /// Create this instance in OnLoad() and use RegisterPatchClass to register a /// patch class. /// /// The Harmony instance to use for patching. 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(8); InstanceData = patches; } /// /// Schedules a patch method instance to be run. /// /// When to run the patch. /// The patch method instance to run. /// The Harmony instance to use for patching. private void AddHandler(uint when, IPatchMethodInstance instance) { if (!patches.TryGetValue(when, out PrivateRunList atTime)) patches.Add(when, atTime = new List(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); } } } /// /// Registers a single patch to be run by Patch Manager. Obviously, the patch must be /// registered before the time that it is used. /// /// The time when the method should be run. /// The patch to execute. 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); } /// /// 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. /// /// The type to register. /// The Harmony instance to use for immediate patches. Use /// the instance provided from UserMod2.OnLoad(). 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!"); } } }