/// <summary> /// Adds a logger to all failed assertions. The assertions will still fail, but a stack /// trace will be printed for each failed assertion. /// </summary> public static void LogAllFailedAsserts() { var inst = HarmonyInstance.Create("PeterHan.PLib.LogFailedAsserts"); MethodBase assert; var handler = new HarmonyMethod(typeof(PPatchTools), nameof(OnAssertFailed)); // This is not for production use PUtil.LogWarning("PLib in mod " + Assembly.GetCallingAssembly().GetName()?.Name + " is logging ALL failed assertions!"); try { // Assert(bool) assert = GetMethodSafe(typeof(Debug), "Assert", true, typeof(bool)); if (assert != null) { inst.Patch(assert, handler); } // Assert(bool, object) assert = GetMethodSafe(typeof(Debug), "Assert", true, typeof(bool), typeof( object)); if (assert != null) { inst.Patch(assert, handler); } // Assert(bool, object, UnityEngine.Object) assert = GetMethodSafe(typeof(Debug), "Assert", true, typeof(bool), typeof( object), typeof(UnityEngine.Object)); if (assert != null) { inst.Patch(assert, handler); } } catch (Exception e) { PUtil.LogException(e); } }
/// <summary> /// Adds a logger to all unhandled exceptions. /// </summary> public static void LogAllExceptions() { // This is not for production use PUtil.LogWarning("PLib in mod " + Assembly.GetCallingAssembly().GetName()?.Name + " is logging ALL unhandled exceptions!"); AppDomain.CurrentDomain.UnhandledException += OnThrown; }
/// <summary> /// Patches a method manually with a transpiler. /// </summary> /// <param name="instance">The Harmony instance.</param> /// <param name="type">The class to modify.</param> /// <param name="methodName">The method to patch.</param> /// <param name="transpiler">The transpiler to apply.</param> public static void PatchTranspile(this HarmonyInstance instance, Type type, string methodName, HarmonyMethod transpiler) { if (type == null) { throw new ArgumentNullException("type"); } if (string.IsNullOrEmpty(methodName)) { throw new ArgumentNullException("method"); } // Fetch the method try { var method = type.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags. Public | BindingFlags.Static | BindingFlags.Instance); if (method != null) { instance.Patch(method, null, null, transpiler); } else { PUtil.LogWarning("Unable to find method {0} on type {1}".F(methodName, type.FullName)); } } catch (AmbiguousMatchException e) { PUtil.LogException(e); } catch (FormatException e) { PUtil.LogWarning("Unable to transpile method {0}: {1}".F(methodName, e.Message)); } }
/// <summary> /// Registers a PAction with the action manager. There is no corresponding Unregister /// call, so avoid spamming PActions. /// /// This call should occur after PUtil.InitLibrary() during the mod OnLoad(). If called /// earlier, it may fail with InvalidOperationException, and if called later, the /// user's custom key bind (if applicable) will be discarded. /// </summary> /// <param name="identifier">The identifier for this action.</param> /// <param name="title">The action's title. To localize the action's title, use the /// UpdateLocalizedTitle method on the PAction instance in a postload patch (which /// runs after localization mods have been loaded)</param> /// <param name="binding">The default key binding for this action.</param> /// <returns>The action thus registered.</returns> /// <exception cref="InvalidOperationException">If PLib is not yet initialized.</exception> public static PAction Register(string identifier, LocString title, PKeyBinding binding = null) { // In case this call is used before the library was initialized if (!PUtil.PLibInit) { PUtil.InitLibrary(false); PUtil.LogWarning("PUtil.InitLibrary was not called before using " + "PAction.Register!"); } int actionID; PAction action; // Avoid any future threading issues lock (PSharedData.GetLock(PRegistry.KEY_ACTION_LOCK)) { actionID = PSharedData.GetData <int>(PRegistry.KEY_ACTION_ID); if (actionID <= 0) { throw new InvalidOperationException("PAction action ID is not set!"); } PSharedData.PutData(PRegistry.KEY_ACTION_ID, actionID + 1); } action = new PAction(actionID, identifier, title); if (!string.IsNullOrEmpty(title)) { PActionManager.ConfigureTitle(action); } action.AddKeyBinding(binding ?? new PKeyBinding()); return(action); }
/// <summary> /// Retrieves a type using its full name (including namespace). However, the assembly /// name is optional, as this method searches all assemblies in the current /// AppDomain if it is null or empty. /// </summary> /// <param name="name">The type name to retrieve.</param> /// <param name="assemblyName">If specified, the name of the assembly that contains /// the type. No other assembly name will be searched if this parameter is not null /// or empty. The assembly name might not match the DLL name, use a decompiler to /// make sure.</param> /// <returns>The type, or null if the type is not found or cannot be loaded.</returns> public static Type GetTypeSafe(string name, string assemblyName = null) { Type type = null; if (string.IsNullOrEmpty(assemblyName)) { foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) { try { type = assembly.GetType(name, false); } catch (System.IO.IOException) { // The common parent of exceptions when the type requires another type // that cannot be loaded } catch (BadImageFormatException) { } if (type != null) { break; } } } else { try { type = Type.GetType(name + ", " + assemblyName, false); } catch (TargetInvocationException e) { PUtil.LogWarning("Unable to load type {0} from assembly {1}:".F(name, assemblyName)); PUtil.LogExcWarn(e); } catch (ArgumentException e) { // A generic type is loaded with bad arguments PUtil.LogWarning("Unable to load type {0} from assembly {1}:".F(name, assemblyName)); PUtil.LogExcWarn(e); } catch (System.IO.IOException) { // The common parent of exceptions when the type requires another type that // cannot be loaded } catch (BadImageFormatException) { } } return(type); }
/// <summary> /// Patches a constructor manually. /// </summary> /// <param name="instance">The Harmony instance.</param> /// <param name="type">The class to modify.</param> /// <param name="arguments">The constructor's argument types.</param> /// <param name="prefix">The prefix to apply, or null if none.</param> /// <param name="postfix">The postfix to apply, or null if none.</param> public static void PatchConstructor(this HarmonyInstance instance, Type type, Type[] arguments, HarmonyMethod prefix = null, HarmonyMethod postfix = null) { if (type == null) { throw new ArgumentNullException("type"); } // Fetch the constructor try { var cons = type.GetConstructor(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance, null, arguments, null); if (cons != null) { instance.Patch(cons, prefix, postfix); } else { PUtil.LogWarning("Unable to find constructor on type {0}".F(type. FullName)); } } catch (ArgumentException e) { PUtil.LogException(e); } }
/// <summary> /// Applies all patches. /// </summary> /// <param name="instance">The Harmony instance to use when patching.</param> private static void PatchAll(HarmonyInstance instance) { if (instance == null) { throw new ArgumentNullException("instance"); } // ColonyAchievementStatus instance.Patch(typeof(ColonyAchievementStatus), "Serialize", PatchMethod(nameof(Serialize_Prefix)), null); // Db instance.Patch(typeof(Db), "Initialize", PatchMethod(nameof(Initialize_Prefix)), PatchMethod(nameof(Initialize_Postfix))); // Game instance.Patch(typeof(Game), "DestroyInstances", null, PatchMethod(nameof( Game_DestroyInstances_Postfix))); instance.Patch(typeof(Game), "OnPrefabInit", null, PatchMethod(nameof( Game_OnPrefabInit_Postfix))); // GameInputMapping instance.Patch(typeof(GameInputMapping), "SetDefaultKeyBindings", null, PatchMethod(nameof(SetDefaultKeyBindings_Postfix))); // GameUtil instance.Patch(typeof(GameUtil), "GetKeycodeLocalized", PatchMethod(nameof(GetKeycodeLocalized_Prefix)), null); // KInputController instance.PatchConstructor(typeof(KInputController.KeyDef), new Type[] { typeof(KKeyCode), typeof(Modifier) }, null, PatchMethod(nameof(CKeyDef_Postfix))); instance.Patch(typeof(KInputController), "IsActive", PatchMethod(nameof(IsActive_Prefix)), null); instance.Patch(typeof(KInputController), "QueueButtonEvent", PatchMethod(nameof(QueueButtonEvent_Prefix)), null); if (PLightManager.InitInstance()) { // DiscreteShadowCaster instance.Patch(typeof(DiscreteShadowCaster), "GetVisibleCells", PatchMethod(nameof(GetVisibleCells_Prefix)), null); // Light2D instance.Patch(typeof(Light2D), "AddToScenePartitioner", PatchMethod(nameof(AddToScenePartitioner_Prefix)), null); instance.Patch(typeof(Light2D), "RefreshShapeAndPosition", null, PatchMethod(nameof(RefreshShapeAndPosition_Postfix))); // LightGridEmitter instance.Patch(typeof(LightGridEmitter), "AddToGrid", null, PatchMethod(nameof(AddToGrid_Postfix))); instance.Patch(typeof(LightGridEmitter), "ComputeLux", PatchMethod(nameof(ComputeLux_Prefix)), null); instance.Patch(typeof(LightGridEmitter), "RemoveFromGrid", null, PatchMethod(nameof(RemoveFromGrid_Postfix))); instance.Patch(typeof(LightGridEmitter), "UpdateLitCells", PatchMethod(nameof(UpdateLitCells_Prefix)), null); // LightGridManager instance.Patch(typeof(LightGridManager), "CreatePreview", PatchMethod(nameof(CreatePreview_Prefix)), null); // LightShapePreview instance.Patch(typeof(LightShapePreview), "Update", PatchMethod(nameof(LightShapePreview_Update_Prefix)), null); // Rotatable instance.Patch(typeof(Rotatable), "OrientVisualizer", null, PatchMethod(nameof(OrientVisualizer_Postfix))); } // MainMenu instance.Patch(typeof(MainMenu), "OnSpawn", null, PatchMethod( nameof(MainMenu_OnSpawn_Postfix))); // PBuilding instance.Patch(typeof(BuildingTemplates), "CreateBuildingDef", null, PatchMethod(nameof(CreateBuildingDef_Postfix))); instance.Patch(typeof(EquipmentTemplates), "CreateEquipmentDef", null, PatchMethod(nameof(CreateEquipmentDef_Postfix))); if (PBuilding.CheckBuildings()) { instance.Patch(typeof(GeneratedBuildings), "LoadGeneratedBuildings", PatchMethod(nameof(LoadGeneratedBuildings_Prefix)), null); } // PCodex instance.Patch(typeof(CodexCache), "CollectEntries", null, PatchMethod(nameof(CollectEntries_Postfix))); instance.Patch(typeof(CodexCache), "CollectSubEntries", null, PatchMethod(nameof(CollectSubEntries_Postfix))); // PLocalization var locale = Localization.GetLocale(); if (locale != null) { PLocalization.LocalizeAll(locale); } // ModsScreen POptions.Init(); instance.Patch(typeof(ModsScreen), "BuildDisplay", null, PatchMethod(nameof(BuildDisplay_Postfix))); // SteamUGCService var ugc = PPatchTools.GetTypeSafe("SteamUGCService", "Assembly-CSharp"); if (ugc != null) { try { instance.PatchTranspile(ugc, "LoadPreviewImage", PatchMethod(nameof( LoadPreviewImage_Transpile))); } catch (Exception e) { PUtil.LogExcWarn(e); } } // TMPro.TMP_InputField try { instance.Patch(typeof(TMPro.TMP_InputField), "OnEnable", null, PatchMethod(nameof(OnEnable_Postfix))); } catch (Exception) { PUtil.LogWarning("Unable to patch TextMeshPro bug, text fields may display " + "improperly inside scroll areas"); } // Postload, legacy and normal PPatchManager.ExecuteLegacyPostload(); PPatchManager.RunAll(RunAt.AfterModsLoad); }
/// <summary> /// Transpiles a method to replace calls to the specified victim methods with /// replacement methods, altering the call type if necessary. /// /// Each key to value pair must meet the criteria defined in /// ReplaceMethodCall(TranspiledMethod, MethodInfo, MethodInfo). /// </summary> /// <param name="method">The method to patch.</param> /// <param name="translation">A mapping from the old method calls to replace, to the /// new method calls to use instead.</param> /// <returns>A transpiled version of that method that replaces or removes all calls /// to the specified methods.</returns> /// <exception cref="ArgumentException">If any of the new methods' argument types do /// not exactly match the old methods' argument types.</exception> public static TranspiledMethod ReplaceMethodCall(TranspiledMethod method, IDictionary <MethodInfo, MethodInfo> translation) { if (method == null) { throw new ArgumentNullException("method"); } if (translation == null) { throw new ArgumentNullException("translation"); } // Sanity check arguments int replaced = 0; foreach (var pair in translation) { var victim = pair.Key; var newMethod = pair.Value; if (victim == null) { throw new ArgumentNullException("victim"); } if (newMethod != null) { PTranspilerTools.CompareMethodParams(victim, victim.GetParameterTypes(), newMethod); } else if (victim.ReturnType != typeof(void)) { throw new ArgumentException("Cannot remove method {0} with a return value". F(victim.Name)); } } foreach (var instruction in method) { var opcode = instruction.opcode; MethodInfo target; if ((opcode == OpCodes.Call || opcode == OpCodes.Calli || opcode == OpCodes. Callvirt) && translation.TryGetValue(target = instruction.operand as MethodInfo, out MethodInfo newMethod)) { if (newMethod != null) { // Replace with new method instruction.opcode = newMethod.IsStatic ? OpCodes.Call : OpCodes.Callvirt; instruction.operand = newMethod; yield return(instruction); } else { // Pop "this" if needed int n = target.GetParameters().Length; if (!target.IsStatic) { n++; } // Pop the arguments off the stack instruction.opcode = (n == 0) ? OpCodes.Nop : OpCodes.Pop; instruction.operand = null; yield return(instruction); for (int i = 0; i < n - 1; i++) { yield return(new CodeInstruction(OpCodes.Pop)); } } replaced++; } else { yield return(instruction); } } #if DEBUG if (replaced == 0) { if (translation.Count == 1) { // Diagnose the method that could not be replaced var items = new KeyValuePair <MethodInfo, MethodInfo> [1]; translation.CopyTo(items, 0); MethodInfo from = items[0].Key, to = items[0].Value; PUtil.LogWarning("No method calls replaced: {0}.{1} to {2}.{3}".F( from.DeclaringType.FullName, from.Name, to.DeclaringType.FullName, to.Name)); } else { PUtil.LogWarning("No method calls replaced (multiple replacements)"); } } #endif }
public static void LogAllFailedAsserts() { PUtil.LogWarning("PLib in mod " + Assembly.GetCallingAssembly().GetName()?.Name + " is logging ALL failed assertions!"); PTranspilerTools.LogAllFailedAsserts(); }
public static void LogAllExceptions() { PUtil.LogWarning("PLib in mod " + Assembly.GetCallingAssembly().GetName()?.Name + " is logging ALL unhandled exceptions!"); PTranspilerTools.LogAllExceptions(); }
/// <summary> /// Transpiles a method to replace all calls to the specified victim method with /// another method, altering the call type if necessary. The argument types and return /// type must match exactly, including in/out/ref parameters. /// /// If replacing an instance method call with a static method, the first argument /// will receive the "this" which the old method would have received. /// /// If newMethod is null, the calls will all be removed silently instead. This will /// fail if the method call being removed had a return type (what would it be replaced /// with?); in those cases, declare an empty method with the same signature and /// replace it instead. /// </summary> /// <param name="method">The method to patch.</param> /// <param name="victim">The old method calls to remove.</param> /// <param name="newMethod">The new method to replace, or null to delete the calls.</param> /// <returns>A transpiled version of that method that replaces or removes all calls /// to method.</returns> /// <exception cref="ArgumentException">If the new method's argument types do not /// exactly match the old method's argument types.</exception> public static TranspiledMethod ReplaceMethodCall(TranspiledMethod method, MethodInfo victim, MethodInfo newMethod = null) { if (method == null) { throw new ArgumentNullException("method"); } if (victim == null) { throw new ArgumentNullException("victim"); } // Sanity check arguments var types = victim.GetParameterTypes(); int n = types.Length, replaced = 0; if (newMethod != null) { CompareMethodParams(victim, types, newMethod); } else if (victim.ReturnType != typeof(void)) { throw new ArgumentException("Cannot remove method {0} with a return value".F( victim.Name)); } // Pop "this" in removal cases if (!victim.IsStatic) { n++; } foreach (var instruction in method) { var opcode = instruction.opcode; if ((opcode == OpCodes.Call || opcode == OpCodes.Calli || opcode == OpCodes. Callvirt) && instruction.operand == victim) { if (newMethod != null) { // Replace with new method instruction.opcode = newMethod.IsStatic ? OpCodes.Call : OpCodes.Callvirt; instruction.operand = newMethod; yield return(instruction); } else { // Pop the arguments off the stack instruction.opcode = (n == 0) ? OpCodes.Nop : OpCodes.Pop; instruction.operand = null; yield return(instruction); for (int i = 0; i < n - 1; i++) { yield return(new CodeInstruction(OpCodes.Pop)); } } replaced++; } else { yield return(instruction); } } #if DEBUG if (replaced == 0) { PUtil.LogWarning("No method calls replaced: {0}.{1} to {2}.{3}".F(victim. DeclaringType.Name, victim.Name, newMethod?.DeclaringType?.Name ?? "None", newMethod?.Name)); } #endif }