diff --git a/BepInEx.MelonLoader.Loader.sln b/BepInEx.MelonLoader.Loader.sln new file mode 100644 index 0000000..cc67765 --- /dev/null +++ b/BepInEx.MelonLoader.Loader.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30413.136 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BepInEx.MelonLoader.Loader", "BepInEx.MelonLoader.Loader\BepInEx.MelonLoader.Loader.csproj", "{7D64B55B-887B-4A70-A5FD-5E01B849B9BC}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7D64B55B-887B-4A70-A5FD-5E01B849B9BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7D64B55B-887B-4A70-A5FD-5E01B849B9BC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7D64B55B-887B-4A70-A5FD-5E01B849B9BC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7D64B55B-887B-4A70-A5FD-5E01B849B9BC}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {22610235-9D7E-4B20-B180-26071ACC246C} + EndGlobalSection +EndGlobal diff --git a/BepInEx.MelonLoader.Loader/BepInEx.MelonLoader.Loader.csproj b/BepInEx.MelonLoader.Loader/BepInEx.MelonLoader.Loader.csproj new file mode 100644 index 0000000..fc069e6 --- /dev/null +++ b/BepInEx.MelonLoader.Loader/BepInEx.MelonLoader.Loader.csproj @@ -0,0 +1,211 @@ + + + + + Debug + AnyCPU + {7D64B55B-887B-4A70-A5FD-5E01B849B9BC} + Library + Properties + BepInEx.MelonLoader.Loader + MelonLoader.ModHandler + v4.7.2 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + true + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + true + + + + ..\..\BepInEx\bin\il2cpp\0Harmony.dll + False + + + ..\..\BepInEx\bin\il2cpp\BepInEx.Core.dll + False + + + ..\..\BepInEx\bin\il2cpp\BepInEx.IL2CPP.dll + False + + + ..\Libs\Il2Cppmscorlib.dll + False + + + ..\Libs\Il2CppSystem.dll + False + + + G:\SteamLibrary\steamapps\common\VRChat\BepInEx\core\MonoMod.RuntimeDetour.dll + False + + + G:\SteamLibrary\steamapps\common\VRChat\BepInEx\core\MonoMod.Utils.dll + False + + + + + + ..\..\BepInEx\bin\il2cpp\UnhollowerBaseLib.dll + False + + + ..\..\BepInEx\bin\il2cpp\UnhollowerRuntimeLib.dll + False + + + ..\Libs\UnityEngine.AssetBundleModule.dll + False + + + ..\Libs\UnityEngine.CoreModule.dll + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/BepInEx.MelonLoader.Loader/MLLoaderPlugin.cs b/BepInEx.MelonLoader.Loader/MLLoaderPlugin.cs new file mode 100644 index 0000000..908b4fb --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MLLoaderPlugin.cs @@ -0,0 +1,15 @@ +using BepInEx.IL2CPP; +using MelonLoader; + +namespace BepInEx.MelonLoader.Loader +{ + [BepInPlugin("io.bepis.mlloaderplugin", "MelonLoader Plugin Loader", "1.0")] + public class MLLoaderPlugin : BasePlugin + { + public override void Load() + { + MelonLoaderBase.Initialize(); + MelonLoaderBase.Startup(); + } + } +} \ No newline at end of file diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/BuildInfo.cs b/BepInEx.MelonLoader.Loader/MelonLoader/BuildInfo.cs new file mode 100644 index 0000000..fa0784d --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/BuildInfo.cs @@ -0,0 +1,11 @@ +namespace MelonLoader +{ + public static class BuildInfo + { + public const string Name = "BepInEx"; + public const string Description = "MelonLoader wrapper for BepInEx"; + public const string Author = "Bepis"; + public const string Company = "https://discord.gg/MpFEDAg"; + public const string Version = "6.0.0.0"; + } +} \ No newline at end of file diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/DependencyGraph.cs b/BepInEx.MelonLoader.Loader/MelonLoader/DependencyGraph.cs new file mode 100644 index 0000000..42de727 --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/DependencyGraph.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Reflection; +using System.IO; + +namespace MelonLoader { + internal class DependencyGraph where T : MelonBase { + + public static void TopologicalSort(IList modsOrPlugins, Func modNameGetter) { + DependencyGraph dependencyGraph = new DependencyGraph(modsOrPlugins, modNameGetter); + modsOrPlugins.Clear(); + dependencyGraph.TopologicalSortInto(modsOrPlugins); + } + + private readonly Vertex[] vertices; + + private DependencyGraph(IList mods, Func modNameGetter) { + int size = mods.Count; + vertices = new Vertex[size]; + IDictionary nameLookup = new Dictionary(size); + + // Create a vertex in the dependency graph for each mod to load + for (int i = 0; i < size; ++i) { + Assembly modAssembly = mods[i].Assembly; + string modName = modNameGetter(mods[i]); + + Vertex modVertex = new Vertex(i, mods[i], modName); + vertices[i] = modVertex; + nameLookup[modAssembly.GetName().Name] = modVertex; + } + + // Add an edge for each dependency between mods + IDictionary> modsWithMissingDeps = new SortedDictionary>(); + List missingDependencies = new List(); + HashSet optionalDependencies = new HashSet(); + + foreach (Vertex modVertex in vertices) { + Assembly modAssembly = modVertex.mod.Assembly; + missingDependencies.Clear(); + optionalDependencies.Clear(); + + MelonOptionalDependenciesAttribute optionals = (MelonOptionalDependenciesAttribute) Attribute.GetCustomAttribute(modAssembly, typeof(MelonOptionalDependenciesAttribute)); + if (optionals != null && optionals.AssemblyNames != null) { + optionalDependencies.UnionWith(optionals.AssemblyNames); + } + + foreach (AssemblyName dependency in modAssembly.GetReferencedAssemblies()) { + if (nameLookup.TryGetValue(dependency.Name, out Vertex dependencyVertex)) { + modVertex.dependencies.Add(dependencyVertex); + dependencyVertex.dependents.Add(modVertex); + } else if (!TryLoad(dependency) && !optionalDependencies.Contains(dependency.Name)) { + missingDependencies.Add(dependency); + } + } + + if (missingDependencies.Count > 0) { + // modVertex.skipLoading = true; + modsWithMissingDeps.Add(modNameGetter(modVertex.mod), missingDependencies.ToArray()); + } + } + + if (modsWithMissingDeps.Count > 0) { + // Some mods are missing dependencies. Don't load these mods and show an error message + MelonLogger.LogWarning(BuildMissingDependencyMessage(modsWithMissingDeps)); + } + } + + // Returns true if 'assembly' was already loaded or could be loaded, false if the required assembly was missing. + private static bool TryLoad(AssemblyName assembly) { + try { + Assembly.Load(assembly); + return true; + } catch (FileNotFoundException) { + return false; + } catch (Exception ex) { + MelonLogger.LogError("Loading mod dependency failed: " + ex); + return false; + } + } + + private static string BuildMissingDependencyMessage(IDictionary> modsWithMissingDeps) { + StringBuilder messageBuilder = new StringBuilder("Some mods are missing dependencies, which you may have to install.\n" + + "If these are optional dependencies, mark them as optional using the MelonOptionalDependencies attribute.\n" + + "This warning will turn into an error and mods with missing dependencies will not be loaded in the next version of MelonLoader.\n"); + foreach (string modName in modsWithMissingDeps.Keys) { + messageBuilder.Append($"- '{modName}' is missing the following dependencies:\n"); + foreach (AssemblyName dependency in modsWithMissingDeps[modName]) { + messageBuilder.Append($" - '{dependency.Name}' v{dependency.Version}\n"); + } + } + messageBuilder.Length -= 1; // Remove trailing newline + return messageBuilder.ToString(); + } + + private void TopologicalSortInto(IList loadedMods) { + int[] unloadedDependencies = new int[vertices.Length]; + SortedList loadableMods = new SortedList(); + int skippedMods = 0; + + // Find all sinks in the dependency graph, i.e. mods without any dependencies on other mods + for (int i = 0; i < vertices.Length; ++i) { + Vertex vertex = vertices[i]; + int dependencyCount = vertex.dependencies.Count; + + unloadedDependencies[i] = dependencyCount; + if (dependencyCount == 0) { + loadableMods.Add(vertex.name, vertex); + } + } + + // Perform the (reverse) topological sorting + while (loadableMods.Count > 0) { + Vertex mod = loadableMods.Values[0]; + loadableMods.RemoveAt(0); + + if (!mod.skipLoading) { + loadedMods.Add(mod.mod); + } else { + ++skippedMods; + } + + foreach (Vertex dependent in mod.dependents) { + unloadedDependencies[dependent.index] -= 1; + dependent.skipLoading |= mod.skipLoading; + + if (unloadedDependencies[dependent.index] == 0) { + loadableMods.Add(dependent.name, dependent); + } + } + } + + // Check if all mods were either loaded or skipped. If this is not the case, there is a cycle in the dependency graph + if (loadedMods.Count + skippedMods < vertices.Length) { + StringBuilder errorMessage = new StringBuilder("Some mods could not be loaded due to a cyclic dependency:\n"); + for (int i = 0; i < vertices.Length; ++i) { + if (unloadedDependencies[i] > 0) + errorMessage.Append($"- '{vertices[i].name}'\n"); + } + errorMessage.Length -= 1; // Remove trailing newline + MelonLogger.LogError(errorMessage.ToString()); + } + } + + private class Vertex { + + internal readonly int index; + internal readonly T mod; + internal readonly string name; + + internal readonly IList dependencies; + internal readonly IList dependents; + internal bool skipLoading; + + internal Vertex(int index, T mod, string name) { + this.index = index; + this.mod = mod; + this.name = name; + + dependencies = new List(); + dependents = new List(); + skipLoading = false; + } + } + } +} diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/Deprecated.cs b/BepInEx.MelonLoader.Loader/MelonLoader/Deprecated.cs new file mode 100644 index 0000000..fd436ac --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/Deprecated.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MelonLoader +{ + [Obsolete("Main is obsolete. Please use MelonLoaderBase or MelonHandler instead.")] + public static class Main + { + public static List Mods = null; + public static List Plugins = null; + public static bool IsVRChat = false; + public static bool IsBoneworks = false; + public static string GetUnityVersion() => MelonLoaderBase.UnityVersion; + public static string GetUserDataPath() => MelonLoaderBase.UserDataPath; + internal static void LegacySupport(List mods, List plugins, bool isVRChat, bool isBoneworks) + { + Mods = mods; + Plugins = plugins; + IsVRChat = isVRChat; + IsBoneworks = isBoneworks; + } + } + [Obsolete("MelonModGame is obsolete. Please use MelonGame instead.")] + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public class MelonModGameAttribute : Attribute + { + public string Developer { get; } + public string GameName { get; } + public MelonModGameAttribute(string developer = null, string gameName = null) + { + Developer = developer; + GameName = gameName; + } + internal MelonGameAttribute Convert() => new MelonGameAttribute(Developer, GameName); + } + [Obsolete("MelonModInfo is obsolete. Please use MelonInfo instead.")] + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)] + public class MelonModInfoAttribute : Attribute + { + public Type SystemType { get; } + public string Name { get; } + public string Version { get; } + public string Author { get; } + public string DownloadLink { get; } + + public MelonModInfoAttribute(Type type, string name, string version, string author, string downloadLink = null) + { + SystemType = type; + Name = name; + Version = version; + Author = author; + DownloadLink = downloadLink; + } + internal MelonInfoAttribute Convert() => new MelonInfoAttribute(SystemType, Name, Version, Author, DownloadLink); + } + [Obsolete("MelonPluginGame is obsolete. Please use MelonGame instead.")] + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public class MelonPluginGameAttribute : Attribute + { + public string Developer { get; } + public string GameName { get; } + public MelonPluginGameAttribute(string developer = null, string gameName = null) + { + Developer = developer; + GameName = gameName; + } + public MelonGameAttribute Convert() => new MelonGameAttribute(Developer, GameName); + } + [Obsolete("MelonPluginInfo is obsolete. Please use MelonInfo instead.")] + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)] + public class MelonPluginInfoAttribute : Attribute + { + public Type SystemType { get; } + public string Name { get; } + public string Version { get; } + public string Author { get; } + public string DownloadLink { get; } + + public MelonPluginInfoAttribute(Type type, string name, string version, string author, string downloadLink = null) + { + SystemType = type; + Name = name; + Version = version; + Author = author; + DownloadLink = downloadLink; + } + public MelonInfoAttribute Convert() => new MelonInfoAttribute(SystemType, Name, Version, Author, DownloadLink); + } + [Obsolete("MelonModLogger is obsolete. Please use MelonLogger instead.")] + public class MelonModLogger : MelonLogger {} + [Obsolete("ModPrefs is obsolete. Please use MelonPrefs instead.")] + public class ModPrefs : MelonPrefs + { + public static Dictionary> GetPrefs() + { + Dictionary> output = new Dictionary>(); + Dictionary> prefs = GetPreferences(); + for (int i = 0; i < prefs.Values.Count; i++) + { + Dictionary prefsdict = prefs.Values.ElementAt(i); + Dictionary newprefsdict = new Dictionary(); + for (int j = 0; j < prefsdict.Values.Count; j++) + { + MelonPreference pref = prefsdict.Values.ElementAt(j); + PrefDesc newpref = new PrefDesc(pref.Value, (PrefType)pref.Type, pref.Hidden, pref.DisplayText); + newpref.ValueEdited = pref.ValueEdited; + newprefsdict.Add(prefsdict.Keys.ElementAt(j), newpref); + } + output.Add(prefs.Keys.ElementAt(i), newprefsdict); + } + return output; + } + public static void RegisterPrefString(string section, string name, string defaultValue, string displayText = null, bool hideFromList = false) => RegisterString(section, name, defaultValue, displayText, hideFromList); + public static void RegisterPrefBool(string section, string name, bool defaultValue, string displayText = null, bool hideFromList = false) => RegisterBool(section, name, defaultValue, displayText, hideFromList); + public static void RegisterPrefInt(string section, string name, int defaultValue, string displayText = null, bool hideFromList = false) => RegisterInt(section, name, defaultValue, displayText, hideFromList); + public static void RegisterPrefFloat(string section, string name, float defaultValue, string displayText = null, bool hideFromList = false) => RegisterFloat(section, name, defaultValue, displayText, hideFromList); + public enum PrefType + { + STRING, + BOOL, + INT, + FLOAT + } + public class PrefDesc : MelonPreference + { + public PrefType Type { get => (PrefType)base.Type; } + public PrefDesc(string value, PrefType type, bool hidden, string displayText) : base(value, type, hidden, displayText) + { + Value = value; + ValueEdited = value; + base.Type = (MelonPreferenceType)type; + Hidden = hidden; + DisplayText = displayText; + } + } + } + } \ No newline at end of file diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Attributes.cs b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Attributes.cs new file mode 100644 index 0000000..14e4f51 --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Attributes.cs @@ -0,0 +1,325 @@ +using System; +using System.Collections.Generic; + +namespace Harmony +{ + public enum MethodType + { + Normal, + Getter, + Setter, + Constructor, + StaticConstructor + } + + [Obsolete("This enum will be removed in the next major version. To define special methods, use MethodType")] + public enum PropertyMethod + { + Getter, + Setter + } + + public enum ArgumentType + { + Normal, + Ref, + Out, + Pointer + } + + public enum HarmonyPatchType + { + All, + Prefix, + Postfix, + Transpiler + } + + public class HarmonyAttribute : Attribute + { + public HarmonyMethod info = new HarmonyMethod(); + } + + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] + public class HarmonyPatch : HarmonyAttribute + { + // no argument (for use with TargetMethod) + + public HarmonyPatch() + { + } + + // starting with 'Type' + + public HarmonyPatch(Type declaringType) + { + info.declaringType = declaringType; + } + + public HarmonyPatch(Type declaringType, Type[] argumentTypes) + { + info.declaringType = declaringType; + info.argumentTypes = argumentTypes; + } + + public HarmonyPatch(Type declaringType, string methodName) + { + info.declaringType = declaringType; + info.methodName = methodName; + } + + public HarmonyPatch(Type declaringType, string methodName, params Type[] argumentTypes) + { + info.declaringType = declaringType; + info.methodName = methodName; + info.argumentTypes = argumentTypes; + } + + public HarmonyPatch(Type declaringType, string methodName, Type[] argumentTypes, ArgumentType[] argumentVariations) + { + info.declaringType = declaringType; + info.methodName = methodName; + ParseSpecialArguments(argumentTypes, argumentVariations); + } + + public HarmonyPatch(Type declaringType, MethodType methodType) + { + info.declaringType = declaringType; + info.methodType = methodType; + } + + public HarmonyPatch(Type declaringType, MethodType methodType, params Type[] argumentTypes) + { + info.declaringType = declaringType; + info.methodType = methodType; + info.argumentTypes = argumentTypes; + } + + public HarmonyPatch(Type declaringType, MethodType methodType, Type[] argumentTypes, ArgumentType[] argumentVariations) + { + info.declaringType = declaringType; + info.methodType = methodType; + ParseSpecialArguments(argumentTypes, argumentVariations); + } + + public HarmonyPatch(Type declaringType, string propertyName, MethodType methodType) + { + info.declaringType = declaringType; + info.methodName = propertyName; + info.methodType = methodType; + } + + // starting with 'string' + + public HarmonyPatch(string methodName) + { + info.methodName = methodName; + } + + public HarmonyPatch(string methodName, params Type[] argumentTypes) + { + info.methodName = methodName; + info.argumentTypes = argumentTypes; + } + + public HarmonyPatch(string methodName, Type[] argumentTypes, ArgumentType[] argumentVariations) + { + info.methodName = methodName; + ParseSpecialArguments(argumentTypes, argumentVariations); + } + + public HarmonyPatch(string propertyName, MethodType methodType) + { + info.methodName = propertyName; + info.methodType = methodType; + } + + // starting with 'MethodType' + + public HarmonyPatch(MethodType methodType) + { + info.methodType = methodType; + } + + public HarmonyPatch(MethodType methodType, params Type[] argumentTypes) + { + info.methodType = methodType; + info.argumentTypes = argumentTypes; + } + + public HarmonyPatch(MethodType methodType, Type[] argumentTypes, ArgumentType[] argumentVariations) + { + info.methodType = methodType; + ParseSpecialArguments(argumentTypes, argumentVariations); + } + + // starting with 'Type[]' + + public HarmonyPatch(Type[] argumentTypes) + { + info.argumentTypes = argumentTypes; + } + + public HarmonyPatch(Type[] argumentTypes, ArgumentType[] argumentVariations) + { + ParseSpecialArguments(argumentTypes, argumentVariations); + } + + // Obsolete attributes + + [Obsolete("This attribute will be removed in the next major version. Use HarmonyPatch together with MethodType.Getter or MethodType.Setter instead")] + public HarmonyPatch(string propertyName, PropertyMethod type) + { + info.methodName = propertyName; + info.methodType = type == PropertyMethod.Getter ? MethodType.Getter : MethodType.Setter; + } + + // + + private void ParseSpecialArguments(Type[] argumentTypes, ArgumentType[] argumentVariations) + { + if (argumentVariations == null || argumentVariations.Length == 0) + { + info.argumentTypes = argumentTypes; + return; + } + + if (argumentTypes.Length < argumentVariations.Length) + throw new ArgumentException("argumentVariations contains more elements than argumentTypes", nameof(argumentVariations)); + + var types = new List(); + for (var i = 0; i < argumentTypes.Length; i++) + { + var type = argumentTypes[i]; + switch (argumentVariations[i]) + { + case ArgumentType.Ref: + case ArgumentType.Out: + type = type.MakeByRefType(); + break; + case ArgumentType.Pointer: + type = type.MakePointerType(); + break; + } + types.Add(type); + } + info.argumentTypes = types.ToArray(); + } + } + + [AttributeUsage(AttributeTargets.Class)] + public class HarmonyPatchAll : HarmonyAttribute + { + public HarmonyPatchAll() + { + } + } + + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] + public class HarmonyPriority : HarmonyAttribute + { + public HarmonyPriority(int prioritiy) + { + info.prioritiy = prioritiy; + } + } + + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] + public class HarmonyBefore : HarmonyAttribute + { + public HarmonyBefore(params string[] before) + { + info.before = before; + } + } + + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] + public class HarmonyAfter : HarmonyAttribute + { + public HarmonyAfter(params string[] after) + { + info.after = after; + } + } + + // If you don't want to use the special method names you can annotate + // using the following attributes: + + [AttributeUsage(AttributeTargets.Method)] + public class HarmonyPrepare : Attribute + { + } + + [AttributeUsage(AttributeTargets.Method)] + public class HarmonyCleanup : Attribute + { + } + + [AttributeUsage(AttributeTargets.Method)] + public class HarmonyTargetMethod : Attribute + { + } + + [AttributeUsage(AttributeTargets.Method)] + public class HarmonyTargetMethods : Attribute + { + } + + [AttributeUsage(AttributeTargets.Method)] + public class HarmonyPrefix : Attribute + { + } + + [AttributeUsage(AttributeTargets.Method)] + public class HarmonyPostfix : Attribute + { + } + + [AttributeUsage(AttributeTargets.Method)] + public class HarmonyTranspiler : Attribute + { + } + + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)] + public class HarmonyArgument : Attribute + { + public string OriginalName { get; private set; } + public int Index { get; private set; } + public string NewName { get; private set; } + + public HarmonyArgument(string originalName) : this(originalName, null) + { + } + + public HarmonyArgument(int index) : this(index, null) + { + } + + public HarmonyArgument(string originalName, string newName) + { + OriginalName = originalName; + Index = -1; + NewName = newName; + } + + public HarmonyArgument(int index, string name) + { + OriginalName = null; + Index = index; + NewName = name; + } + } + + // This attribute is for Harmony patching itself to the latest + // + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor)] + internal class UpgradeToLatestVersion : Attribute + { + public int version; + + public UpgradeToLatestVersion(int version) + { + this.version = version; + } + } +} \ No newline at end of file diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/CodeInstruction.cs b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/CodeInstruction.cs new file mode 100644 index 0000000..a6d8972 --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/CodeInstruction.cs @@ -0,0 +1,62 @@ +using Harmony.ILCopying; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Emit; + +namespace Harmony +{ + public class CodeInstruction + { + public OpCode opcode; + public object operand; + public List labels = new List(); + public List blocks = new List(); + + public CodeInstruction(OpCode opcode, object operand = null) + { + this.opcode = opcode; + this.operand = operand; + } + + public CodeInstruction(CodeInstruction instruction) + { + opcode = instruction.opcode; + operand = instruction.operand; + labels = instruction.labels.ToArray().ToList(); + } + + public CodeInstruction Clone() + { + return new CodeInstruction(this) { labels = new List() }; + } + + public CodeInstruction Clone(OpCode opcode) + { + var instruction = new CodeInstruction(this) { labels = new List() }; + instruction.opcode = opcode; + return instruction; + } + + public CodeInstruction Clone(OpCode opcode, object operand) + { + var instruction = new CodeInstruction(this) { labels = new List() }; + instruction.opcode = opcode; + instruction.operand = operand; + return instruction; + } + + public override string ToString() + { + var list = new List(); + foreach (var label in labels) + list.Add("Label" + label.GetHashCode()); + foreach (var block in blocks) + list.Add("EX_" + block.blockType.ToString().Replace("Block", "")); + + var extras = list.Count > 0 ? " [" + string.Join(", ", list.ToArray()) + "]" : ""; + var operandStr = Emitter.FormatArgument(operand); + if (operandStr != "") operandStr = " " + operandStr; + return opcode + operandStr + extras; + } + } +} \ No newline at end of file diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/CodeTranspiler.cs b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/CodeTranspiler.cs new file mode 100644 index 0000000..6deb15b --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/CodeTranspiler.cs @@ -0,0 +1,295 @@ +using System.Collections.Generic; +using Harmony.ILCopying; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System; +using System.Collections; +using System.Diagnostics; + +namespace Harmony +{ + public class CodeTranspiler + { + private IEnumerable codeInstructions; + private List transpilers = new List(); + + public CodeTranspiler(List ilInstructions) + { + codeInstructions = ilInstructions + .Select(ilInstruction => ilInstruction.GetCodeInstruction()) + .ToList().AsEnumerable(); + } + + public void Add(MethodInfo transpiler) + { + transpilers.Add(transpiler); + } + + [UpgradeToLatestVersion(1)] + public static object ConvertInstruction(Type type, object op, out Dictionary unassigned) + { + var nonExisting = new Dictionary(); + var elementTo = AccessTools.MakeDeepCopy(op, type, (namePath, trvSrc, trvDest) => + { + var value = trvSrc.GetValue(); + + if (trvDest.FieldExists() == false) + { + nonExisting[namePath] = value; + return null; + } + + if (namePath == nameof(CodeInstruction.opcode)) + return ReplaceShortJumps((OpCode)value); + + return value; + }); + unassigned = nonExisting; + return elementTo; + } + + // ShouldAddExceptionInfo is used to determine if CodeInstructions from an older Harmony version were duplicating + // exception information as well as to preserve the exception information from being dropped when piping through + // multiple transpilers with mixed Harmony versions. + // + public static bool ShouldAddExceptionInfo(object op, int opIndex, List originalInstructions, List newInstructions, Dictionary> unassignedValues) + { + var originalIndex = originalInstructions.IndexOf(op); + if (originalIndex == -1) + return false; // no need, new instruction + + Dictionary unassigned = null; + if (unassignedValues.TryGetValue(op, out unassigned) == false) + return false; // no need, no unassigned info + + if (unassigned.TryGetValue(nameof(CodeInstruction.blocks), out var blocksObject) == false) + return false; // no need, no try-catch info + var blocks = blocksObject as List; + + var dupCount = newInstructions.Count(instr => instr == op); + if (dupCount <= 1) + return true; // ok, no duplicate found + + var isStartBlock = blocks.FirstOrDefault(block => block.blockType != ExceptionBlockType.EndExceptionBlock); + var isEndBlock = blocks.FirstOrDefault(block => block.blockType == ExceptionBlockType.EndExceptionBlock); + + if (isStartBlock != null && isEndBlock == null) + { + var pairInstruction = originalInstructions.Skip(originalIndex + 1).FirstOrDefault(instr => + { + if (unassignedValues.TryGetValue(instr, out unassigned) == false) + return false; + if (unassigned.TryGetValue(nameof(CodeInstruction.blocks), out blocksObject) == false) + return false; + blocks = blocksObject as List; + return blocks.Count() > 0; + }); + if (pairInstruction != null) + { + var pairStart = originalIndex + 1; + var pairEnd = pairStart + originalInstructions.Skip(pairStart).ToList().IndexOf(pairInstruction) - 1; + var originalBetweenInstructions = originalInstructions + .GetRange(pairStart, pairEnd - pairStart) + .Intersect(newInstructions); + + pairInstruction = newInstructions.Skip(opIndex + 1).FirstOrDefault(instr => + { + if (unassignedValues.TryGetValue(instr, out unassigned) == false) + return false; + if (unassigned.TryGetValue(nameof(CodeInstruction.blocks), out blocksObject) == false) + return false; + blocks = blocksObject as List; + return blocks.Count() > 0; + }); + if (pairInstruction != null) + { + pairStart = opIndex + 1; + pairEnd = pairStart + newInstructions.Skip(opIndex + 1).ToList().IndexOf(pairInstruction) - 1; + var newBetweenInstructions = newInstructions.GetRange(pairStart, pairEnd - pairStart); + var remaining = originalBetweenInstructions.Except(newBetweenInstructions).ToList(); + return remaining.Count() == 0; + } + } + } + if (isStartBlock == null && isEndBlock != null) + { + var pairInstruction = originalInstructions.GetRange(0, originalIndex).LastOrDefault(instr => + { + if (unassignedValues.TryGetValue(instr, out unassigned) == false) + return false; + if (unassigned.TryGetValue(nameof(CodeInstruction.blocks), out blocksObject) == false) + return false; + blocks = blocksObject as List; + return blocks.Count() > 0; + }); + if (pairInstruction != null) + { + var pairStart = originalInstructions.GetRange(0, originalIndex).LastIndexOf(pairInstruction); + var pairEnd = originalIndex; + var originalBetweenInstructions = originalInstructions + .GetRange(pairStart, pairEnd - pairStart) + .Intersect(newInstructions); + + pairInstruction = newInstructions.GetRange(0, opIndex).LastOrDefault(instr => + { + if (unassignedValues.TryGetValue(instr, out unassigned) == false) + return false; + if (unassigned.TryGetValue(nameof(CodeInstruction.blocks), out blocksObject) == false) + return false; + blocks = blocksObject as List; + return blocks.Count() > 0; + }); + if (pairInstruction != null) + { + pairStart = newInstructions.GetRange(0, opIndex).LastIndexOf(pairInstruction); + pairEnd = opIndex; + var newBetweenInstructions = newInstructions.GetRange(pairStart, pairEnd - pairStart); + var remaining = originalBetweenInstructions.Except(newBetweenInstructions); + return remaining.Count() == 0; + } + } + } + + // unclear or unexpected case, ok by default + return true; + } + + public static IEnumerable ConvertInstructionsAndUnassignedValues(Type type, IEnumerable enumerable, out Dictionary> unassignedValues) + { + var enumerableAssembly = type.GetGenericTypeDefinition().Assembly; + var genericListType = enumerableAssembly.GetType(typeof(List<>).FullName); + var elementType = type.GetGenericArguments()[0]; + var listType = enumerableAssembly.GetType(genericListType.MakeGenericType(new Type[] { elementType }).FullName); + var list = Activator.CreateInstance(listType); + var listAdd = list.GetType().GetMethod("Add"); + unassignedValues = new Dictionary>(); + foreach (var op in enumerable) + { + var elementTo = ConvertInstruction(elementType, op, out var unassigned); + unassignedValues.Add(elementTo, unassigned); + listAdd.Invoke(list, new object[] { elementTo }); + // cannot yield return 'elementTo' here because we have an out parameter in the method + } + return list as IEnumerable; + } + + [UpgradeToLatestVersion(1)] + public static IEnumerable ConvertToOurInstructions(IEnumerable instructions, List originalInstructions, Dictionary> unassignedValues) + { + // Since we are patching this method, we cannot use typeof(CodeInstruction) + // because that would use the CodeInstruction of the Harmony lib that patches + // us and we get a type assignment error. + // Instead, we know that our caller returns List where X is the type we need + // + var codeInstructionType = new StackTrace().GetFrames() + .Select(frame => frame.GetMethod()) + .OfType() + .Select(method => + { + var returnType = method.ReturnType; + if (returnType.IsGenericType == false) return null; + var listTypes = returnType.GetGenericArguments(); + if (listTypes.Length != 1) return null; + var type = listTypes[0]; + return type.FullName == typeof(CodeInstruction).FullName ? type : null; + }) + .Where(type => type != null) + .First(); + + var newInstructions = instructions.Cast().ToList(); + + var index = -1; + foreach (var op in newInstructions) + { + index++; + var elementTo = AccessTools.MakeDeepCopy(op, codeInstructionType); + if (unassignedValues.TryGetValue(op, out var fields)) + { + var addExceptionInfo = ShouldAddExceptionInfo(op, index, originalInstructions, newInstructions, unassignedValues); + + var trv = Traverse.Create(elementTo); + foreach (var field in fields) + { + if (addExceptionInfo || field.Key != nameof(CodeInstruction.blocks)) + trv.Field(field.Key).SetValue(field.Value); + } + } + yield return elementTo; + } + } + + public static IEnumerable ConvertToGeneralInstructions(MethodInfo transpiler, IEnumerable enumerable, out Dictionary> unassignedValues) + { + var type = transpiler.GetParameters() + .Select(p => p.ParameterType) + .FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition().Name.StartsWith("IEnumerable")); + return ConvertInstructionsAndUnassignedValues(type, enumerable, out unassignedValues); + } + + public static List GetTranspilerCallParameters(ILGenerator generator, MethodInfo transpiler, MethodBase method, IEnumerable instructions) + { + var parameter = new List(); + transpiler.GetParameters().Select(param => param.ParameterType).Do(type => + { + if (type.IsAssignableFrom(typeof(ILGenerator))) + parameter.Add(generator); + else if (type.IsAssignableFrom(typeof(MethodBase))) + parameter.Add(method); + else + parameter.Add(instructions); + }); + return parameter; + } + + public List GetResult(ILGenerator generator, MethodBase method) + { + IEnumerable instructions = codeInstructions; + transpilers.ForEach(transpiler => + { + // before calling some transpiler, convert the input to 'their' CodeInstruction type + // also remember any unassignable values that otherwise would be lost + instructions = ConvertToGeneralInstructions(transpiler, instructions, out var unassignedValues); + + // remember the order of the original input (for detection of dupped code instructions) + var originalInstructions = new List(); + originalInstructions.AddRange(instructions.Cast()); + + // call the transpiler + var parameter = GetTranspilerCallParameters(generator, transpiler, method, instructions); + instructions = transpiler.Invoke(null, parameter.ToArray()) as IEnumerable; + + // convert result back to 'our' CodeInstruction and re-assign otherwise lost fields + instructions = ConvertToOurInstructions(instructions, originalInstructions, unassignedValues); + }); + return instructions.Cast().ToList(); + } + + // + + static readonly Dictionary allJumpCodes = new Dictionary + { + { OpCodes.Beq_S, OpCodes.Beq }, + { OpCodes.Bge_S, OpCodes.Bge }, + { OpCodes.Bge_Un_S, OpCodes.Bge_Un }, + { OpCodes.Bgt_S, OpCodes.Bgt }, + { OpCodes.Bgt_Un_S, OpCodes.Bgt_Un }, + { OpCodes.Ble_S, OpCodes.Ble }, + { OpCodes.Ble_Un_S, OpCodes.Ble_Un }, + { OpCodes.Blt_S, OpCodes.Blt }, + { OpCodes.Blt_Un_S, OpCodes.Blt_Un }, + { OpCodes.Bne_Un_S, OpCodes.Bne_Un }, + { OpCodes.Brfalse_S, OpCodes.Brfalse }, + { OpCodes.Brtrue_S, OpCodes.Brtrue }, + { OpCodes.Br_S, OpCodes.Br }, + { OpCodes.Leave_S, OpCodes.Leave } + }; + static OpCode ReplaceShortJumps(OpCode opcode) + { + foreach (var pair in allJumpCodes) + if (opcode == pair.Key) + return pair.Value; + return opcode; + } + } +} \ No newline at end of file diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Extras/DelegateTypeFactory.cs b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Extras/DelegateTypeFactory.cs new file mode 100644 index 0000000..a7e45b9 --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Extras/DelegateTypeFactory.cs @@ -0,0 +1,43 @@ +using System; +using System.Reflection; +using System.Reflection.Emit; + +namespace Harmony +{ + public class DelegateTypeFactory + { + readonly ModuleBuilder module; + + static int counter; + public DelegateTypeFactory() + { + counter++; + var name = new AssemblyName("HarmonyDTFAssembly" + counter); + var assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(name, AssemblyBuilderAccess.Run); + module = assembly.DefineDynamicModule("HarmonyDTFModule" + counter); + } + + public Type CreateDelegateType(MethodInfo method) + { + var attr = TypeAttributes.Sealed | TypeAttributes.Public; + var typeBuilder = module.DefineType("HarmonyDTFType" + counter, attr, typeof(MulticastDelegate)); + + var constructor = typeBuilder.DefineConstructor( + MethodAttributes.RTSpecialName | MethodAttributes.HideBySig | MethodAttributes.Public, + CallingConventions.Standard, new[] { typeof(object), typeof(IntPtr) }); + constructor.SetImplementationFlags(MethodImplAttributes.CodeTypeMask); + + var parameters = method.GetParameters(); + + var invokeMethod = typeBuilder.DefineMethod( + "Invoke", MethodAttributes.HideBySig | MethodAttributes.Virtual | MethodAttributes.Public, + method.ReturnType, parameters.Types()); + invokeMethod.SetImplementationFlags(MethodImplAttributes.CodeTypeMask); + + for (var i = 0; i < parameters.Length; i++) + invokeMethod.DefineParameter(i + 1, ParameterAttributes.None, parameters[i].Name); + + return typeBuilder.CreateType(); + } + } +} diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Extras/FastAccess.cs b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Extras/FastAccess.cs new file mode 100644 index 0000000..ad74831 --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Extras/FastAccess.cs @@ -0,0 +1,123 @@ +using System; +using System.Reflection; +using System.Reflection.Emit; + +namespace Harmony +{ + // Based on https://www.codeproject.com/Articles/14973/Dynamic-Code-Generation-vs-Reflection + + public delegate object GetterHandler(object source); + public delegate void SetterHandler(object source, object value); + public delegate object InstantiationHandler(); + + public class FastAccess + { + public static InstantiationHandler CreateInstantiationHandler(Type type) + { + var constructorInfo = type.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[0], null); + if (constructorInfo == null) + { + throw new ApplicationException(string.Format("The type {0} must declare an empty constructor (the constructor may be private, internal, protected, protected internal, or public).", type)); + } + + var dynamicMethod = new DynamicMethod("InstantiateObject_" + type.Name, MethodAttributes.Static | MethodAttributes.Public, CallingConventions.Standard, typeof(object), null, type, true); + var generator = dynamicMethod.GetILGenerator(); + generator.Emit(OpCodes.Newobj, constructorInfo); + generator.Emit(OpCodes.Ret); + return (InstantiationHandler)dynamicMethod.CreateDelegate(typeof(InstantiationHandler)); + } + + public static GetterHandler CreateGetterHandler(PropertyInfo propertyInfo) + { + var getMethodInfo = propertyInfo.GetGetMethod(true); + var dynamicGet = CreateGetDynamicMethod(propertyInfo.DeclaringType); + var getGenerator = dynamicGet.GetILGenerator(); + + getGenerator.Emit(OpCodes.Ldarg_0); + getGenerator.Emit(OpCodes.Call, getMethodInfo); + BoxIfNeeded(getMethodInfo.ReturnType, getGenerator); + getGenerator.Emit(OpCodes.Ret); + + return (GetterHandler)dynamicGet.CreateDelegate(typeof(GetterHandler)); + } + + public static GetterHandler CreateGetterHandler(FieldInfo fieldInfo) + { + var dynamicGet = CreateGetDynamicMethod(fieldInfo.DeclaringType); + var getGenerator = dynamicGet.GetILGenerator(); + + getGenerator.Emit(OpCodes.Ldarg_0); + getGenerator.Emit(OpCodes.Ldfld, fieldInfo); + BoxIfNeeded(fieldInfo.FieldType, getGenerator); + getGenerator.Emit(OpCodes.Ret); + + return (GetterHandler)dynamicGet.CreateDelegate(typeof(GetterHandler)); + } + + public static GetterHandler CreateFieldGetter(Type type, params string[] names) + { + foreach (var name in names) + { + if (AccessTools.Field(typeof(ILGenerator), name) != null) + return CreateGetterHandler(AccessTools.Field(type, name)); + + if (AccessTools.Property(typeof(ILGenerator), name) != null) + return CreateGetterHandler(AccessTools.Property(type, name)); + } + return null; + } + + public static SetterHandler CreateSetterHandler(PropertyInfo propertyInfo) + { + var setMethodInfo = propertyInfo.GetSetMethod(true); + var dynamicSet = CreateSetDynamicMethod(propertyInfo.DeclaringType); + var setGenerator = dynamicSet.GetILGenerator(); + + setGenerator.Emit(OpCodes.Ldarg_0); + setGenerator.Emit(OpCodes.Ldarg_1); + UnboxIfNeeded(setMethodInfo.GetParameters()[0].ParameterType, setGenerator); + setGenerator.Emit(OpCodes.Call, setMethodInfo); + setGenerator.Emit(OpCodes.Ret); + + return (SetterHandler)dynamicSet.CreateDelegate(typeof(SetterHandler)); + } + + public static SetterHandler CreateSetterHandler(FieldInfo fieldInfo) + { + var dynamicSet = CreateSetDynamicMethod(fieldInfo.DeclaringType); + var setGenerator = dynamicSet.GetILGenerator(); + + setGenerator.Emit(OpCodes.Ldarg_0); + setGenerator.Emit(OpCodes.Ldarg_1); + UnboxIfNeeded(fieldInfo.FieldType, setGenerator); + setGenerator.Emit(OpCodes.Stfld, fieldInfo); + setGenerator.Emit(OpCodes.Ret); + + return (SetterHandler)dynamicSet.CreateDelegate(typeof(SetterHandler)); + } + + // + + static DynamicMethod CreateGetDynamicMethod(Type type) + { + return new DynamicMethod("DynamicGet_" + type.Name, typeof(object), new Type[] { typeof(object) }, type, true); + } + + static DynamicMethod CreateSetDynamicMethod(Type type) + { + return new DynamicMethod("DynamicSet_" + type.Name, typeof(void), new Type[] { typeof(object), typeof(object) }, type, true); + } + + static void BoxIfNeeded(Type type, ILGenerator generator) + { + if (type.IsValueType) + generator.Emit(OpCodes.Box, type); + } + + static void UnboxIfNeeded(Type type, ILGenerator generator) + { + if (type.IsValueType) + generator.Emit(OpCodes.Unbox_Any, type); + } + } +} \ No newline at end of file diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Extras/MethodInvoker.cs b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Extras/MethodInvoker.cs new file mode 100644 index 0000000..e45e2bf --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Extras/MethodInvoker.cs @@ -0,0 +1,179 @@ +using System; +using System.Reflection; +using System.Reflection.Emit; + +namespace Harmony +{ + // Based on https://www.codeproject.com/Articles/14593/A-General-Fast-Method-Invoker + + public delegate object FastInvokeHandler(object target, object[] paramters); + + public class MethodInvoker + { + public static FastInvokeHandler GetHandler(DynamicMethod methodInfo, Module module) + { + return Handler(methodInfo, module); + } + + public static FastInvokeHandler GetHandler(MethodInfo methodInfo) + { + return Handler(methodInfo, methodInfo.DeclaringType.Module); + } + + static FastInvokeHandler Handler(MethodInfo methodInfo, Module module, bool directBoxValueAccess = false) + { + var dynamicMethod = new DynamicMethod("FastInvoke_" + methodInfo.Name + "_" + (directBoxValueAccess ? "direct" : "indirect"), typeof(object), new Type[] { typeof(object), typeof(object[]) }, module, true); + var il = dynamicMethod.GetILGenerator(); + + if (!methodInfo.IsStatic) + { + il.Emit(OpCodes.Ldarg_0); + EmitUnboxIfNeeded(il, methodInfo.DeclaringType); + } + + var generateLocalBoxValuePtr = true; + var ps = methodInfo.GetParameters(); + for (var i = 0; i < ps.Length; i++) + { + var argType = ps[i].ParameterType; + var argIsByRef = argType.IsByRef; + if (argIsByRef) + argType = argType.GetElementType(); + var argIsValueType = argType.IsValueType; + + if (argIsByRef && argIsValueType && !directBoxValueAccess) + { + // used later when storing back the reference to the new box in the array. + il.Emit(OpCodes.Ldarg_1); + EmitFastInt(il, i); + } + + il.Emit(OpCodes.Ldarg_1); + EmitFastInt(il, i); + + if (argIsByRef && !argIsValueType) + { + il.Emit(OpCodes.Ldelema, typeof(object)); + } + else + { + il.Emit(OpCodes.Ldelem_Ref); + if (argIsValueType) + { + if (!argIsByRef || !directBoxValueAccess) + { + // if !directBoxValueAccess, create a new box if required + il.Emit(OpCodes.Unbox_Any, argType); + if (argIsByRef) + { + // box back + il.Emit(OpCodes.Box, argType); + + // store new box value address to local 0 + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Unbox, argType); + if (generateLocalBoxValuePtr) + { + generateLocalBoxValuePtr = false; + // Yes, you're seeing this right - a local of type void* to store the box value address! + il.DeclareLocal(typeof(void*), true); + } + il.Emit(OpCodes.Stloc_0); + + // arr and index set up already + il.Emit(OpCodes.Stelem_Ref); + + // load address back to stack + il.Emit(OpCodes.Ldloc_0); + } + } + else + { + // if directBoxValueAccess, emit unbox (get value address) + il.Emit(OpCodes.Unbox, argType); + } + } + } + } + +#pragma warning disable XS0001 + if (methodInfo.IsStatic) + il.EmitCall(OpCodes.Call, methodInfo, null); + else + il.EmitCall(OpCodes.Callvirt, methodInfo, null); +#pragma warning restore XS0001 + + if (methodInfo.ReturnType == typeof(void)) + il.Emit(OpCodes.Ldnull); + else + EmitBoxIfNeeded(il, methodInfo.ReturnType); + + il.Emit(OpCodes.Ret); + + var invoder = (FastInvokeHandler)dynamicMethod.CreateDelegate(typeof(FastInvokeHandler)); + return invoder; + } + + static void EmitCastToReference(ILGenerator il, Type type) + { + if (type.IsValueType) + il.Emit(OpCodes.Unbox_Any, type); + else + il.Emit(OpCodes.Castclass, type); + } + + static void EmitUnboxIfNeeded(ILGenerator il, Type type) + { + if (type.IsValueType) + il.Emit(OpCodes.Unbox_Any, type); + } + + static void EmitBoxIfNeeded(ILGenerator il, Type type) + { + if (type.IsValueType) + il.Emit(OpCodes.Box, type); + } + + static void EmitFastInt(ILGenerator il, int value) + { + switch (value) + { + case -1: + il.Emit(OpCodes.Ldc_I4_M1); + return; + case 0: + il.Emit(OpCodes.Ldc_I4_0); + return; + case 1: + il.Emit(OpCodes.Ldc_I4_1); + return; + case 2: + il.Emit(OpCodes.Ldc_I4_2); + return; + case 3: + il.Emit(OpCodes.Ldc_I4_3); + return; + case 4: + il.Emit(OpCodes.Ldc_I4_4); + return; + case 5: + il.Emit(OpCodes.Ldc_I4_5); + return; + case 6: + il.Emit(OpCodes.Ldc_I4_6); + return; + case 7: + il.Emit(OpCodes.Ldc_I4_7); + return; + case 8: + il.Emit(OpCodes.Ldc_I4_8); + return; + } + + if (value > -129 && value < 128) + il.Emit(OpCodes.Ldc_I4_S, (sbyte)value); + else + il.Emit(OpCodes.Ldc_I4, value); + } + } +} \ No newline at end of file diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/GlobalSuppressions.cs b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/GlobalSuppressions.cs new file mode 100644 index 0000000..c535640 --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/GlobalSuppressions.cs @@ -0,0 +1,4 @@ +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands", Scope = "member", Target = "Harmony.FileLog.#LogBytes(System.Int64,System.Int32)")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands", Scope = "member", Target = "Harmony.DynamicTools.#PrepareDynamicMethod(System.Reflection.Emit.DynamicMethod)")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2002:DoNotLockOnObjectsWithWeakIdentity", Scope = "member", Target = "Harmony.HarmonySharedState.#GetState()")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands", Scope = "member", Target = "Harmony.ILCopying.Memory.#GetMethodStart(System.Reflection.MethodBase)")] diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/HarmonyInstance.cs b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/HarmonyInstance.cs new file mode 100644 index 0000000..4b3bfe8 --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/HarmonyInstance.cs @@ -0,0 +1,224 @@ +using Harmony.Tools; +using MelonLoader; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; + +namespace Harmony +{ + public class Patches + { + public readonly ReadOnlyCollection Prefixes; + public readonly ReadOnlyCollection Postfixes; + public readonly ReadOnlyCollection Transpilers; + + public ReadOnlyCollection Owners + { + get + { + var result = new HashSet(); + result.UnionWith(Prefixes.Select(p => p.owner)); + result.UnionWith(Postfixes.Select(p => p.owner)); + result.UnionWith(Transpilers.Select(p => p.owner)); + return result.ToList().AsReadOnly(); + } + } + + public Patches(Patch[] prefixes, Patch[] postfixes, Patch[] transpilers) + { + if (prefixes == null) prefixes = new Patch[0]; + if (postfixes == null) postfixes = new Patch[0]; + if (transpilers == null) transpilers = new Patch[0]; + + Prefixes = prefixes.ToList().AsReadOnly(); + Postfixes = postfixes.ToList().AsReadOnly(); + Transpilers = transpilers.ToList().AsReadOnly(); + } + } + + public class HarmonyInstance + { + readonly string id; + public string Id => id; + public static bool DEBUG = false; + private static bool selfPatchingDone = false; + private static List instancelist = new List(); + + HarmonyInstance(string id) + { + if (DEBUG) + { + var assembly = typeof(HarmonyInstance).Assembly; + var version = assembly.GetName().Version; + var location = assembly.Location; + if (location == null || location == "") location = new Uri(assembly.CodeBase).LocalPath; + FileLog.Log("### Harmony id=" + id + ", version=" + version + ", location=" + location); + var callingMethod = GetOutsideCaller(); + var callingAssembly = callingMethod.DeclaringType.Assembly; + location = callingAssembly.Location; + if (location == null || location == "") location = new Uri(callingAssembly.CodeBase).LocalPath; + FileLog.Log("### Started from " + callingMethod.FullDescription() + ", location " + location); + FileLog.Log("### At " + DateTime.Now.ToString("yyyy-MM-dd hh.mm.ss")); + } + + this.id = id; + + if (!selfPatchingDone) + { + selfPatchingDone = true; + SelfPatching.PatchOldHarmonyMethods(); + } + + instancelist.Add(this); + } + + public static HarmonyInstance Create(string id) + { + if (id == null) throw new Exception("id cannot be null"); + return new HarmonyInstance(id); + } + + private MethodBase GetOutsideCaller() + { + var trace = new StackTrace(true); + foreach (var frame in trace.GetFrames()) + { + var method = frame.GetMethod(); + if (method.DeclaringType.Namespace != typeof(HarmonyInstance).Namespace) + return method; + } + throw new Exception("Unexpected end of stack trace"); + } + + public void PatchAll() + { + var method = new StackTrace().GetFrame(1).GetMethod(); + var assembly = method.ReflectedType.Assembly; + PatchAll(assembly); + } + + public void PatchAll(Assembly assembly) + { + assembly.GetTypes().Do(type => + { + var parentMethodInfos = type.GetHarmonyMethods(); + if (parentMethodInfos != null && parentMethodInfos.Count() > 0) + { + var info = HarmonyMethod.Merge(parentMethodInfos); + var processor = new PatchProcessor(this, type, info); + processor.Patch(); + } + }); + } + + public DynamicMethod Patch(MethodBase original, HarmonyMethod prefix = null, HarmonyMethod postfix = null, HarmonyMethod transpiler = null) + { + var processor = new PatchProcessor(this, new List { original }, prefix, postfix, transpiler); + return processor.Patch().FirstOrDefault(); + } + + public void UnpatchAll(string harmonyID = null) + { + bool IDCheck(Patch patchInfo) => harmonyID == null || patchInfo.owner == harmonyID; + + var originals = GetPatchedMethods().ToList(); + foreach (var original in originals) + { + var info = GetPatchInfo(original); + info.Postfixes.DoIf(IDCheck, patchInfo => Unpatch(original, patchInfo.patch)); + info.Prefixes.DoIf(IDCheck, patchInfo => Unpatch(original, patchInfo.patch)); + info.Transpilers.DoIf(IDCheck, patchInfo => Unpatch(original, patchInfo.patch)); + } + } + + public void UnpatchAll(MelonBase melon) + { + bool IDCheck(Patch patchInfo) => (patchInfo.patch.Module.Assembly == melon.Assembly); + var originals = GetPatchedMethods().ToList(); + foreach (var original in originals) + { + var info = GetPatchInfo(original); + info.Postfixes.DoIf(IDCheck, patchInfo => Unpatch(original, patchInfo.patch)); + info.Prefixes.DoIf(IDCheck, patchInfo => Unpatch(original, patchInfo.patch)); + info.Transpilers.DoIf(IDCheck, patchInfo => Unpatch(original, patchInfo.patch)); + } + } + + public static void UnpatchAllMelonInstances(MelonBase melon) + { + if (instancelist.Count > 0) + { + for (int i = 0; i < instancelist.Count; i++) + instancelist[i].UnpatchAll(melon); + instancelist.Clear(); + } + } + + public static void UnpatchAllInstances() + { + if (instancelist.Count > 0) + { + for (int i = 0; i < instancelist.Count; i++) + instancelist[i].UnpatchAll(); + instancelist.Clear(); + } + } + + public void Unpatch(MethodBase original, HarmonyPatchType type, string harmonyID = null) + { + var processor = new PatchProcessor(this, new List { original }); + processor.Unpatch(type, harmonyID); + } + + public void Unpatch(MethodBase original, MethodInfo patch) + { + var processor = new PatchProcessor(this, new List { original }); + processor.Unpatch(patch); + } + + // + + public bool HasAnyPatches(string harmonyID) + { + return GetPatchedMethods() + .Select(original => GetPatchInfo(original)) + .Any(info => info.Owners.Contains(harmonyID)); + } + + public Patches GetPatchInfo(MethodBase method) + { + return PatchProcessor.GetPatchInfo(method); + } + + public IEnumerable GetPatchedMethods() + { + return HarmonySharedState.GetPatchedMethods(); + } + + public Dictionary VersionInfo(out Version currentVersion) + { + currentVersion = typeof(HarmonyInstance).Assembly.GetName().Version; + var assemblies = new Dictionary(); + GetPatchedMethods().Do(method => + { + var info = HarmonySharedState.GetPatchInfo(method); + info.prefixes.Do(fix => assemblies[fix.owner] = fix.patch.DeclaringType.Assembly); + info.postfixes.Do(fix => assemblies[fix.owner] = fix.patch.DeclaringType.Assembly); + info.transpilers.Do(fix => assemblies[fix.owner] = fix.patch.DeclaringType.Assembly); + }); + + var result = new Dictionary(); + assemblies.Do(info => + { + var assemblyName = info.Value.GetReferencedAssemblies().FirstOrDefault(a => a.FullName.StartsWith("0Harmony, Version")); + if (assemblyName != null) + result[info.Key] = assemblyName.Version; + }); + return result; + } + } +} diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/HarmonyMethod.cs b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/HarmonyMethod.cs new file mode 100644 index 0000000..74e6e25 --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/HarmonyMethod.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; + +namespace Harmony +{ + public class HarmonyMethod + { + public MethodInfo method; // need to be called 'method' + + public Type declaringType; + public string methodName; + public MethodType? methodType; + public Type[] argumentTypes; + public int prioritiy = -1; + public string[] before; + public string[] after; + + public HarmonyMethod() + { + } + + void ImportMethod(MethodInfo theMethod) + { + method = theMethod; + if (method != null) + { + var infos = method.GetHarmonyMethods(); + if (infos != null) + Merge(infos).CopyTo(this); + } + } + + public HarmonyMethod(MethodInfo method) + { + ImportMethod(method); + } + + public HarmonyMethod(Type type, string name, Type[] parameters = null) + { + var method = AccessTools.Method(type, name, parameters); + ImportMethod(method); + } + + public static List HarmonyFields() + { + return AccessTools + .GetFieldNames(typeof(HarmonyMethod)) + .Where(s => s != "method") + .ToList(); + } + + public static HarmonyMethod Merge(List attributes) + { + var result = new HarmonyMethod(); + if (attributes == null) return result; + var resultTrv = Traverse.Create(result); + attributes.ForEach(attribute => + { + var trv = Traverse.Create(attribute); + HarmonyFields().ForEach(f => + { + var val = trv.Field(f).GetValue(); + if (val != null) + resultTrv.Field(f).SetValue(val); + }); + }); + return result; + } + + public override string ToString() + { + var result = "HarmonyMethod["; + var trv = Traverse.Create(this); + HarmonyFields().ForEach(f => + { + result += f + '=' + trv.Field(f).GetValue(); + }); + return result + "]"; + } + } + + public static class HarmonyMethodExtensions + { + public static void CopyTo(this HarmonyMethod from, HarmonyMethod to) + { + if (to == null) return; + var fromTrv = Traverse.Create(from); + var toTrv = Traverse.Create(to); + HarmonyMethod.HarmonyFields().ForEach(f => + { + var val = fromTrv.Field(f).GetValue(); + if (val != null) toTrv.Field(f).SetValue(val); + }); + } + + public static HarmonyMethod Clone(this HarmonyMethod original) + { + var result = new HarmonyMethod(); + original.CopyTo(result); + return result; + } + + public static HarmonyMethod Merge(this HarmonyMethod master, HarmonyMethod detail) + { + if (detail == null) return master; + var result = new HarmonyMethod(); + var resultTrv = Traverse.Create(result); + var masterTrv = Traverse.Create(master); + var detailTrv = Traverse.Create(detail); + HarmonyMethod.HarmonyFields().ForEach(f => + { + var baseValue = masterTrv.Field(f).GetValue(); + var detailValue = detailTrv.Field(f).GetValue(); + resultTrv.Field(f).SetValue(detailValue ?? baseValue); + }); + return result; + } + + public static List GetHarmonyMethods(this Type type) + { + return type.GetCustomAttributes(true) + .Where(attr => attr is HarmonyAttribute) + .Cast() + .Select(attr => attr.info) + .ToList(); + } + + public static List GetHarmonyMethods(this MethodBase method) + { + if (method is DynamicMethod) return new List(); + return method.GetCustomAttributes(true) + .Where(attr => attr is HarmonyAttribute) + .Cast() + .Select(attr => attr.info) + .ToList(); + } + } +} \ No newline at end of file diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/HarmonySharedState.cs b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/HarmonySharedState.cs new file mode 100644 index 0000000..2fdc08a --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/HarmonySharedState.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; + +namespace Harmony +{ + public static class HarmonySharedState + { + static readonly string name = "HarmonySharedState"; + internal static readonly int internalVersion = 100; + internal static int actualVersion = -1; + + static Dictionary GetState() + { + lock (name) + { + var assembly = SharedStateAssembly(); + if (assembly == null) + { + var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName(name), AssemblyBuilderAccess.Run); + var moduleBuilder = assemblyBuilder.DefineDynamicModule(name); + var typeAttributes = TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.Sealed | TypeAttributes.Abstract; + var typeBuilder = moduleBuilder.DefineType(name, typeAttributes); + typeBuilder.DefineField("state", typeof(Dictionary), FieldAttributes.Static | FieldAttributes.Public); + typeBuilder.DefineField("version", typeof(int), FieldAttributes.Static | FieldAttributes.Public).SetConstant(internalVersion); + typeBuilder.CreateType(); + + assembly = SharedStateAssembly(); + if (assembly == null) throw new Exception("Cannot find or create harmony shared state"); + } + + var versionField = assembly.GetType(name).GetField("version"); + if (versionField == null) throw new Exception("Cannot find harmony state version field"); + actualVersion = (int)versionField.GetValue(null); + + var stateField = assembly.GetType(name).GetField("state"); + if (stateField == null) throw new Exception("Cannot find harmony state field"); + if (stateField.GetValue(null) == null) stateField.SetValue(null, new Dictionary()); + return (Dictionary)stateField.GetValue(null); + } + } + + static Assembly SharedStateAssembly() + { + return AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => a.GetName().Name.Contains(name)); + } + + internal static PatchInfo GetPatchInfo(MethodBase method) + { + var bytes = GetState().GetValueSafe(method); + if (bytes == null) return null; + return PatchInfoSerialization.Deserialize(bytes); + } + + internal static IEnumerable GetPatchedMethods() + { + return GetState().Keys.AsEnumerable(); + } + + internal static void UpdatePatchInfo(MethodBase method, PatchInfo patchInfo) + { + GetState()[method] = patchInfo.Serialize(); + } + } +} \ No newline at end of file diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/ILCopying/ByteBuffer.cs b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/ILCopying/ByteBuffer.cs new file mode 100644 index 0000000..d1dcf69 --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/ILCopying/ByteBuffer.cs @@ -0,0 +1,104 @@ +using System; + +namespace Harmony.ILCopying +{ + public class ByteBuffer + { + public byte[] buffer; + public int position; + + public ByteBuffer(byte[] buffer) + { + this.buffer = buffer; + } + + public byte ReadByte() + { + CheckCanRead(1); + return buffer[position++]; + } + + public byte[] ReadBytes(int length) + { + CheckCanRead(length); + var value = new byte[length]; + Buffer.BlockCopy(buffer, position, value, 0, length); + position += length; + return value; + } + + public short ReadInt16() + { + CheckCanRead(2); + var value = (short)(buffer[position] + | (buffer[position + 1] << 8)); + position += 2; + return value; + } + + public int ReadInt32() + { + CheckCanRead(4); + var value = buffer[position] + | (buffer[position + 1] << 8) + | (buffer[position + 2] << 16) + | (buffer[position + 3] << 24); + position += 4; + return value; + } + + public long ReadInt64() + { + CheckCanRead(8); + var low = (uint)(buffer[position] + | (buffer[position + 1] << 8) + | (buffer[position + 2] << 16) + | (buffer[position + 3] << 24)); + + var high = (uint)(buffer[position + 4] + | (buffer[position + 5] << 8) + | (buffer[position + 6] << 16) + | (buffer[position + 7] << 24)); + + var value = (((long)high) << 32) | low; + position += 8; + return value; + } + + public float ReadSingle() + { + if (!BitConverter.IsLittleEndian) + { + var bytes = ReadBytes(4); + Array.Reverse(bytes); + return BitConverter.ToSingle(bytes, 0); + } + + CheckCanRead(4); + var value = BitConverter.ToSingle(buffer, position); + position += 4; + return value; + } + + public double ReadDouble() + { + if (!BitConverter.IsLittleEndian) + { + var bytes = ReadBytes(8); + Array.Reverse(bytes); + return BitConverter.ToDouble(bytes, 0); + } + + CheckCanRead(8); + var value = BitConverter.ToDouble(buffer, position); + position += 8; + return value; + } + + void CheckCanRead(int count) + { + if (position + count > buffer.Length) + throw new ArgumentOutOfRangeException(); + } + } +} diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/ILCopying/Emitter.cs b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/ILCopying/Emitter.cs new file mode 100644 index 0000000..d57d8ed --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/ILCopying/Emitter.cs @@ -0,0 +1,315 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.InteropServices; + +namespace Harmony.ILCopying +{ + public class LeaveTry + { + public override string ToString() + { + return "(autogenerated)"; + } + } + + public static class Emitter + { + static readonly GetterHandler codeLenGetter = FastAccess.CreateFieldGetter(typeof(ILGenerator), "code_len", "m_length"); + static readonly GetterHandler localsGetter = FastAccess.CreateFieldGetter(typeof(ILGenerator), "locals"); + static readonly GetterHandler localCountGetter = FastAccess.CreateFieldGetter(typeof(ILGenerator), "m_localCount"); + + public static string CodePos(ILGenerator il) + { + var offset = (int)codeLenGetter(il); + return string.Format("L_{0:x4}: ", offset); + } + + public static void LogIL(ILGenerator il, OpCode opCode, object argument) + { + if (HarmonyInstance.DEBUG) + { + var argStr = FormatArgument(argument); + var space = argStr.Length > 0 ? " " : ""; + FileLog.LogBuffered(string.Format("{0}{1}{2}{3}", CodePos(il), opCode, space, argStr)); + } + } + + public static void LogLocalVariable(ILGenerator il, LocalBuilder variable) + { + if (HarmonyInstance.DEBUG) + { + var localCount = -1; + var localsArray = localsGetter != null ? (LocalBuilder[])localsGetter(il) : null; + if (localsArray != null && localsArray.Length > 0) + localCount = localsArray.Length; + else + localCount = (int)localCountGetter(il); + + var str = string.Format("{0}Local var {1}: {2}{3}", CodePos(il), localCount - 1, variable.LocalType.FullName, variable.IsPinned ? "(pinned)" : ""); + FileLog.LogBuffered(str); + } + } + + public static string FormatArgument(object argument) + { + if (argument == null) return "NULL"; + var type = argument.GetType(); + + if (type == typeof(string)) + return "\"" + argument + "\""; + if (type == typeof(Label)) + return "Label" + ((Label)argument).GetHashCode(); + if (type == typeof(Label[])) + return "Labels" + string.Join(",", ((Label[])argument).Select(l => l.GetHashCode().ToString()).ToArray()); + if (type == typeof(LocalBuilder)) + return ((LocalBuilder)argument).LocalIndex + " (" + ((LocalBuilder)argument).LocalType + ")"; + + return argument.ToString().Trim(); + } + + public static void MarkLabel(ILGenerator il, Label label) + { + if (HarmonyInstance.DEBUG) FileLog.LogBuffered(CodePos(il) + FormatArgument(label)); + il.MarkLabel(label); + } + + public static void MarkBlockBefore(ILGenerator il, ExceptionBlock block, out Label? label) + { + label = null; + switch (block.blockType) + { + case ExceptionBlockType.BeginExceptionBlock: + if (HarmonyInstance.DEBUG) + { + FileLog.LogBuffered(".try"); + FileLog.LogBuffered("{"); + FileLog.ChangeIndent(1); + } + label = il.BeginExceptionBlock(); + return; + + case ExceptionBlockType.BeginCatchBlock: + if (HarmonyInstance.DEBUG) + { + // fake log a LEAVE code since BeginCatchBlock() does add it + LogIL(il, OpCodes.Leave, new LeaveTry()); + + FileLog.ChangeIndent(-1); + FileLog.LogBuffered("} // end try"); + + FileLog.LogBuffered(".catch " + block.catchType); + FileLog.LogBuffered("{"); + FileLog.ChangeIndent(1); + } + il.BeginCatchBlock(block.catchType); + return; + + case ExceptionBlockType.BeginExceptFilterBlock: + if (HarmonyInstance.DEBUG) + { + // fake log a LEAVE code since BeginCatchBlock() does add it + LogIL(il, OpCodes.Leave, new LeaveTry()); + + FileLog.ChangeIndent(-1); + FileLog.LogBuffered("} // end try"); + + FileLog.LogBuffered(".filter"); + FileLog.LogBuffered("{"); + FileLog.ChangeIndent(1); + } + il.BeginExceptFilterBlock(); + return; + + case ExceptionBlockType.BeginFaultBlock: + if (HarmonyInstance.DEBUG) + { + // fake log a LEAVE code since BeginCatchBlock() does add it + LogIL(il, OpCodes.Leave, new LeaveTry()); + + FileLog.ChangeIndent(-1); + FileLog.LogBuffered("} // end try"); + + FileLog.LogBuffered(".fault"); + FileLog.LogBuffered("{"); + FileLog.ChangeIndent(1); + } + il.BeginFaultBlock(); + return; + + case ExceptionBlockType.BeginFinallyBlock: + if (HarmonyInstance.DEBUG) + { + // fake log a LEAVE code since BeginCatchBlock() does add it + LogIL(il, OpCodes.Leave, new LeaveTry()); + + FileLog.ChangeIndent(-1); + FileLog.LogBuffered("} // end try"); + + FileLog.LogBuffered(".finally"); + FileLog.LogBuffered("{"); + FileLog.ChangeIndent(1); + } + il.BeginFinallyBlock(); + return; + } + } + + public static void MarkBlockAfter(ILGenerator il, ExceptionBlock block) + { + if (block.blockType == ExceptionBlockType.EndExceptionBlock) + { + if (HarmonyInstance.DEBUG) + { + // fake log a LEAVE code since BeginCatchBlock() does add it + LogIL(il, OpCodes.Leave, new LeaveTry()); + + FileLog.ChangeIndent(-1); + FileLog.LogBuffered("} // end handler"); + } + il.EndExceptionBlock(); + } + } + + // MethodCopier calls when Operand type is InlineNone + public static void Emit(ILGenerator il, OpCode opcode) + { + if (HarmonyInstance.DEBUG) FileLog.LogBuffered(CodePos(il) + opcode); + il.Emit(opcode); + } + + // MethodCopier calls by 3rd argument type + public static void Emit(ILGenerator il, OpCode opcode, LocalBuilder local) + { + LogIL(il, opcode, local); + il.Emit(opcode, local); + } + + // MethodCopier calls by 3rd argument type + public static void Emit(ILGenerator il, OpCode opcode, FieldInfo field) + { + LogIL(il, opcode, field); + il.Emit(opcode, field); + } + + // MethodCopier calls by 3rd argument type + public static void Emit(ILGenerator il, OpCode opcode, Label[] labels) + { + LogIL(il, opcode, labels); + il.Emit(opcode, labels); + } + + // MethodCopier calls by 3rd argument type + public static void Emit(ILGenerator il, OpCode opcode, Label label) + { + LogIL(il, opcode, label); + il.Emit(opcode, label); + } + + // MethodCopier calls by 3rd argument type + public static void Emit(ILGenerator il, OpCode opcode, string str) + { + LogIL(il, opcode, str); + il.Emit(opcode, str); + } + + // MethodCopier calls by 3rd argument type + public static void Emit(ILGenerator il, OpCode opcode, float arg) + { + LogIL(il, opcode, arg); + il.Emit(opcode, arg); + } + + // MethodCopier calls by 3rd argument type + public static void Emit(ILGenerator il, OpCode opcode, byte arg) + { + LogIL(il, opcode, arg); + il.Emit(opcode, arg); + } + + // MethodCopier calls by 3rd argument type + public static void Emit(ILGenerator il, OpCode opcode, sbyte arg) + { + LogIL(il, opcode, arg); + il.Emit(opcode, arg); + } + + // MethodCopier calls by 3rd argument type + public static void Emit(ILGenerator il, OpCode opcode, double arg) + { + LogIL(il, opcode, arg); + il.Emit(opcode, arg); + } + + // MethodCopier calls by 3rd argument type + public static void Emit(ILGenerator il, OpCode opcode, int arg) + { + LogIL(il, opcode, arg); + il.Emit(opcode, arg); + } + + // MethodCopier calls by 3rd argument type + public static void Emit(ILGenerator il, OpCode opcode, MethodInfo meth) + { + LogIL(il, opcode, meth); + il.Emit(opcode, meth); + } + + // MethodCopier calls by 3rd argument type + public static void Emit(ILGenerator il, OpCode opcode, short arg) + { + LogIL(il, opcode, arg); + il.Emit(opcode, arg); + } + + // MethodCopier calls by 3rd argument type + public static void Emit(ILGenerator il, OpCode opcode, SignatureHelper signature) + { + LogIL(il, opcode, signature); + il.Emit(opcode, signature); + } + + // MethodCopier calls by 3rd argument type + public static void Emit(ILGenerator il, OpCode opcode, ConstructorInfo con) + { + LogIL(il, opcode, con); + il.Emit(opcode, con); + } + + // MethodCopier calls by 3rd argument type + public static void Emit(ILGenerator il, OpCode opcode, Type cls) + { + LogIL(il, opcode, cls); + il.Emit(opcode, cls); + } + + // MethodCopier calls by 3rd argument type + public static void Emit(ILGenerator il, OpCode opcode, long arg) + { + LogIL(il, opcode, arg); + il.Emit(opcode, arg); + } + + // called from MethodInvoker (calls from MethodCopier use the corresponding Emit() call above) + public static void EmitCall(ILGenerator il, OpCode opcode, MethodInfo methodInfo, Type[] optionalParameterTypes) + { + if (HarmonyInstance.DEBUG) FileLog.LogBuffered(string.Format("{0}Call {1} {2} {3}", CodePos(il), opcode, methodInfo, optionalParameterTypes)); + il.EmitCall(opcode, methodInfo, optionalParameterTypes); + } + + // not called yet + public static void EmitCalli(ILGenerator il, OpCode opcode, CallingConvention unmanagedCallConv, Type returnType, Type[] parameterTypes) + { + if (HarmonyInstance.DEBUG) FileLog.LogBuffered(string.Format("{0}Calli {1} {2} {3} {4}", CodePos(il), opcode, unmanagedCallConv, returnType, parameterTypes)); + il.EmitCalli(opcode, unmanagedCallConv, returnType, parameterTypes); + } + + // not called yet + public static void EmitCalli(ILGenerator il, OpCode opcode, CallingConventions callingConvention, Type returnType, Type[] parameterTypes, Type[] optionalParameterTypes) + { + if (HarmonyInstance.DEBUG) FileLog.LogBuffered(string.Format("{0}Calli {1} {2} {3} {4} {5}", CodePos(il), opcode, callingConvention, returnType, parameterTypes, optionalParameterTypes)); + il.EmitCalli(opcode, callingConvention, returnType, parameterTypes, optionalParameterTypes); + } + } +} \ No newline at end of file diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/ILCopying/ILInstruction.cs b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/ILCopying/ILInstruction.cs new file mode 100644 index 0000000..3c8c8b9 --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/ILCopying/ILInstruction.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Reflection.Emit; + +namespace Harmony.ILCopying +{ + public enum ExceptionBlockType + { + BeginExceptionBlock, + BeginCatchBlock, + BeginExceptFilterBlock, + BeginFaultBlock, + BeginFinallyBlock, + EndExceptionBlock + } + + public class ExceptionBlock + { + public ExceptionBlockType blockType; + public Type catchType; + + public ExceptionBlock(ExceptionBlockType blockType, Type catchType) + { + this.blockType = blockType; + this.catchType = catchType; + } + } + + public class ILInstruction + { + public int offset; + public OpCode opcode; + public object operand; + public object argument; + + public List labels = new List(); + public List blocks = new List(); + + public ILInstruction(OpCode opcode, object operand = null) + { + this.opcode = opcode; + this.operand = operand; + argument = operand; + } + + public CodeInstruction GetCodeInstruction() + { + var instr = new CodeInstruction(opcode, argument); + if (opcode.OperandType == OperandType.InlineNone) + instr.operand = null; + instr.labels = labels; + instr.blocks = blocks; + return instr; + } + + public int GetSize() + { + var size = opcode.Size; + + switch (opcode.OperandType) + { + case OperandType.InlineSwitch: + size += (1 + ((Array)operand).Length) * 4; + break; + + case OperandType.InlineI8: + case OperandType.InlineR: + size += 8; + break; + + case OperandType.InlineBrTarget: + case OperandType.InlineField: + case OperandType.InlineI: + case OperandType.InlineMethod: + case OperandType.InlineSig: + case OperandType.InlineString: + case OperandType.InlineTok: + case OperandType.InlineType: + case OperandType.ShortInlineR: + size += 4; + break; + + case OperandType.InlineVar: + size += 2; + break; + + case OperandType.ShortInlineBrTarget: + case OperandType.ShortInlineI: + case OperandType.ShortInlineVar: + size += 1; + break; + } + + return size; + } + + public override string ToString() + { + var instruction = ""; + + AppendLabel(ref instruction, this); + instruction = instruction + ": " + opcode.Name; + + if (operand == null) + return instruction; + + instruction = instruction + " "; + + switch (opcode.OperandType) + { + case OperandType.ShortInlineBrTarget: + case OperandType.InlineBrTarget: + AppendLabel(ref instruction, operand); + break; + + case OperandType.InlineSwitch: + var switchLabels = (ILInstruction[])operand; + for (var i = 0; i < switchLabels.Length; i++) + { + if (i > 0) + instruction = instruction + ","; + + AppendLabel(ref instruction, switchLabels[i]); + } + break; + + case OperandType.InlineString: + instruction = instruction + "\"" + operand + "\""; + break; + + default: + instruction = instruction + operand; + break; + } + + return instruction; + } + + static void AppendLabel(ref string str, object argument) + { + var instruction = argument as ILInstruction; + if (instruction != null) + str = str + "IL_" + instruction.offset.ToString("X4"); + else + str = str + "IL_" + argument; + } + } +} diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/ILCopying/Memory.cs b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/ILCopying/Memory.cs new file mode 100644 index 0000000..f7e86f0 --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/ILCopying/Memory.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Harmony.ILCopying +{ + [Flags] + public enum Protection + { + PAGE_NOACCESS = 0x01, + PAGE_READONLY = 0x02, + PAGE_READWRITE = 0x04, + PAGE_WRITECOPY = 0x08, + PAGE_EXECUTE = 0x10, + PAGE_EXECUTE_READ = 0x20, + PAGE_EXECUTE_READWRITE = 0x40, + PAGE_EXECUTE_WRITECOPY = 0x80, + PAGE_GUARD = 0x100, + PAGE_NOCACHE = 0x200, + PAGE_WRITECOMBINE = 0x400 + } + + public static class Memory + { + private static readonly HashSet WindowsPlatformIDSet = new HashSet + { + PlatformID.Win32NT, PlatformID.Win32S, PlatformID.Win32Windows, PlatformID.WinCE + }; + + public static bool IsWindows => WindowsPlatformIDSet.Contains(Environment.OSVersion.Platform); + + // Safe to use windows reference since this will only ever be called on windows + // + [DllImport("kernel32.dll")] + public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, Protection flNewProtect, out Protection lpflOldProtect); + + public static void UnprotectMemoryPage(long memory) + { + if (IsWindows) + { + var success = VirtualProtect(new IntPtr(memory), new UIntPtr(1), Protection.PAGE_EXECUTE_READWRITE, out var _ignored); + if (success == false) + throw new System.ComponentModel.Win32Exception(); + } + } + + public static string DetourMethod(MethodBase original, MethodBase replacement) + { + var originalCodeStart = GetMethodStart(original, out var exception); + if (originalCodeStart == 0) + return exception.Message; + var patchCodeStart = GetMethodStart(replacement, out exception); + if (patchCodeStart == 0) + return exception.Message; + + return WriteJump(originalCodeStart, patchCodeStart); + } + + /* + * This is still a rough part in Harmony. So much information and no easy way + * to determine when and what is valid. Especially with different environments + * and .NET versions. More information might be found here: + * + * https://stackoverflow.com/questions/38782934/how-to-replace-the-pointer-to-the-overridden-virtual-method-in-the-pointer-of/ + * https://stackoverflow.com/questions/39034018/how-to-replace-a-pointer-to-a-pointer-to-a-method-in-a-class-of-my-method-inheri + * + */ + public static string WriteJump(long memory, long destination) + { + UnprotectMemoryPage(memory); + + if (IntPtr.Size == sizeof(long)) + { + if (CompareBytes(memory, new byte[] { 0xe9 })) + { + var offset = ReadInt(memory + 1); + memory += 5 + offset; + } + + memory = WriteBytes(memory, new byte[] { 0x48, 0xB8 }); + memory = WriteLong(memory, destination); + memory = WriteBytes(memory, new byte[] { 0xFF, 0xE0 }); + } + else + { + memory = WriteByte(memory, 0x68); + memory = WriteInt(memory, (int)destination); + memory = WriteByte(memory, 0xc3); + } + return null; + } + + private static RuntimeMethodHandle GetRuntimeMethodHandle(MethodBase method) + { + if (method is DynamicMethod) + { + var nonPublicInstance = BindingFlags.NonPublic | BindingFlags.Instance; + + // DynamicMethod actually generates its m_methodHandle on-the-fly and therefore + // we should call GetMethodDescriptor to force it to be created + // + var m_GetMethodDescriptor = typeof(DynamicMethod).GetMethod("GetMethodDescriptor", nonPublicInstance); + if (m_GetMethodDescriptor != null) + return (RuntimeMethodHandle)m_GetMethodDescriptor.Invoke(method, new object[0]); + + // .Net Core + var f_m_method = typeof(DynamicMethod).GetField("m_method", nonPublicInstance); + if (f_m_method != null) + return (RuntimeMethodHandle)f_m_method.GetValue(method); + + // Mono + var f_mhandle = typeof(DynamicMethod).GetField("mhandle", nonPublicInstance); + return (RuntimeMethodHandle)f_mhandle.GetValue(method); + } + + return method.MethodHandle; + } + + public static long GetMethodStart(MethodBase method, out Exception exception) + { + // required in .NET Core so that the method is JITed and the method start does not change + // + var handle = GetRuntimeMethodHandle(method); + try + { + RuntimeHelpers.PrepareMethod(handle); + } + catch (Exception) + { + } + + try + { + exception = null; + return handle.GetFunctionPointer().ToInt64(); + } + catch (Exception ex) + { + exception = ex; + return 0; + } + } + + public static unsafe bool CompareBytes(long memory, byte[] values) + { + var p = (byte*)memory; + foreach (var value in values) + { + if (value != *p) return false; + p++; + } + return true; + } + + public static unsafe byte ReadByte(long memory) + { + var p = (byte*)memory; + return *p; + } + + public static unsafe int ReadInt(long memory) + { + var p = (int*)memory; + return *p; + } + + public static unsafe long ReadLong(long memory) + { + var p = (long*)memory; + return *p; + } + + public static unsafe long WriteByte(long memory, byte value) + { + var p = (byte*)memory; + *p = value; + return memory + sizeof(byte); + } + + public static unsafe long WriteBytes(long memory, byte[] values) + { + foreach (var value in values) + memory = WriteByte(memory, value); + return memory; + } + + public static unsafe long WriteInt(long memory, int value) + { + var p = (int*)memory; + *p = value; + return memory + sizeof(int); + } + + public static unsafe long WriteLong(long memory, long value) + { + var p = (long*)memory; + *p = value; + return memory + sizeof(long); + } + } +} diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/ILCopying/MethodCopier.cs b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/ILCopying/MethodCopier.cs new file mode 100644 index 0000000..8344ca9 --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/ILCopying/MethodCopier.cs @@ -0,0 +1,730 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; + +namespace Harmony.ILCopying +{ + public class MethodCopier + { + readonly MethodBodyReader reader; + readonly List transpilers = new List(); + + public MethodCopier(MethodBase fromMethod, ILGenerator toILGenerator, LocalBuilder[] existingVariables = null) + { + if (fromMethod == null) throw new ArgumentNullException("Method cannot be null"); + reader = new MethodBodyReader(fromMethod, toILGenerator); + reader.DeclareVariables(existingVariables); + reader.ReadInstructions(); + } + + public void AddTranspiler(MethodInfo transpiler) + { + transpilers.Add(transpiler); + } + + public void Finalize(List endLabels, List endBlocks) + { + reader.FinalizeILCodes(transpilers, endLabels, endBlocks); + } + } + + public class MethodBodyReader + { + readonly ILGenerator generator; + + readonly MethodBase method; + readonly Module module; + readonly Type[] typeArguments; + readonly Type[] methodArguments; + readonly ByteBuffer ilBytes; + readonly ParameterInfo this_parameter; + readonly ParameterInfo[] parameters; + readonly IList locals; + readonly IList exceptions; + List ilInstructions; + + LocalBuilder[] variables; + + // NOTE: you cannot simply "copy" ILInstructions from a method. They contain references to + // local variables which must be CREATED on an ILGenerator or else they are invalid when you + // want to use the ILInstruction. If you are really clever, you can supply a dummy generator + // and edit out all labels during the processing but that might be more trickier than you think + // + // In order to copy together a bunch of method parts within a transpiler, you have to pass in + // your current generator that builds your new method + // + // You will end up with the sum of all declared local variables of all methods you run + // GetInstructions on or use a dummy generator but edit out the invalid labels from the codes + // you copy + // + public static List GetInstructions(ILGenerator generator, MethodBase method) + { + if (method == null) throw new ArgumentNullException("Method cannot be null"); + var reader = new MethodBodyReader(method, generator); + reader.DeclareVariables(null); + reader.ReadInstructions(); + return reader.ilInstructions; + } + + // constructor + // + public MethodBodyReader(MethodBase method, ILGenerator generator) + { + this.generator = generator; + this.method = method; + module = method.Module; + + var body = method.GetMethodBody(); + if (body == null) + throw new ArgumentException("Method " + method.FullDescription() + " has no body"); + + var bytes = body.GetILAsByteArray(); + if (bytes == null) + throw new ArgumentException("Can not get IL bytes of method " + method.FullDescription()); + ilBytes = new ByteBuffer(bytes); + ilInstructions = new List((bytes.Length + 1) / 2); + + var type = method.DeclaringType; + + if (type.IsGenericType) + { + try { typeArguments = type.GetGenericArguments(); } + catch { typeArguments = null; } + } + + if (method.IsGenericMethod) + { + try { methodArguments = method.GetGenericArguments(); } + catch { methodArguments = null; } + } + + if (!method.IsStatic) + this_parameter = new ThisParameter(method); + parameters = method.GetParameters(); + + locals = body.LocalVariables; + exceptions = body.ExceptionHandlingClauses; + } + + // read and parse IL codes + // + public void ReadInstructions() + { + while (ilBytes.position < ilBytes.buffer.Length) + { + var loc = ilBytes.position; // get location first (ReadOpCode will advance it) + var instruction = new ILInstruction(ReadOpCode()) { offset = loc }; + ReadOperand(instruction); + ilInstructions.Add(instruction); + } + + ResolveBranches(); + ParseExceptions(); + } + + // declare local variables + // + public void DeclareVariables(LocalBuilder[] existingVariables) + { + if (generator == null) return; + if (existingVariables != null) + variables = existingVariables; + else + variables = locals.Select( + lvi => generator.DeclareLocal(lvi.LocalType, lvi.IsPinned) + ).ToArray(); + } + + // process all jumps + // + void ResolveBranches() + { + foreach (var ilInstruction in ilInstructions) + { + switch (ilInstruction.opcode.OperandType) + { + case OperandType.ShortInlineBrTarget: + case OperandType.InlineBrTarget: + ilInstruction.operand = GetInstruction((int)ilInstruction.operand, false); + break; + + case OperandType.InlineSwitch: + var offsets = (int[])ilInstruction.operand; + var branches = new ILInstruction[offsets.Length]; + for (var j = 0; j < offsets.Length; j++) + branches[j] = GetInstruction(offsets[j], false); + + ilInstruction.operand = branches; + break; + } + } + } + + // process all exception blocks + // + void ParseExceptions() + { + foreach (var exception in exceptions) + { + var try_start = exception.TryOffset; + var try_end = exception.TryOffset + exception.TryLength - 1; + + var handler_start = exception.HandlerOffset; + var handler_end = exception.HandlerOffset + exception.HandlerLength - 1; + + var instr1 = GetInstruction(try_start, false); + instr1.blocks.Add(new ExceptionBlock(ExceptionBlockType.BeginExceptionBlock, null)); + + var instr2 = GetInstruction(handler_end, true); + instr2.blocks.Add(new ExceptionBlock(ExceptionBlockType.EndExceptionBlock, null)); + + // The FilterOffset property is meaningful only for Filter clauses. + // The CatchType property is not meaningful for Filter or Finally clauses. + // + switch (exception.Flags) + { + case ExceptionHandlingClauseOptions.Filter: + var instr3 = GetInstruction(exception.FilterOffset, false); + instr3.blocks.Add(new ExceptionBlock(ExceptionBlockType.BeginExceptFilterBlock, null)); + break; + + case ExceptionHandlingClauseOptions.Finally: + var instr4 = GetInstruction(handler_start, false); + instr4.blocks.Add(new ExceptionBlock(ExceptionBlockType.BeginFinallyBlock, null)); + break; + + case ExceptionHandlingClauseOptions.Clause: + var instr5 = GetInstruction(handler_start, false); + instr5.blocks.Add(new ExceptionBlock(ExceptionBlockType.BeginCatchBlock, exception.CatchType)); + break; + + case ExceptionHandlingClauseOptions.Fault: + var instr6 = GetInstruction(handler_start, false); + instr6.blocks.Add(new ExceptionBlock(ExceptionBlockType.BeginFaultBlock, null)); + break; + } + } + } + + // used in FinalizeILCodes to convert short jumps to long ones + static Dictionary shortJumps = new Dictionary() + { + { OpCodes.Leave_S, OpCodes.Leave }, + { OpCodes.Brfalse_S, OpCodes.Brfalse }, + { OpCodes.Brtrue_S, OpCodes.Brtrue }, + { OpCodes.Beq_S, OpCodes.Beq }, + { OpCodes.Bge_S, OpCodes.Bge }, + { OpCodes.Bgt_S, OpCodes.Bgt }, + { OpCodes.Ble_S, OpCodes.Ble }, + { OpCodes.Blt_S, OpCodes.Blt }, + { OpCodes.Bne_Un_S, OpCodes.Bne_Un }, + { OpCodes.Bge_Un_S, OpCodes.Bge_Un }, + { OpCodes.Bgt_Un_S, OpCodes.Bgt_Un }, + { OpCodes.Ble_Un_S, OpCodes.Ble_Un }, + { OpCodes.Br_S, OpCodes.Br }, + { OpCodes.Blt_Un_S, OpCodes.Blt_Un } + }; + + // use parsed IL codes and emit them to a generator + // + public void FinalizeILCodes(List transpilers, List endLabels, List endBlocks) + { + if (generator == null) return; + + // pass1 - define labels and add them to instructions that are target of a jump + // + foreach (var ilInstruction in ilInstructions) + { + switch (ilInstruction.opcode.OperandType) + { + case OperandType.InlineSwitch: + { + var targets = ilInstruction.operand as ILInstruction[]; + if (targets != null) + { + var labels = new List(); + foreach (var target in targets) + { + var label = generator.DefineLabel(); + target.labels.Add(label); + labels.Add(label); + } + ilInstruction.argument = labels.ToArray(); + } + break; + } + + case OperandType.ShortInlineBrTarget: + case OperandType.InlineBrTarget: + { + var target = ilInstruction.operand as ILInstruction; + if (target != null) + { + var label = generator.DefineLabel(); + target.labels.Add(label); + ilInstruction.argument = label; + } + break; + } + } + } + + // pass2 - filter through all processors + // + var codeTranspiler = new CodeTranspiler(ilInstructions); + transpilers.Do(transpiler => codeTranspiler.Add(transpiler)); + var codeInstructions = codeTranspiler.GetResult(generator, method); + + // pass3 - remove RET if it appears at the end + while (true) + { + var lastInstruction = codeInstructions.LastOrDefault(); + if (lastInstruction == null || lastInstruction.opcode != OpCodes.Ret) break; + + // remember any existing labels + endLabels.AddRange(lastInstruction.labels); + + codeInstructions.RemoveAt(codeInstructions.Count - 1); + } + + // pass4 - mark labels and exceptions and emit codes + // + var idx = 0; + codeInstructions.Do(codeInstruction => + { + // mark all labels + codeInstruction.labels.Do(label => Emitter.MarkLabel(generator, label)); + + // start all exception blocks + // TODO: we ignore the resulting label because we have no way to use it + // + codeInstruction.blocks.Do(block => { Label? label; Emitter.MarkBlockBefore(generator, block, out label); }); + + var code = codeInstruction.opcode; + var operand = codeInstruction.operand; + + // replace RET with a jump to the end (outside this code) + if (code == OpCodes.Ret) + { + var endLabel = generator.DefineLabel(); + code = OpCodes.Br; + operand = endLabel; + endLabels.Add(endLabel); + } + + // replace short jumps with long ones (can be optimized but requires byte counting, not instruction counting) + if (shortJumps.TryGetValue(code, out var longJump)) + code = longJump; + + var emitCode = true; + + //if (code == OpCodes.Leave || code == OpCodes.Leave_S) + //{ + // // skip LEAVE on EndExceptionBlock + // if (codeInstruction.blocks.Any(block => block.blockType == ExceptionBlockType.EndExceptionBlock)) + // emitCode = false; + + // // skip LEAVE on next instruction starts a new exception handler and we are already in + // if (idx < instructions.Length - 1) + // if (instructions[idx + 1].blocks.Any(block => block.blockType != ExceptionBlockType.EndExceptionBlock)) + // emitCode = false; + //} + + if (emitCode) + { + switch (code.OperandType) + { + case OperandType.InlineNone: + Emitter.Emit(generator, code); + break; + + case OperandType.InlineSig: + + // TODO the following will fail because we do not convert the token (operand) + // All the decompilers can show the arguments correctly, we just need to find out how + // + if (operand == null) throw new Exception("Wrong null argument: " + codeInstruction); + if ((operand is int) == false) throw new Exception("Wrong Emit argument type " + operand.GetType() + " in " + codeInstruction); + Emitter.Emit(generator, code, (int)operand); + + /* + // the following will only work if we can convert the original signature token to the required arguments + // + var callingConvention = System.Runtime.InteropServices.CallingConvention.ThisCall; + var returnType = typeof(object); + var parameterTypes = new[] { typeof(object) }; + Emitter.EmitCalli(generator, code, callingConvention, returnType, parameterTypes); + + var callingConventions = System.Reflection.CallingConventions.Standard; + var optionalParameterTypes = new[] { typeof(object) }; + Emitter.EmitCalli(generator, code, callingConventions, returnType, parameterTypes, optionalParameterTypes); + */ + break; + + default: + if (operand == null) throw new Exception("Wrong null argument: " + codeInstruction); + var emitMethod = EmitMethodForType(operand.GetType()); + if (emitMethod == null) throw new Exception("Unknown Emit argument type " + operand.GetType() + " in " + codeInstruction); + if (HarmonyInstance.DEBUG) FileLog.LogBuffered(Emitter.CodePos(generator) + code + " " + Emitter.FormatArgument(operand)); + emitMethod.Invoke(generator, new object[] { code, operand }); + break; + } + } + + codeInstruction.blocks.Do(block => Emitter.MarkBlockAfter(generator, block)); + + idx++; + }); + } + + // interpret member info value + // + static void GetMemberInfoValue(MemberInfo info, out object result) + { + result = null; + switch (info.MemberType) + { + case MemberTypes.Constructor: + result = (ConstructorInfo)info; + break; + + case MemberTypes.Event: + result = (EventInfo)info; + break; + + case MemberTypes.Field: + result = (FieldInfo)info; + break; + + case MemberTypes.Method: + result = (MethodInfo)info; + break; + + case MemberTypes.TypeInfo: + case MemberTypes.NestedType: + result = (Type)info; + break; + + case MemberTypes.Property: + result = (PropertyInfo)info; + break; + } + } + + // interpret instruction operand + // + void ReadOperand(ILInstruction instruction) + { + switch (instruction.opcode.OperandType) + { + case OperandType.InlineNone: + { + instruction.argument = null; + break; + } + + case OperandType.InlineSwitch: + { + var length = ilBytes.ReadInt32(); + var base_offset = ilBytes.position + (4 * length); + var branches = new int[length]; + for (var i = 0; i < length; i++) + branches[i] = ilBytes.ReadInt32() + base_offset; + instruction.operand = branches; + break; + } + + case OperandType.ShortInlineBrTarget: + { + var val = (sbyte)ilBytes.ReadByte(); + instruction.operand = val + ilBytes.position; + break; + } + + case OperandType.InlineBrTarget: + { + var val = ilBytes.ReadInt32(); + instruction.operand = val + ilBytes.position; + break; + } + + case OperandType.ShortInlineI: + { + if (instruction.opcode == OpCodes.Ldc_I4_S) + { + var sb = (sbyte)ilBytes.ReadByte(); + instruction.operand = sb; + instruction.argument = (sbyte)instruction.operand; + } + else + { + var b = ilBytes.ReadByte(); + instruction.operand = b; + instruction.argument = (byte)instruction.operand; + } + break; + } + + case OperandType.InlineI: + { + var val = ilBytes.ReadInt32(); + instruction.operand = val; + instruction.argument = (int)instruction.operand; + break; + } + + case OperandType.ShortInlineR: + { + var val = ilBytes.ReadSingle(); + instruction.operand = val; + instruction.argument = (float)instruction.operand; + break; + } + + case OperandType.InlineR: + { + var val = ilBytes.ReadDouble(); + instruction.operand = val; + instruction.argument = (double)instruction.operand; + break; + } + + case OperandType.InlineI8: + { + var val = ilBytes.ReadInt64(); + instruction.operand = val; + instruction.argument = (long)instruction.operand; + break; + } + + case OperandType.InlineSig: + { + var val = ilBytes.ReadInt32(); + var bytes = module.ResolveSignature(val); + instruction.operand = bytes; + instruction.argument = bytes; + Debugger.Log(0, "TEST", "METHOD " + method.FullDescription() + "\n"); + Debugger.Log(0, "TEST", "Signature = " + bytes.Select(b => string.Format("0x{0:x02}", b)).Aggregate((a, b) => a + " " + b) + "\n"); + Debugger.Break(); + break; + } + + case OperandType.InlineString: + { + var val = ilBytes.ReadInt32(); + instruction.operand = module.ResolveString(val); + instruction.argument = (string)instruction.operand; + break; + } + + case OperandType.InlineTok: + { + var val = ilBytes.ReadInt32(); + instruction.operand = module.ResolveMember(val, typeArguments, methodArguments); + GetMemberInfoValue((MemberInfo)instruction.operand, out instruction.argument); + break; + } + + case OperandType.InlineType: + { + var val = ilBytes.ReadInt32(); + instruction.operand = module.ResolveType(val, typeArguments, methodArguments); + instruction.argument = (Type)instruction.operand; + break; + } + + case OperandType.InlineMethod: + { + var val = ilBytes.ReadInt32(); + instruction.operand = module.ResolveMethod(val, typeArguments, methodArguments); + if (instruction.operand is ConstructorInfo) + instruction.argument = (ConstructorInfo)instruction.operand; + else + instruction.argument = (MethodInfo)instruction.operand; + break; + } + + case OperandType.InlineField: + { + var val = ilBytes.ReadInt32(); + instruction.operand = module.ResolveField(val, typeArguments, methodArguments); + instruction.argument = (FieldInfo)instruction.operand; + break; + } + + case OperandType.ShortInlineVar: + { + var idx = ilBytes.ReadByte(); + if (TargetsLocalVariable(instruction.opcode)) + { + var lvi = GetLocalVariable(idx); + if (lvi == null) + instruction.argument = idx; + else + { + instruction.operand = lvi; + instruction.argument = variables[lvi.LocalIndex]; + } + } + else + { + instruction.operand = GetParameter(idx); + instruction.argument = idx; + } + break; + } + + case OperandType.InlineVar: + { + var idx = ilBytes.ReadInt16(); + if (TargetsLocalVariable(instruction.opcode)) + { + var lvi = GetLocalVariable(idx); + if (lvi == null) + instruction.argument = idx; + else + { + instruction.operand = lvi; + instruction.argument = variables[lvi.LocalIndex]; + } + } + else + { + instruction.operand = GetParameter(idx); + instruction.argument = idx; + } + break; + } + + default: + throw new NotSupportedException(); + } + } + + ILInstruction GetInstruction(int offset, bool isEndOfInstruction) + { + var lastInstructionIndex = ilInstructions.Count - 1; + if (offset < 0 || offset > ilInstructions[lastInstructionIndex].offset) + throw new Exception("Instruction offset " + offset + " is outside valid range 0 - " + ilInstructions[lastInstructionIndex].offset); + + var min = 0; + var max = lastInstructionIndex; + while (min <= max) + { + var mid = min + ((max - min) / 2); + var instruction = ilInstructions[mid]; + + if (isEndOfInstruction) + { + if (offset == instruction.offset + instruction.GetSize() - 1) + return instruction; + } + else + { + if (offset == instruction.offset) + return instruction; + } + + if (offset < instruction.offset) + max = mid - 1; + else + min = mid + 1; + } + + throw new Exception("Cannot find instruction for " + offset.ToString("X4")); + } + + static bool TargetsLocalVariable(OpCode opcode) + { + return opcode.Name.Contains("loc"); + } + + LocalVariableInfo GetLocalVariable(int index) + { + return locals?[index]; + } + + ParameterInfo GetParameter(int index) + { + if (index == 0) + return this_parameter; + + return parameters[index - 1]; + } + + OpCode ReadOpCode() + { + var op = ilBytes.ReadByte(); + return op != 0xfe + ? one_byte_opcodes[op] + : two_bytes_opcodes[ilBytes.ReadByte()]; + } + + MethodInfo EmitMethodForType(Type type) + { + foreach (var entry in emitMethods) + if (entry.Key == type) return entry.Value; + foreach (var entry in emitMethods) + if (entry.Key.IsAssignableFrom(type)) return entry.Value; + return null; + } + + // static initializer to prep opcodes + + static readonly OpCode[] one_byte_opcodes; + static readonly OpCode[] two_bytes_opcodes; + + static readonly Dictionary emitMethods; + + [MethodImpl(MethodImplOptions.Synchronized)] + static MethodBodyReader() + { + one_byte_opcodes = new OpCode[0xe1]; + two_bytes_opcodes = new OpCode[0x1f]; + + var fields = typeof(OpCodes).GetFields( + BindingFlags.Public | BindingFlags.Static); + + foreach (var field in fields) + { + var opcode = (OpCode)field.GetValue(null); + if (opcode.OpCodeType == OpCodeType.Nternal) + continue; + + if (opcode.Size == 1) + one_byte_opcodes[opcode.Value] = opcode; + else + two_bytes_opcodes[opcode.Value & 0xff] = opcode; + } + + emitMethods = new Dictionary(); + typeof(ILGenerator).GetMethods().ToList() + .Do(method => + { + if (method.Name != "Emit") return; + var pinfos = method.GetParameters(); + if (pinfos.Length != 2) return; + var types = pinfos.Select(p => p.ParameterType).ToArray(); + if (types[0] != typeof(OpCode)) return; + emitMethods[types[1]] = method; + }); + } + + // a custom this parameter + + class ThisParameter : ParameterInfo + { + public ThisParameter(MethodBase method) + { + MemberImpl = method; + ClassImpl = method.DeclaringType; + NameImpl = "this"; + PositionImpl = -1; + } + } + } +} \ No newline at end of file diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/ILCopying/Signature.cs b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/ILCopying/Signature.cs new file mode 100644 index 0000000..fae8a20 --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/ILCopying/Signature.cs @@ -0,0 +1,271 @@ +using System; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.Remoting.Contexts; +using System.Linq; + +namespace Harmony.ILCopying +{ + /* + * TODO - this needs to be worked on. The purpose is to decode the signature into + * a high level reflection based calling signature that is valid in the + * current assembly + * + * See also where OperandType.InlineSig is handled in MethodCopier.cs + * + public static class Signature + { + internal const byte DEFAULT = 0x00; + internal const byte VARARG = 0x05; + internal const byte GENERIC = 0x10; + internal const byte HASTHIS = 0x20; + internal const byte EXPLICITTHIS = 0x40; + internal const byte FIELD = 0x06; + internal const byte LOCAL_SIG = 0x07; + internal const byte PROPERTY = 0x08; + internal const byte GENERICINST = 0x0A; + internal const byte SENTINEL = 0x41; + internal const byte ELEMENT_TYPE_VOID = 0x01; + internal const byte ELEMENT_TYPE_BOOLEAN = 0x02; + internal const byte ELEMENT_TYPE_CHAR = 0x03; + internal const byte ELEMENT_TYPE_I1 = 0x04; + internal const byte ELEMENT_TYPE_U1 = 0x05; + internal const byte ELEMENT_TYPE_I2 = 0x06; + internal const byte ELEMENT_TYPE_U2 = 0x07; + internal const byte ELEMENT_TYPE_I4 = 0x08; + internal const byte ELEMENT_TYPE_U4 = 0x09; + internal const byte ELEMENT_TYPE_I8 = 0x0a; + internal const byte ELEMENT_TYPE_U8 = 0x0b; + internal const byte ELEMENT_TYPE_R4 = 0x0c; + internal const byte ELEMENT_TYPE_R8 = 0x0d; + internal const byte ELEMENT_TYPE_STRING = 0x0e; + internal const byte ELEMENT_TYPE_PTR = 0x0f; + internal const byte ELEMENT_TYPE_BYREF = 0x10; + internal const byte ELEMENT_TYPE_VALUETYPE = 0x11; + internal const byte ELEMENT_TYPE_CLASS = 0x12; + internal const byte ELEMENT_TYPE_VAR = 0x13; + internal const byte ELEMENT_TYPE_ARRAY = 0x14; + internal const byte ELEMENT_TYPE_GENERICINST = 0x15; + internal const byte ELEMENT_TYPE_TYPEDBYREF = 0x16; + internal const byte ELEMENT_TYPE_I = 0x18; + internal const byte ELEMENT_TYPE_U = 0x19; + internal const byte ELEMENT_TYPE_FNPTR = 0x1b; + internal const byte ELEMENT_TYPE_OBJECT = 0x1c; + internal const byte ELEMENT_TYPE_SZARRAY = 0x1d; + internal const byte ELEMENT_TYPE_MVAR = 0x1e; + internal const byte ELEMENT_TYPE_CMOD_REQD = 0x1f; + internal const byte ELEMENT_TYPE_CMOD_OPT = 0x20; + internal const byte ELEMENT_TYPE_PINNED = 0x45; + + static int ReadCompressedUInt(byte[] bytes, ref int n) + { + var b1 = bytes[n++]; + if (b1 <= 0x7F) + { + return b1; + } + else if ((b1 & 0xC0) == 0x80) + { + var b2 = bytes[n++]; + return ((b1 & 0x3F) << 8) | b2; + } + else + { + var b2 = bytes[n++]; + var b3 = bytes[n++]; + var b4 = bytes[n++]; + return ((b1 & 0x3F) << 24) + (b2 << 16) + (b3 << 8) + b4; + } + } + + static Type ReadTypeOrVoid(Module module, byte[] bytes, ref int n, Context context) + { + if (bytes[n] == ELEMENT_TYPE_VOID) + { + n++; + return module.GetType("System.Void"); + } + else + { + return ReadType(module, br, context); + } + } + + static Type ReadFunctionPointer(Module module, byte[] bytes, ref int n, Context context) + { + var len = bytes.Length - n; + var newBytes = new byte[len]; + for (var i = 0; i < len; i++) + newBytes[i] = bytes[n + i]; + + var sig = ReadStandaloneSignature(module, newBytes, context); + if (module.universe.EnableFunctionPointers) + { + return FunctionPointerType.Make(module.universe, sig); + } + else + { + // by default, like .NET we return System.IntPtr here + return module.universe.System_IntPtr; + } + } + + static Type ReadType(Module module, byte[] bytes, ref int n, Context context) + { + CustomModifiers mods; + switch (bytes[n++]) + { + case ELEMENT_TYPE_CLASS: + return ReadTypeDefOrRefEncoded(module, br, context).MarkNotValueType(); + case ELEMENT_TYPE_VALUETYPE: + return ReadTypeDefOrRefEncoded(module, br, context).MarkValueType(); + case ELEMENT_TYPE_BOOLEAN: + return module.GetType("System.Boolean"); + case ELEMENT_TYPE_CHAR: + return module.GetType("System.Char"); + case ELEMENT_TYPE_I1: + return module.GetType("System.SByte"); + case ELEMENT_TYPE_U1: + return module.GetType("System.Byte"); + case ELEMENT_TYPE_I2: + return module.GetType("System.Int16"); + case ELEMENT_TYPE_U2: + return module.GetType("System.UInt16"); + case ELEMENT_TYPE_I4: + return module.GetType("System.Int32"); + case ELEMENT_TYPE_U4: + return module.GetType("System.UInt32"); + case ELEMENT_TYPE_I8: + return module.GetType("System.Int64"); + case ELEMENT_TYPE_U8: + return module.GetType("System.UInt64"); + case ELEMENT_TYPE_R4: + return module.GetType("System.Single"); + case ELEMENT_TYPE_R8: + return module.GetType("System.Double"); + case ELEMENT_TYPE_I: + return module.GetType("System.IntPtr"); + case ELEMENT_TYPE_U: + return module.GetType("System.UIntPtr"); + case ELEMENT_TYPE_STRING: + return module.GetType("System.String"); + case ELEMENT_TYPE_OBJECT: + return module.GetType("System.Object"); + case ELEMENT_TYPE_VAR: + return context.GetGenericTypeArgument(br.ReadCompressedUInt()); + case ELEMENT_TYPE_MVAR: + return context.GetGenericMethodArgument(br.ReadCompressedUInt()); + case ELEMENT_TYPE_GENERICINST: + return ReadGenericInst(module, br, context); + case ELEMENT_TYPE_SZARRAY: + mods = CustomModifiers.Read(module, br, context); + return ReadType(module, br, context).__MakeArrayType(mods); + case ELEMENT_TYPE_ARRAY: + mods = CustomModifiers.Read(module, br, context); + return ReadType(module, br, context).__MakeArrayType(br.ReadCompressedUInt(), ReadArraySizes(br), ReadArrayBounds(br), mods); + case ELEMENT_TYPE_PTR: + mods = CustomModifiers.Read(module, br, context); + return ReadTypeOrVoid(module, br, context).__MakePointerType(mods); + case ELEMENT_TYPE_FNPTR: + return ReadFunctionPointer(module, br, context); + default: + throw new BadImageFormatException(); + } + } + + static Type ReadTypeOrByRef(Module module, byte[] bytes, ref int n, Context context) + { + if (bytes[n] == ELEMENT_TYPE_BYREF) + { + n++; + // LAMESPEC it is allowed (by C++/CLI, ilasm and peverify) to have custom modifiers after the BYREF + // (which makes sense, as it is analogous to pointers) + CustomModifiers mods = CustomModifiers.Read(module, br, context); + // C++/CLI generates void& local variables, so we need to use ReadTypeOrVoid here + return ReadTypeOrVoid(module, br, context).__MakeByRefType(mods); + } + return ReadType(module, br, context); + } + + static Type ReadRetType(Module module, byte[] bytes, ref int n, Context context) + { + switch (bytes[n]) + { + case ELEMENT_TYPE_VOID: + n++; + return module.GetType("System.Void"); + case ELEMENT_TYPE_TYPEDBYREF: + n++; + return module.GetType("System.TypedReference"); + default: + return ReadTypeOrByRef(module, br, context); + } + } + + public static void ReadStandaloneSignature(Module module, byte[] bytes, Context context) + { + CallingConvention unmanagedCallingConvention; + CallingConventions callingConvention; + + var n = 0; + unmanagedCallingConvention = 0; + callingConvention = 0; + + var flags = bytes[n++]; + bool unmanaged; + switch (flags & 7) + { + case DEFAULT: + callingConvention = CallingConventions.Standard; + unmanaged = false; + break; + case 0x01: // C + unmanagedCallingConvention = CallingConvention.Cdecl; + unmanaged = true; + break; + case 0x02: // STDCALL + unmanagedCallingConvention = CallingConvention.StdCall; + unmanaged = true; + break; + case 0x03: // THISCALL + unmanagedCallingConvention = CallingConvention.ThisCall; + unmanaged = true; + break; + case 0x04: // FASTCALL + unmanagedCallingConvention = CallingConvention.FastCall; + unmanaged = true; + break; + case VARARG: + callingConvention = CallingConventions.VarArgs; + unmanaged = false; + break; + default: + throw new BadImageFormatException(); + } + if ((flags & HASTHIS) != 0) callingConvention |= CallingConventions.HasThis; + if ((flags & EXPLICITTHIS) != 0) callingConvention |= CallingConventions.ExplicitThis; + if ((flags & GENERIC) != 0) throw new BadImageFormatException(); + var paramCount = ReadCompressedUInt(bytes, ref n); + CustomModifiers[] customModifiers = null; + PackedCustomModifiers.Pack(ref customModifiers, 0, CustomModifiers.Read(module, br, context), paramCount + 1); + Type returnType = ReadRetType(module, br, context); + List parameterTypes = new List(); + List optionalParameterTypes = new List(); + List curr = parameterTypes; + for (int i = 0; i < paramCount; i++) + { + if (br.PeekByte() == SENTINEL) + { + br.ReadByte(); + curr = optionalParameterTypes; + } + PackedCustomModifiers.Pack(ref customModifiers, i + 1, CustomModifiers.Read(module, br, context), paramCount + 1); + curr.Add(ReadParam(module, br, context)); + } + } + } + */ +} diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/LICENSE b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/LICENSE new file mode 100644 index 0000000..edeae8e --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Andreas Pardeike + +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. \ No newline at end of file diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/MethodPatcher.cs b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/MethodPatcher.cs new file mode 100644 index 0000000..84024c5 --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/MethodPatcher.cs @@ -0,0 +1,437 @@ +using Harmony.ILCopying; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; + +namespace Harmony +{ + public static class MethodPatcher + { + // special parameter names that can be used in prefix and postfix methods + // + public static string INSTANCE_PARAM = "__instance"; + public static string ORIGINAL_METHOD_PARAM = "__originalMethod"; + public static string RESULT_VAR = "__result"; + public static string STATE_VAR = "__state"; + public static string PARAM_INDEX_PREFIX = "__"; + public static string INSTANCE_FIELD_PREFIX = "___"; + + // in case of trouble, set to true to write dynamic method to desktop as a dll + // won't work for all methods because of the inability to extend a type compared + // to the way DynamicTools.CreateDynamicMethod works + // + static readonly bool DEBUG_METHOD_GENERATION_BY_DLL_CREATION = false; + + // for fixing old harmony bugs + [UpgradeToLatestVersion(1)] + public static DynamicMethod CreatePatchedMethod(MethodBase original, List prefixes, List postfixes, List transpilers) + { + return CreatePatchedMethod(original, ("MELONLOADER_HARMONY_PATCH_" + MelonLoader.BuildInfo.Version), prefixes, postfixes, transpilers); + } + + public static DynamicMethod CreatePatchedMethod(MethodBase original, string harmonyInstanceID, List prefixes, List postfixes, List transpilers) + { + try + { + if (HarmonyInstance.DEBUG) FileLog.LogBuffered("### Patch " + original.DeclaringType + ", " + original); + + bool isIl2Cpp = MelonLoader.UnhollowerSupport.IsGeneratedAssemblyType(original.DeclaringType); + var idx = prefixes.Count() + postfixes.Count(); + var patch = DynamicTools.CreateDynamicMethod(original, "_Patch" + idx, isIl2Cpp); + if (patch == null) + return null; + + var il = patch.GetILGenerator(); + + // for debugging + AssemblyBuilder assemblyBuilder = null; + TypeBuilder typeBuilder = null; + if (DEBUG_METHOD_GENERATION_BY_DLL_CREATION) + il = DynamicTools.CreateSaveableMethod(original, "_Patch" + idx, out assemblyBuilder, out typeBuilder); + + var originalVariables = DynamicTools.DeclareLocalVariables(original, il); + var privateVars = new Dictionary(); + + LocalBuilder resultVariable = null; + if (idx > 0) + { + resultVariable = DynamicTools.DeclareLocalVariable(il, AccessTools.GetReturnedType(original)); + privateVars[RESULT_VAR] = resultVariable; + } + + prefixes.ForEach(prefix => + { + prefix.GetParameters() + .Where(patchParam => patchParam.Name == STATE_VAR) + .Do(patchParam => + { + var privateStateVariable = DynamicTools.DeclareLocalVariable(il, patchParam.ParameterType); + privateVars[prefix.DeclaringType.FullName] = privateStateVariable; + }); + }); + + var skipOriginalLabel = il.DefineLabel(); + var canHaveJump = AddPrefixes(il, original, prefixes, privateVars, skipOriginalLabel, isIl2Cpp); + + var copier = new MethodCopier(original, il, originalVariables); + foreach (var transpiler in transpilers) + copier.AddTranspiler(transpiler); + + var endLabels = new List(); + var endBlocks = new List(); + copier.Finalize(endLabels, endBlocks); + + foreach (var label in endLabels) + Emitter.MarkLabel(il, label); + foreach (var block in endBlocks) + Emitter.MarkBlockAfter(il, block); + if (resultVariable != null) + Emitter.Emit(il, OpCodes.Stloc, resultVariable); + if (canHaveJump) + Emitter.MarkLabel(il, skipOriginalLabel); + + AddPostfixes(il, original, postfixes, privateVars, false, isIl2Cpp); + + if (resultVariable != null) + Emitter.Emit(il, OpCodes.Ldloc, resultVariable); + + AddPostfixes(il, original, postfixes, privateVars, true, isIl2Cpp); + + Emitter.Emit(il, OpCodes.Ret); + + if (HarmonyInstance.DEBUG) + { + FileLog.LogBuffered("DONE"); + FileLog.LogBuffered(""); + FileLog.FlushBuffer(); + } + + // for debugging + if (DEBUG_METHOD_GENERATION_BY_DLL_CREATION) + { + DynamicTools.SaveMethod(assemblyBuilder, typeBuilder); + return null; + } + + DynamicTools.PrepareDynamicMethod(patch); + return patch; + } + catch (Exception ex) + { + throw new Exception("Exception from HarmonyInstance \"" + harmonyInstanceID + "\"", ex); + } + finally + { + if (HarmonyInstance.DEBUG) + FileLog.FlushBuffer(); + } + } + + static OpCode LoadIndOpCodeFor(Type type) + { + if (type.IsEnum) return OpCodes.Ldind_I4; + + if (type == typeof(float)) return OpCodes.Ldind_R4; + if (type == typeof(double)) return OpCodes.Ldind_R8; + + if (type == typeof(byte)) return OpCodes.Ldind_U1; + if (type == typeof(ushort)) return OpCodes.Ldind_U2; + if (type == typeof(uint)) return OpCodes.Ldind_U4; + if (type == typeof(ulong)) return OpCodes.Ldind_I8; + + if (type == typeof(sbyte)) return OpCodes.Ldind_I1; + if (type == typeof(short)) return OpCodes.Ldind_I2; + if (type == typeof(int)) return OpCodes.Ldind_I4; + if (type == typeof(long)) return OpCodes.Ldind_I8; + + return OpCodes.Ldind_Ref; + } + + static HarmonyArgument GetArgumentAttribute(this ParameterInfo parameter) + { + return parameter.GetCustomAttributes(false).FirstOrDefault(attr => attr is HarmonyArgument) as HarmonyArgument; + } + + static HarmonyArgument[] GetArgumentAttributes(this MethodInfo method) + { + return method.GetCustomAttributes(false).Where(attr => attr is HarmonyArgument).Cast().ToArray(); + } + + static HarmonyArgument[] GetArgumentAttributes(this Type type) + { + return type.GetCustomAttributes(false).Where(attr => attr is HarmonyArgument).Cast().ToArray(); + } + + static string GetOriginalArgumentName(this ParameterInfo parameter, string[] originalParameterNames) + { + var attribute = parameter.GetArgumentAttribute(); + if (attribute == null) + return null; + + if (string.IsNullOrEmpty(attribute.OriginalName) == false) + return attribute.OriginalName; + + if (attribute.Index >= 0 && attribute.Index < originalParameterNames.Length) + return originalParameterNames[attribute.Index]; + + return null; + } + + static string GetOriginalArgumentName(HarmonyArgument[] attributes, string name, string[] originalParameterNames) + { + if (attributes.Length <= 0) + return null; + + var attribute = attributes.SingleOrDefault(p => p.NewName == name); + if (attribute == null) + return null; + + if (string.IsNullOrEmpty(attribute.OriginalName) == false) + return attribute.OriginalName; + + if (attribute.Index >= 0 && attribute.Index < originalParameterNames.Length) + return originalParameterNames[attribute.Index]; + + return null; + } + + static string GetOriginalArgumentName(this MethodInfo method, string[] originalParameterNames, string name) + { + string argumentName; + + argumentName = GetOriginalArgumentName(method.GetArgumentAttributes(), name, originalParameterNames); + if (argumentName != null) + return argumentName; + + argumentName = GetOriginalArgumentName(method.DeclaringType.GetArgumentAttributes(), name, originalParameterNames); + if (argumentName != null) + return argumentName; + + return name; + } + + private static int GetArgumentIndex(MethodInfo patch, string[] originalParameterNames, ParameterInfo patchParam) + { + var originalName = patchParam.GetOriginalArgumentName(originalParameterNames); + if (originalName != null) + return Array.IndexOf(originalParameterNames, originalName); + + var patchParamName = patchParam.Name; + originalName = patch.GetOriginalArgumentName(originalParameterNames, patchParamName); + if (originalName != null) + return Array.IndexOf(originalParameterNames, originalName); + + return -1; + } + + static MethodInfo getMethodMethod = typeof(MethodBase).GetMethod("GetMethodFromHandle", new[] { typeof(RuntimeMethodHandle) }); + + static void EmitCallParameter(ILGenerator il, MethodBase original, MethodInfo patch, Dictionary variables, bool allowFirsParamPassthrough, bool isIl2Cpp) + { + var isInstance = original.IsStatic == false; + var originalParameters = original.GetParameters(); + var originalParameterNames = originalParameters.Select(p => p.Name).ToArray(); + + // check for passthrough using first parameter (which must have same type as return type) + var parameters = patch.GetParameters().ToList(); + if (allowFirsParamPassthrough && patch.ReturnType != typeof(void) && parameters.Count > 0 && parameters[0].ParameterType == patch.ReturnType) + parameters.RemoveRange(0, 1); + + foreach (var patchParam in parameters) + { + if (patchParam.Name == ORIGINAL_METHOD_PARAM) + { + var constructorInfo = original as ConstructorInfo; + if (constructorInfo != null) + { + Emitter.Emit(il, OpCodes.Ldtoken, constructorInfo); + Emitter.Emit(il, OpCodes.Call, getMethodMethod); + continue; + } + var methodInfo = original as MethodInfo; + if (methodInfo != null) + { + Emitter.Emit(il, OpCodes.Ldtoken, methodInfo); + Emitter.Emit(il, OpCodes.Call, getMethodMethod); + continue; + } + Emitter.Emit(il, OpCodes.Ldnull); + continue; + } + + if (patchParam.Name == INSTANCE_PARAM) + { + if (original.IsStatic) + Emitter.Emit(il, OpCodes.Ldnull); + else if (patchParam.ParameterType.IsByRef) + Emitter.Emit(il, OpCodes.Ldarga, 0); // probably won't work or will be useless + else + Emitter.Emit(il, OpCodes.Ldarg_0); + continue; + } + + if (patchParam.Name.StartsWith(INSTANCE_FIELD_PREFIX)) + { + var fieldName = patchParam.Name.Substring(INSTANCE_FIELD_PREFIX.Length); + if (isIl2Cpp) + { + if (patchParam.ParameterType.IsByRef) + throw new NotSupportedException("Ref parameters to fields are not supported in IL2CPP patches"); + + var getterMethod = AccessTools.Property(original.DeclaringType, fieldName)?.GetGetMethod(); + if (getterMethod == null) + throw new ArgumentException("No such field defined in class " + original.DeclaringType.FullName, fieldName); + + if (!getterMethod.IsStatic) + Emitter.Emit(il, OpCodes.Ldarg_0); + var opcode = getterMethod.IsVirtual ? OpCodes.Callvirt : OpCodes.Call; + Emitter.Emit(il, opcode, getterMethod); + } + else + { + FieldInfo fieldInfo; + if (fieldName.All(char.IsDigit)) + { + fieldInfo = AccessTools.Field(original.DeclaringType, int.Parse(fieldName)); + if (fieldInfo == null) + throw new ArgumentException("No field found at given index in class " + original.DeclaringType.FullName, fieldName); + } + else + { + fieldInfo = AccessTools.Field(original.DeclaringType, fieldName); + if (fieldInfo == null) + throw new ArgumentException("No such field defined in class " + original.DeclaringType.FullName, fieldName); + } + + if (fieldInfo.IsStatic) + { + if (patchParam.ParameterType.IsByRef) + Emitter.Emit(il, OpCodes.Ldsflda, fieldInfo); + else + Emitter.Emit(il, OpCodes.Ldsfld, fieldInfo); + } + else + { + if (patchParam.ParameterType.IsByRef) + { + Emitter.Emit(il, OpCodes.Ldarg_0); + Emitter.Emit(il, OpCodes.Ldflda, fieldInfo); + } + else + { + Emitter.Emit(il, OpCodes.Ldarg_0); + Emitter.Emit(il, OpCodes.Ldfld, fieldInfo); + } + } + } + continue; + } + + if (patchParam.Name == STATE_VAR) + { + var ldlocCode = patchParam.ParameterType.IsByRef ? OpCodes.Ldloca : OpCodes.Ldloc; + Emitter.Emit(il, ldlocCode, variables[patch.DeclaringType.FullName]); + continue; + } + + if (patchParam.Name == RESULT_VAR) + { + if (AccessTools.GetReturnedType(original) == typeof(void)) + throw new Exception("Cannot get result from void method " + original.FullDescription()); + var ldlocCode = patchParam.ParameterType.IsByRef ? OpCodes.Ldloca : OpCodes.Ldloc; + Emitter.Emit(il, ldlocCode, variables[RESULT_VAR]); + continue; + } + + int idx; + if (patchParam.Name.StartsWith(PARAM_INDEX_PREFIX)) + { + var val = patchParam.Name.Substring(PARAM_INDEX_PREFIX.Length); + if (!int.TryParse(val, out idx)) + throw new Exception("Parameter " + patchParam.Name + " does not contain a valid index"); + if (idx < 0 || idx >= originalParameters.Length) + throw new Exception("No parameter found at index " + idx); + } + else + { + idx = GetArgumentIndex(patch, originalParameterNames, patchParam); + if (idx == -1) throw new Exception("Parameter \"" + patchParam.Name + "\" not found in method " + original.FullDescription()); + } + + // original -> patch opcode + // -------------------------------------- + // 1 normal -> normal : LDARG + // 2 normal -> ref/out : LDARGA + // 3 ref/out -> normal : LDARG, LDIND_x + // 4 ref/out -> ref/out : LDARG + // + var originalIsNormal = originalParameters[idx].IsOut == false && originalParameters[idx].ParameterType.IsByRef == false; + var patchIsNormal = patchParam.IsOut == false && patchParam.ParameterType.IsByRef == false; + var patchArgIndex = idx + (isInstance ? 1 : 0); + + // Case 1 + 4 + if (originalIsNormal == patchIsNormal) + { + Emitter.Emit(il, OpCodes.Ldarg, patchArgIndex); + continue; + } + + // Case 2 + if (originalIsNormal && patchIsNormal == false) + { + Emitter.Emit(il, OpCodes.Ldarga, patchArgIndex); + continue; + } + + // Case 3 + Emitter.Emit(il, OpCodes.Ldarg, patchArgIndex); + Emitter.Emit(il, LoadIndOpCodeFor(originalParameters[idx].ParameterType)); + } + } + + static bool AddPrefixes(ILGenerator il, MethodBase original, List prefixes, Dictionary variables, Label label, bool isIl2Cpp) + { + var canHaveJump = false; + prefixes.ForEach(fix => + { + EmitCallParameter(il, original, fix, variables, false, isIl2Cpp); + Emitter.Emit(il, OpCodes.Call, fix); + + if (fix.ReturnType != typeof(void)) + { + if (fix.ReturnType != typeof(bool)) + throw new Exception("Prefix patch " + fix + " has not \"bool\" or \"void\" return type: " + fix.ReturnType); + Emitter.Emit(il, OpCodes.Brfalse, label); + canHaveJump = true; + } + }); + return canHaveJump; + } + + static void AddPostfixes(ILGenerator il, MethodBase original, List postfixes, Dictionary variables, bool passthroughPatches, bool isIl2Cpp) + { + postfixes + .Where(fix => passthroughPatches == (fix.ReturnType != typeof(void))) + .Do(fix => + { + EmitCallParameter(il, original, fix, variables, true, isIl2Cpp); + Emitter.Emit(il, OpCodes.Call, fix); + + if (fix.ReturnType != typeof(void)) + { + var firstFixParam = fix.GetParameters().FirstOrDefault(); + var hasPassThroughResultParam = firstFixParam != null && fix.ReturnType == firstFixParam.ParameterType; + if (!hasPassThroughResultParam) + { + if (firstFixParam != null) + throw new Exception("Return type of postfix patch " + fix + " does match type of its first parameter"); + + throw new Exception("Postfix patch " + fix + " must have a \"void\" return type"); + } + } + }); + } + } +} \ No newline at end of file diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Patch.cs b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Patch.cs new file mode 100644 index 0000000..06c9dc6 --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Patch.cs @@ -0,0 +1,198 @@ +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; + +namespace Harmony +{ + public static class PatchInfoSerialization + { + class Binder : SerializationBinder + { + public override Type BindToType(string assemblyName, string typeName) + { + var types = new Type[] { + typeof(PatchInfo), + typeof(Patch[]), + typeof(Patch) + }; + foreach (var type in types) + if (typeName == type.FullName) + return type; + var typeToDeserialize = Type.GetType(string.Format("{0}, {1}", typeName, assemblyName)); + return typeToDeserialize; + } + } + + public static byte[] Serialize(this PatchInfo patchInfo) + { +#pragma warning disable XS0001 + using (var streamMemory = new MemoryStream()) + { + var formatter = new BinaryFormatter(); + formatter.Serialize(streamMemory, patchInfo); + return streamMemory.GetBuffer(); + } +#pragma warning restore XS0001 + } + + public static PatchInfo Deserialize(byte[] bytes) + { + var formatter = new BinaryFormatter { Binder = new Binder() }; +#pragma warning disable XS0001 + var streamMemory = new MemoryStream(bytes); +#pragma warning restore XS0001 + return (PatchInfo)formatter.Deserialize(streamMemory); + } + + // general sorting by (in that order): before, after, priority and index + public static int PriorityComparer(object obj, int index, int priority, string[] before, string[] after) + { + var trv = Traverse.Create(obj); + var theirOwner = trv.Field("owner").GetValue(); + var theirPriority = trv.Field("priority").GetValue(); + var theirIndex = trv.Field("index").GetValue(); + + if (before != null && Array.IndexOf(before, theirOwner) > -1) + return -1; + if (after != null && Array.IndexOf(after, theirOwner) > -1) + return 1; + + if (priority != theirPriority) + return -(priority.CompareTo(theirPriority)); + + return index.CompareTo(theirIndex); + } + } + + [Serializable] + public class PatchInfo + { + public Patch[] prefixes; + public Patch[] postfixes; + public Patch[] transpilers; + public IntPtr methodDetourPointer; + public IntPtr copiedMethodInfoPointer; + + public PatchInfo() + { + prefixes = new Patch[0]; + postfixes = new Patch[0]; + transpilers = new Patch[0]; + methodDetourPointer = IntPtr.Zero; + copiedMethodInfoPointer = IntPtr.Zero; + } + + public void AddPrefix(MethodInfo patch, string owner, int priority, string[] before, string[] after) + { + var l = prefixes.ToList(); + l.Add(new Patch(patch, prefixes.Count() + 1, owner, priority, before, after)); + prefixes = l.ToArray(); + } + + public void RemovePrefix(string owner) + { + if (owner == "*") + { + prefixes = new Patch[0]; + return; + } + prefixes = prefixes.Where(patch => patch.owner != owner).ToArray(); + } + + public void AddPostfix(MethodInfo patch, string owner, int priority, string[] before, string[] after) + { + var l = postfixes.ToList(); + l.Add(new Patch(patch, postfixes.Count() + 1, owner, priority, before, after)); + postfixes = l.ToArray(); + } + + public void RemovePostfix(string owner) + { + if (owner == "*") + { + postfixes = new Patch[0]; + return; + } + postfixes = postfixes.Where(patch => patch.owner != owner).ToArray(); + } + + public void AddTranspiler(MethodInfo patch, string owner, int priority, string[] before, string[] after) + { + var l = transpilers.ToList(); + l.Add(new Patch(patch, transpilers.Count() + 1, owner, priority, before, after)); + transpilers = l.ToArray(); + } + + public void RemoveTranspiler(string owner) + { + if (owner == "*") + { + transpilers = new Patch[0]; + return; + } + transpilers = transpilers.Where(patch => patch.owner != owner).ToArray(); + } + + public void RemovePatch(MethodInfo patch) + { + prefixes = prefixes.Where(p => p.patch != patch).ToArray(); + postfixes = postfixes.Where(p => p.patch != patch).ToArray(); + transpilers = transpilers.Where(p => p.patch != patch).ToArray(); + } + } + + [Serializable] + public class Patch : IComparable + { + readonly public int index; + readonly public string owner; + readonly public int priority; + readonly public string[] before; + readonly public string[] after; + + readonly public MethodInfo patch; + + public Patch(MethodInfo patch, int index, string owner, int priority, string[] before, string[] after) + { + if (patch is DynamicMethod) throw new Exception("Cannot directly reference dynamic method \"" + patch.FullDescription() + "\" in Harmony. Use a factory method instead that will return the dynamic method."); + + this.index = index; + this.owner = owner; + this.priority = priority; + this.before = before; + this.after = after; + this.patch = patch; + } + + public MethodInfo GetMethod(MethodBase original) + { + if (patch.ReturnType != typeof(DynamicMethod)) return patch; + if (patch.IsStatic == false) return patch; + var parameters = patch.GetParameters(); + if (parameters.Count() != 1) return patch; + if (parameters[0].ParameterType != typeof(MethodBase)) return patch; + + // we have a DynamicMethod factory, let's use it + return patch.Invoke(null, new object[] { original }) as DynamicMethod; + } + + public override bool Equals(object obj) + { + return ((obj != null) && (obj is Patch) && (patch == ((Patch)obj).patch)); + } + + public int CompareTo(object obj) + { + return PatchInfoSerialization.PriorityComparer(obj, index, priority, before, after); + } + + public override int GetHashCode() + { + return patch.GetHashCode(); + } + } +} \ No newline at end of file diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/PatchFunctions.cs b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/PatchFunctions.cs new file mode 100644 index 0000000..9c0a389 --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/PatchFunctions.cs @@ -0,0 +1,380 @@ +using Harmony.ILCopying; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using MelonLoader; +using System.Runtime.InteropServices; +using BepInEx.IL2CPP.Hook; + +namespace Harmony +{ + public static class PatchFunctions + { + public static void AddPrefix(PatchInfo patchInfo, string owner, HarmonyMethod info) + { + if (info == null || info.method == null) return; + + var priority = info.prioritiy == -1 ? Priority.Normal : info.prioritiy; + var before = info.before ?? new string[0]; + var after = info.after ?? new string[0]; + + patchInfo.AddPrefix(info.method, owner, priority, before, after); + } + + public static void RemovePrefix(PatchInfo patchInfo, string owner) + { + patchInfo.RemovePrefix(owner); + } + + public static void AddPostfix(PatchInfo patchInfo, string owner, HarmonyMethod info) + { + if (info == null || info.method == null) return; + + var priority = info.prioritiy == -1 ? Priority.Normal : info.prioritiy; + var before = info.before ?? new string[0]; + var after = info.after ?? new string[0]; + + patchInfo.AddPostfix(info.method, owner, priority, before, after); + } + + public static void RemovePostfix(PatchInfo patchInfo, string owner) + { + patchInfo.RemovePostfix(owner); + } + + public static void AddTranspiler(PatchInfo patchInfo, string owner, HarmonyMethod info) + { + if (info == null || info.method == null) return; + + var priority = info.prioritiy == -1 ? Priority.Normal : info.prioritiy; + var before = info.before ?? new string[0]; + var after = info.after ?? new string[0]; + + patchInfo.AddTranspiler(info.method, owner, priority, before, after); + } + + public static void RemoveTranspiler(PatchInfo patchInfo, string owner) + { + patchInfo.RemoveTranspiler(owner); + } + + public static void RemovePatch(PatchInfo patchInfo, MethodInfo patch) + { + patchInfo.RemovePatch(patch); + } + + // pass in a generator that will create local variables for the returned instructions + // + public static List GetInstructions(ILGenerator generator, MethodBase method) + { + return MethodBodyReader.GetInstructions(generator, method); + } + + public static List GetSortedPatchMethods(MethodBase original, Patch[] patches) + { + return patches + .Where(p => p.patch != null) + .OrderBy(p => p) + .Select(p => p.GetMethod(original)) + .ToList(); + } + + public static DynamicMethod UpdateWrapper(MethodBase original, PatchInfo patchInfo, string instanceID) + { + var sortedPrefixes = GetSortedPatchMethods(original, patchInfo.prefixes); + var sortedPostfixes = GetSortedPatchMethods(original, patchInfo.postfixes); + var sortedTranspilers = GetSortedPatchMethods(original, patchInfo.transpilers); + bool isIl2Cpp = UnhollowerSupport.IsGeneratedAssemblyType(original.DeclaringType); + + if (isIl2Cpp) { + if (sortedTranspilers.Count > 0) { + throw new NotSupportedException("IL2CPP patches cannot use transpilers (got " + sortedTranspilers.Count + ")"); + } + + if (patchInfo.copiedMethodInfoPointer == IntPtr.Zero) { + IntPtr origMethodPtr = UnhollowerSupport.MethodBaseToIl2CppMethodInfoPointer(original); + patchInfo.copiedMethodInfoPointer = CopyMethodInfoStruct(origMethodPtr); + HarmonySharedState.UpdatePatchInfo(original, patchInfo); + } + + sortedTranspilers.Add(AccessTools.DeclaredMethod(typeof(PatchFunctions), "UnhollowerTranspiler")); + } + + var replacement = MethodPatcher.CreatePatchedMethod(original, instanceID, sortedPrefixes, sortedPostfixes, sortedTranspilers); + if (replacement == null) throw new MissingMethodException("Cannot create dynamic replacement for " + original.FullDescription()); + + if (isIl2Cpp) { + DynamicMethod il2CppShim = CreateIl2CppShim(replacement, original); + InstallIl2CppPatch(patchInfo, il2CppShim); + PatchTools.RememberObject(original, new PotatoTuple { First = replacement, Second = il2CppShim}); + } else { + var errorString = Memory.DetourMethod(original, replacement); + if (errorString != null) + throw new FormatException("Method " + original.FullDescription() + " cannot be patched. Reason: " + errorString); + + PatchTools.RememberObject(original, replacement); // no gc for new value + release old value to gc + } + + return replacement; + } + + private class PotatoTuple + { + public MethodBase First; + public MethodBase Second; + } + + private static IEnumerable UnhollowerTranspiler(MethodBase method, IEnumerable instructionsIn) { + List instructions = new List(instructionsIn); + PatchInfo patchInfo = HarmonySharedState.GetPatchInfo(method); + IntPtr copiedMethodInfo = patchInfo.copiedMethodInfoPointer; + + bool found = false; + int replaceIdx = 0; + int replaceCount = 0; + + for (int i = instructions.Count - 2; i >= 0; --i) { + if (instructions[i].opcode != OpCodes.Ldsfld) continue; + + found = true; + CodeInstruction next = instructions[i + 1]; + if (next.opcode == OpCodes.Call && ((MethodInfo) next.operand).Name == "il2cpp_object_get_virtual_method") { + // Virtual method: Replace the sequence + // - ldarg.0 + // - call native int[UnhollowerBaseLib] UnhollowerBaseLib.IL2CPP::Il2CppObjectBaseToPtr(class [UnhollowerBaseLib] UnhollowerBaseLib.Il2CppObjectBase) + // - ldsfld native int SomeClass::NativeMethodInfoPtr_Etc + // - call native int[UnhollowerBaseLib] UnhollowerBaseLib.IL2CPP::il2cpp_object_get_virtual_method(native int, native int) + + replaceIdx = i - 2; + replaceCount = 4; + } else { + // Everything else: Just replace the static load + replaceIdx = i; + replaceCount = 1; + } + break; + } + + if (!found) { + MelonLogger.LogError("Harmony transpiler could not rewrite Unhollower method. Expect a stack overflow."); + return instructions; + } + + CodeInstruction[] replacement = { + new CodeInstruction(OpCodes.Ldc_I8, copiedMethodInfo.ToInt64()), + new CodeInstruction(OpCodes.Conv_I) + }; + + instructions.RemoveRange(replaceIdx, replaceCount); + instructions.InsertRange(replaceIdx, replacement); + + return instructions; + } + + private static void InstallIl2CppPatch(PatchInfo patchInfo, DynamicMethod il2CppShim) + { + IntPtr methodInfoPtr = patchInfo.copiedMethodInfoPointer; + IntPtr oldDetourPtr = patchInfo.methodDetourPointer; + IntPtr newDetourPtr = il2CppShim.MethodHandle.GetFunctionPointer(); + + if (oldDetourPtr != IntPtr.Zero) + { + Imports.Unhook(methodInfoPtr, oldDetourPtr); + } + + Imports.Hook(methodInfoPtr, newDetourPtr); + + patchInfo.methodDetourPointer = newDetourPtr; + } + + private static IntPtr CopyMethodInfoStruct(IntPtr origMethodInfo) { + // Il2CppMethodInfo *copiedMethodInfo = malloc(sizeof(Il2CppMethodInfo)); + int sizeOfMethodInfo = Marshal.SizeOf(UnhollowerSupport.Il2CppMethodInfoType); + IntPtr copiedMethodInfo = Marshal.AllocHGlobal(sizeOfMethodInfo); + + // *copiedMethodInfo = *origMethodInfo; + object temp = Marshal.PtrToStructure(origMethodInfo, UnhollowerSupport.Il2CppMethodInfoType); + Marshal.StructureToPtr(temp, copiedMethodInfo, false); + + return copiedMethodInfo; + } + + private static DynamicMethod CreateIl2CppShim(DynamicMethod patch, MethodBase original) + { + var patchName = patch.Name + "_il2cpp"; + + var parameters = patch.GetParameters(); + var result = parameters.Types().ToList(); + var origParamTypes = result.ToArray(); + var paramTypes = new Type[origParamTypes.Length]; + for (int i = 0; i < paramTypes.Length; ++i) + paramTypes[i] = Il2CppTypeForPatchType(origParamTypes[i]); + + var origReturnType = AccessTools.GetReturnedType(patch); + var returnType = Il2CppTypeForPatchType(origReturnType); + + DynamicMethod method = new DynamicMethod( + patchName, + MethodAttributes.Public | MethodAttributes.Static, + CallingConventions.Standard, + returnType, + paramTypes, + original.DeclaringType, + true + ); + + for (var i = 0; i < parameters.Length; i++) + method.DefineParameter(i + 1, parameters[i].Attributes, parameters[i].Name); + + var il = method.GetILGenerator(); + + LocalBuilder[] byRefValues = new LocalBuilder[parameters.Length]; + LocalBuilder returnLocal = null; + if (origReturnType != typeof(void)) { + returnLocal = il.DeclareLocal(origReturnType); + Emitter.LogLocalVariable(il, returnLocal); + } + LocalBuilder exceptionLocal = il.DeclareLocal(typeof(Exception)); + Emitter.LogLocalVariable(il, exceptionLocal); + + // Start a try-block for the call to the original patch + Emitter.MarkBlockBefore(il, new ExceptionBlock(ExceptionBlockType.BeginExceptionBlock, null), out _); + + // Load arguments, invoking the IntPrt -> Il2CppObject constructor for IL2CPP types + for (int i = 0; i < origParamTypes.Length; ++i) { + Emitter.Emit(il, OpCodes.Ldarg, i); + ConvertArgument(il, origParamTypes[i], ref byRefValues[i]); + if (byRefValues[i] != null) { + Emitter.LogLocalVariable(il, byRefValues[i]); + } + } + + // Call the original patch with the now-correct types + Emitter.Emit(il, OpCodes.Call, patch); + + // Store the result, if any + if (returnLocal != null) { + Emitter.Emit(il, OpCodes.Stloc, returnLocal); + } + + // Catch any exceptions that may have been thrown + Emitter.MarkBlockBefore(il, new ExceptionBlock(ExceptionBlockType.BeginCatchBlock, typeof(Exception)), out _); + + // MelonLogger.LogError("Exception in ...\n" + exception.ToString()); + Emitter.Emit(il, OpCodes.Stloc, exceptionLocal); + Emitter.Emit(il, OpCodes.Ldstr, $"Exception in Harmony patch of method {original.FullDescription()}:\n"); + Emitter.Emit(il, OpCodes.Ldloc, exceptionLocal); + Emitter.Emit(il, OpCodes.Call, AccessTools.DeclaredMethod(typeof(Exception), "ToString", new Type[0])); + Emitter.Emit(il, OpCodes.Call, AccessTools.DeclaredMethod(typeof(string), "Concat", new Type[] { typeof(string), typeof(string) })); + Emitter.Emit(il, OpCodes.Call, AccessTools.DeclaredMethod(typeof(MelonLogger), "LogError", new Type[] { typeof(string) })); + + // Close the exception block + Emitter.MarkBlockAfter(il, new ExceptionBlock(ExceptionBlockType.EndExceptionBlock, null)); + + // Write back the pointers of ref arguments + for (int i = 0; i < parameters.Length; ++i) { + if (byRefValues[i] == null) continue; + + Emitter.Emit(il, OpCodes.Ldarg, i); // -> [intptr*] + Emitter.Emit(il, OpCodes.Ldloc, byRefValues[i]); // -> [intptr*, obj] + if (origParamTypes[i].GetElementType() == typeof(string)) { + Emitter.Emit(il, OpCodes.Call, UnhollowerSupport.ManagedStringToIl2CppMethod); // -> [intptr*, intptr] + } else { + Emitter.Emit(il, OpCodes.Call, UnhollowerSupport.Il2CppObjectBaseToPtrMethod); // -> [intptr*, intptr] + } + Emitter.Emit(il, OpCodes.Stind_I); // -> [] + } + + // Load the return value, if any, and unwrap it if required + if (returnLocal != null) { + Emitter.Emit(il, OpCodes.Ldloc, returnLocal); + ConvertReturnValue(il, origReturnType); + } + + Emitter.Emit(il, OpCodes.Ret); + + DynamicTools.PrepareDynamicMethod(method); + return method; + } + + private static Type Il2CppTypeForPatchType(Type type) { + if (type.IsByRef) { + Type element = type.GetElementType(); + if (element == typeof(string) || UnhollowerSupport.IsGeneratedAssemblyType(element)) { + return typeof(IntPtr*); + } else { + return type; + } + } else if (type == typeof(string) || UnhollowerSupport.IsGeneratedAssemblyType(type)) { + return typeof(IntPtr); + } else { + return type; + } + } + + private static void ConvertArgument(ILGenerator il, Type paramType, ref LocalBuilder byRefLocal) { + if (paramType.IsValueType) + return; + + if (paramType.IsByRef) { + Type elementType = paramType.GetElementType(); + + if (paramType.GetElementType() == typeof(string)) { + // byRefLocal = Il2CppStringToManaged(*ptr); + // return ref byRefLocal; + + byRefLocal = il.DeclareLocal(elementType); + Emitter.Emit(il, OpCodes.Ldind_I); + Emitter.Emit(il, OpCodes.Call, UnhollowerSupport.Il2CppStringToManagedMethod); + Emitter.Emit(il, OpCodes.Stloc, byRefLocal); + Emitter.Emit(il, OpCodes.Ldloca, byRefLocal); + } else if (UnhollowerSupport.IsGeneratedAssemblyType(elementType)) { + // byRefLocal = *ptr == 0 ? null : new SomeType(*ptr); + // return ref byRefLocal; + Label ptrNonZero = il.DefineLabel(); + Label done = il.DefineLabel(); + + byRefLocal = il.DeclareLocal(elementType); + Emitter.Emit(il, OpCodes.Ldind_I); + Emitter.Emit(il, OpCodes.Dup); + Emitter.Emit(il, OpCodes.Brtrue_S, ptrNonZero); + Emitter.Emit(il, OpCodes.Pop); + Emitter.Emit(il, OpCodes.Br_S, done); + Emitter.MarkLabel(il, ptrNonZero); + Emitter.Emit(il, OpCodes.Newobj, Il2CppConstuctor(elementType)); + Emitter.Emit(il, OpCodes.Stloc, byRefLocal); + Emitter.MarkLabel(il, done); + Emitter.Emit(il, OpCodes.Ldloca, byRefLocal); + } + } else if (paramType == typeof(string)) { + // return Il2CppStringToManaged(ptr); + Emitter.Emit(il, OpCodes.Call, UnhollowerSupport.Il2CppStringToManagedMethod); + } else if (UnhollowerSupport.IsGeneratedAssemblyType(paramType)) { + // return ptr == 0 ? null : new SomeType(ptr); + Label ptrNonZero = il.DefineLabel(); + Label done = il.DefineLabel(); + + Emitter.Emit(il, OpCodes.Dup); + Emitter.Emit(il, OpCodes.Brtrue_S, ptrNonZero); + Emitter.Emit(il, OpCodes.Pop); + Emitter.Emit(il, OpCodes.Ldnull); + Emitter.Emit(il, OpCodes.Br_S, done); + Emitter.MarkLabel(il, ptrNonZero); + Emitter.Emit(il, OpCodes.Newobj, Il2CppConstuctor(paramType)); + Emitter.MarkLabel(il, done); + } + } + + private static void ConvertReturnValue(ILGenerator il, Type returnType) { + if (returnType == typeof(string)) { + Emitter.Emit(il, OpCodes.Call, UnhollowerSupport.ManagedStringToIl2CppMethod); + } else if (!returnType.IsValueType && UnhollowerSupport.IsGeneratedAssemblyType(returnType)) { + Emitter.Emit(il, OpCodes.Call, UnhollowerSupport.Il2CppObjectBaseToPtrMethod); + } + } + + private static ConstructorInfo Il2CppConstuctor(Type type) => AccessTools.DeclaredConstructor(type, new Type[] { typeof(IntPtr) }); + } +} diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/PatchProcessor.cs b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/PatchProcessor.cs new file mode 100644 index 0000000..b59d859 --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/PatchProcessor.cs @@ -0,0 +1,303 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; + +namespace Harmony +{ + public class PatchProcessor + { + static object locker = new object(); + + readonly HarmonyInstance instance; + + readonly Type container; + readonly HarmonyMethod containerAttributes; + + List originals = new List(); + HarmonyMethod prefix; + HarmonyMethod postfix; + HarmonyMethod transpiler; + + public PatchProcessor(HarmonyInstance instance, Type type, HarmonyMethod attributes) + { + this.instance = instance; + container = type; + containerAttributes = attributes ?? new HarmonyMethod(null); + prefix = containerAttributes.Clone(); + postfix = containerAttributes.Clone(); + transpiler = containerAttributes.Clone(); + PrepareType(); + } + + public PatchProcessor(HarmonyInstance instance, List originals, HarmonyMethod prefix = null, HarmonyMethod postfix = null, HarmonyMethod transpiler = null) + { + this.instance = instance; + this.originals = originals; + this.prefix = prefix ?? new HarmonyMethod(null); + this.postfix = postfix ?? new HarmonyMethod(null); + this.transpiler = transpiler ?? new HarmonyMethod(null); + } + + public static Patches GetPatchInfo(MethodBase method) + { + lock (locker) + { + var patchInfo = HarmonySharedState.GetPatchInfo(method); + if (patchInfo == null) return null; + return new Patches(patchInfo.prefixes, patchInfo.postfixes, patchInfo.transpilers); + } + } + + public static IEnumerable AllPatchedMethods() + { + lock (locker) + { + return HarmonySharedState.GetPatchedMethods(); + } + } + + public List Patch() + { + lock (locker) + { + var dynamicMethods = new List(); + foreach (var original in originals) + { + if (original == null) + throw new NullReferenceException("original"); + + var individualPrepareResult = RunMethod(true, original); + if (individualPrepareResult) + { + var patchInfo = HarmonySharedState.GetPatchInfo(original); + if (patchInfo == null) patchInfo = new PatchInfo(); + + PatchFunctions.AddPrefix(patchInfo, instance.Id, prefix); + PatchFunctions.AddPostfix(patchInfo, instance.Id, postfix); + PatchFunctions.AddTranspiler(patchInfo, instance.Id, transpiler); + dynamicMethods.Add(PatchFunctions.UpdateWrapper(original, patchInfo, instance.Id)); + + HarmonySharedState.UpdatePatchInfo(original, patchInfo); + + RunMethod(original); + } + } + return dynamicMethods; + } + } + + public void Unpatch(HarmonyPatchType type, string harmonyID) + { + lock (locker) + { + foreach (var original in originals) + { + var patchInfo = HarmonySharedState.GetPatchInfo(original); + if (patchInfo == null) patchInfo = new PatchInfo(); + + if (type == HarmonyPatchType.All || type == HarmonyPatchType.Prefix) + PatchFunctions.RemovePrefix(patchInfo, harmonyID); + if (type == HarmonyPatchType.All || type == HarmonyPatchType.Postfix) + PatchFunctions.RemovePostfix(patchInfo, harmonyID); + if (type == HarmonyPatchType.All || type == HarmonyPatchType.Transpiler) + PatchFunctions.RemoveTranspiler(patchInfo, harmonyID); + PatchFunctions.UpdateWrapper(original, patchInfo, instance.Id); + + HarmonySharedState.UpdatePatchInfo(original, patchInfo); + } + } + } + + public void Unpatch(MethodInfo patch) + { + lock (locker) + { + foreach (var original in originals) + { + var patchInfo = HarmonySharedState.GetPatchInfo(original); + if (patchInfo == null) patchInfo = new PatchInfo(); + + PatchFunctions.RemovePatch(patchInfo, patch); + PatchFunctions.UpdateWrapper(original, patchInfo, instance.Id); + + HarmonySharedState.UpdatePatchInfo(original, patchInfo); + } + } + } + + void PrepareType() + { + var mainPrepareResult = RunMethod(true); + if (mainPrepareResult == false) + return; + + var customOriginals = RunMethod>(null); + if (customOriginals != null) + { + originals = customOriginals.ToList(); + } + else + { + var originalMethodType = containerAttributes.methodType; + + // MethodType default is Normal + if (containerAttributes.methodType == null) + containerAttributes.methodType = MethodType.Normal; + + var isPatchAll = Attribute.GetCustomAttribute(container, typeof(HarmonyPatchAll)) != null; + if (isPatchAll) + { + var type = containerAttributes.declaringType; + originals.AddRange(AccessTools.GetDeclaredConstructors(type).Cast()); + originals.AddRange(AccessTools.GetDeclaredMethods(type).Cast()); + } + else + { + var original = RunMethod(null); + + if (original == null) + original = GetOriginalMethod(); + + if (original == null) + { + var info = "("; + info += "declaringType=" + containerAttributes.declaringType + ", "; + info += "methodName =" + containerAttributes.methodName + ", "; + info += "methodType=" + originalMethodType + ", "; + info += "argumentTypes=" + containerAttributes.argumentTypes.Description(); + info += ")"; + throw new ArgumentException("No target method specified for class " + container.FullName + " " + info); + } + + originals.Add(original); + } + } + + PatchTools.GetPatches(container, out prefix.method, out postfix.method, out transpiler.method); + + if (prefix.method != null) + { + if (prefix.method.IsStatic == false) + throw new ArgumentException("Patch method " + prefix.method.FullDescription() + " must be static"); + + var prefixAttributes = prefix.method.GetHarmonyMethods(); + containerAttributes.Merge(HarmonyMethod.Merge(prefixAttributes)).CopyTo(prefix); + } + + if (postfix.method != null) + { + if (postfix.method.IsStatic == false) + throw new ArgumentException("Patch method " + postfix.method.FullDescription() + " must be static"); + + var postfixAttributes = postfix.method.GetHarmonyMethods(); + containerAttributes.Merge(HarmonyMethod.Merge(postfixAttributes)).CopyTo(postfix); + } + + if (transpiler.method != null) + { + if (transpiler.method.IsStatic == false) + throw new ArgumentException("Patch method " + transpiler.method.FullDescription() + " must be static"); + + var infixAttributes = transpiler.method.GetHarmonyMethods(); + containerAttributes.Merge(HarmonyMethod.Merge(infixAttributes)).CopyTo(transpiler); + } + } + + MethodBase GetOriginalMethod() + { + var attr = containerAttributes; + if (attr.declaringType == null) return null; + + switch (attr.methodType) + { + case MethodType.Normal: + if (attr.methodName == null) + return null; + return AccessTools.DeclaredMethod(attr.declaringType, attr.methodName, attr.argumentTypes); + + case MethodType.Getter: + if (attr.methodName == null) + return null; + return AccessTools.DeclaredProperty(attr.declaringType, attr.methodName).GetGetMethod(true); + + case MethodType.Setter: + if (attr.methodName == null) + return null; + return AccessTools.DeclaredProperty(attr.declaringType, attr.methodName).GetSetMethod(true); + + case MethodType.Constructor: + return AccessTools.DeclaredConstructor(attr.declaringType, attr.argumentTypes); + + case MethodType.StaticConstructor: + return AccessTools.StaticConstructor(attr.declaringType); + } + + return null; + } + + T RunMethod(T defaultIfNotExisting, params object[] parameters) + { + if (container == null) + return defaultIfNotExisting; + + var methodName = typeof(S).Name.Replace("Harmony", ""); + + var paramList = new List { instance }; + paramList.AddRange(parameters); + var paramTypes = AccessTools.GetTypes(paramList.ToArray()); + var method = PatchTools.GetPatchMethod(container, methodName, paramTypes); + if (method != null && typeof(T).IsAssignableFrom(method.ReturnType)) + return (T)method.Invoke(null, paramList.ToArray()); + + method = PatchTools.GetPatchMethod(container, methodName, new Type[] { typeof(HarmonyInstance) }); + if (method != null && typeof(T).IsAssignableFrom(method.ReturnType)) + return (T)method.Invoke(null, new object[] { instance }); + + method = PatchTools.GetPatchMethod(container, methodName, Type.EmptyTypes); + if (method != null) + { + if (typeof(T).IsAssignableFrom(method.ReturnType)) + return (T)method.Invoke(null, Type.EmptyTypes); + + method.Invoke(null, Type.EmptyTypes); + return defaultIfNotExisting; + } + + return defaultIfNotExisting; + } + + void RunMethod(params object[] parameters) + { + if (container == null) + return; + + var methodName = typeof(S).Name.Replace("Harmony", ""); + + var paramList = new List { instance }; + paramList.AddRange(parameters); + var paramTypes = AccessTools.GetTypes(paramList.ToArray()); + var method = PatchTools.GetPatchMethod(container, methodName, paramTypes); + if (method != null) + { + method.Invoke(null, paramList.ToArray()); + return; + } + + method = PatchTools.GetPatchMethod(container, methodName, new Type[] { typeof(HarmonyInstance) }); + if (method != null) + { + method.Invoke(null, new object[] { instance }); + return; + } + + method = PatchTools.GetPatchMethod(container, methodName, Type.EmptyTypes); + if (method != null) + { + method.Invoke(null, Type.EmptyTypes); + return; + } + } + } +} \ No newline at end of file diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Priority.cs b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Priority.cs new file mode 100644 index 0000000..55f98c3 --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Priority.cs @@ -0,0 +1,15 @@ +namespace Harmony +{ + public static class Priority + { + public const int Last = 0; + public const int VeryLow = 100; + public const int Low = 200; + public const int LowerThanNormal = 300; + public const int Normal = 400; + public const int HigherThanNormal = 500; + public const int High = 600; + public const int VeryHigh = 700; + public const int First = 800; + } +} \ No newline at end of file diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/README.md b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/README.md new file mode 100644 index 0000000..367c406 --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/README.md @@ -0,0 +1,83 @@ + + + Version 2 + A library for patching, replacing and decorating + .NET and Mono methods during runtime. + + +### About + +Harmony gives you an elegant and high level way to alter the functionality in applications written in C#. It works great in games and is well established in titles like **7 Days To Die**, **BattleTech**, **Besiege**, **Cities:Skylines**, **Kerbal Space Program**, **Oxygen Not Included**, Ravenfield, **Rimworld**, Sheltered, **Stardew Valley**, Staxel, **Subnautica**, The Ultimate Nerd Game, Total Miner, Unturned and many more. + +It is also used in unit testing WFP controls and in many other areas. + +### How it works + +If you develop in C# and your code is loaded as a module/plugin into a host application, you can use Harmony to alter the functionality of all the available assemblies of that application. Where other patch libraries simply allow you to replace the original method, Harmony goes one step further and gives you: + +• A way to keep the original method intact +• Execute your code before and/or after the original method +• Modify the original with IL code processors +• Multiple Harmony patches co-exist and don't conflict with each other +• Works at runtime and does not touch any files + +### Installation + +Installation is done by using [0Harmony.dll](https://github.com/pardeike/Harmony/releases) in your project or by using the [Lib.Harmony](https://www.nuget.org/packages/Lib.Harmony) nuget package. + +### Documentation + +Please check out the [documentation](https://harmony.pardeike.net) and join the official [discord server](https://discord.gg/xXgghXR). + +### Contribute + +I put thousands of hours into this project and its support. So every little action helps: + +• Upvote this [stackoverflow answer](https://stackoverflow.com/questions/7299097/dynamically-replace-the-contents-of-a-c-sharp-method/42043003#42043003) +• Spread the word in your developer communities +• Become a [GitHub sponsor](https://github.com/sponsors/pardeike) or a [Patreon](https://www.patreon.com/pardeike) + +This project uses the great [MonoMod.Common](https://github.com/MonoMod/MonoMod.Common) library by [0x0ade](https://github.com/orgs/MonoMod/people/0x0ade). + +### Harmony 1 + +Harmony 1 is deprecated and not under active development anymore. The latest version of it (v1.2.0.1) is stable and contains only minor bugs. Keep using it if you are in an environment that exclusively uses Harmony 1. Currently Harmony 1.x and 2.x are **NOT COMPATIBLE** with each other and **SHOULD NOT BE MIXED**. The old documentation can still be found at the [Wiki](https://github.com/pardeike/Harmony/wiki). + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Tools/AccessCache.cs b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Tools/AccessCache.cs new file mode 100644 index 0000000..a5b769c --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Tools/AccessCache.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Harmony +{ + public class AccessCache + { + Dictionary> fields = new Dictionary>(); + Dictionary> properties = new Dictionary>(); + readonly Dictionary>> methods = new Dictionary>>(); + + [UpgradeToLatestVersion(1)] + public FieldInfo GetFieldInfo(Type type, string name) + { + Dictionary fieldsByType = null; + if (fields.TryGetValue(type, out fieldsByType) == false) + { + fieldsByType = new Dictionary(); + fields.Add(type, fieldsByType); + } + + FieldInfo field = null; + if (fieldsByType.TryGetValue(name, out field) == false) + { + field = AccessTools.Field(type, name); + fieldsByType.Add(name, field); + } + return field; + } + + public PropertyInfo GetPropertyInfo(Type type, string name) + { + Dictionary propertiesByType = null; + if (properties.TryGetValue(type, out propertiesByType) == false) + { + propertiesByType = new Dictionary(); + properties.Add(type, propertiesByType); + } + + PropertyInfo property = null; + if (propertiesByType.TryGetValue(name, out property) == false) + { + property = AccessTools.Property(type, name); + propertiesByType.Add(name, property); + } + return property; + } + + static int CombinedHashCode(IEnumerable objects) + { + var hash1 = (5381 << 16) + 5381; + var hash2 = hash1; + var i = 0; + foreach (var obj in objects) + { + if (i % 2 == 0) + hash1 = ((hash1 << 5) + hash1 + (hash1 >> 27)) ^ obj.GetHashCode(); + else + hash2 = ((hash2 << 5) + hash2 + (hash2 >> 27)) ^ obj.GetHashCode(); + ++i; + } + return hash1 + (hash2 * 1566083941); + } + + public MethodBase GetMethodInfo(Type type, string name, Type[] arguments) + { + Dictionary> methodsByName = null; + methods.TryGetValue(type, out methodsByName); + if (methodsByName == null) + { + methodsByName = new Dictionary>(); + methods.Add(type, methodsByName); + } + + Dictionary methodsByArguments = null; + methodsByName.TryGetValue(name, out methodsByArguments); + if (methodsByArguments == null) + { + methodsByArguments = new Dictionary(); + methodsByName.Add(name, methodsByArguments); + } + + MethodBase method = null; + var argumentsHash = CombinedHashCode(arguments); + methodsByArguments.TryGetValue(argumentsHash, out method); + if (method == null) + { + method = AccessTools.Method(type, name, arguments); + methodsByArguments.Add(argumentsHash, method); + } + + return method; + } + } +} \ No newline at end of file diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Tools/AccessTools.cs b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Tools/AccessTools.cs new file mode 100644 index 0000000..06d0961 --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Tools/AccessTools.cs @@ -0,0 +1,404 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.Serialization; + +namespace Harmony +{ + public static class AccessTools + { + public static BindingFlags all = BindingFlags.Public + | BindingFlags.NonPublic + | BindingFlags.Instance + | BindingFlags.Static + | BindingFlags.GetField + | BindingFlags.SetField + | BindingFlags.GetProperty + | BindingFlags.SetProperty; + + public static Type TypeByName(string name) + { + var type = Type.GetType(name, false); + if (type == null) + type = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(x => x.GetTypes()) + .FirstOrDefault(x => x.FullName == name); + if (type == null) + type = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(x => x.GetTypes()) + .FirstOrDefault(x => x.Name == name); + return type; + } + + public static T FindIncludingBaseTypes(Type type, Func action) + { + while (true) + { + var result = action(type); + if (result != null) return result; + if (type == typeof(object)) return default(T); + type = type.BaseType; + } + } + + public static T FindIncludingInnerTypes(Type type, Func action) + { + var result = action(type); + if (result != null) return result; + foreach (var subType in type.GetNestedTypes(all)) + { + result = FindIncludingInnerTypes(subType, action); + if (result != null) + break; + } + return result; + } + + public static FieldInfo Field(Type type, string name) + { + if (type == null || name == null) return null; + return FindIncludingBaseTypes(type, t => t.GetField(name, all)); + } + + public static FieldInfo Field(Type type, int idx) + { + return GetDeclaredFields(type).ElementAtOrDefault(idx); + } + + public static PropertyInfo DeclaredProperty(Type type, string name) + { + if (type == null || name == null) return null; + return type.GetProperty(name, all); + } + + public static PropertyInfo Property(Type type, string name) + { + if (type == null || name == null) return null; + return FindIncludingBaseTypes(type, t => t.GetProperty(name, all)); + } + + public static MethodInfo DeclaredMethod(Type type, string name, Type[] parameters = null, Type[] generics = null) + { + if (type == null || name == null) return null; + MethodInfo result; + var modifiers = new ParameterModifier[] { }; + + if (parameters == null) + result = type.GetMethod(name, all); + else + result = type.GetMethod(name, all, null, parameters, modifiers); + + if (result == null) return null; + if (generics != null) result = result.MakeGenericMethod(generics); + return result; + } + + public static MethodInfo Method(Type type, string name, Type[] parameters = null, Type[] generics = null) + { + if (type == null || name == null) return null; + MethodInfo result; + var modifiers = new ParameterModifier[] { }; + if (parameters == null) + { + try + { + result = FindIncludingBaseTypes(type, t => t.GetMethod(name, all)); + } + catch (AmbiguousMatchException) + { + result = FindIncludingBaseTypes(type, t => t.GetMethod(name, all, null, new Type[0], modifiers)); + } + } + else + { + result = FindIncludingBaseTypes(type, t => t.GetMethod(name, all, null, parameters, modifiers)); + } + if (result == null) return null; + if (generics != null) result = result.MakeGenericMethod(generics); + return result; + } + + public static MethodInfo Method(string typeColonMethodname, Type[] parameters = null, Type[] generics = null) + { + if (typeColonMethodname == null) return null; + var parts = typeColonMethodname.Split(':'); + if (parts.Length != 2) + throw new ArgumentException("Method must be specified as 'Namespace.Type1.Type2:MethodName", nameof(typeColonMethodname)); + + var type = TypeByName(parts[0]); + return Method(type, parts[1], parameters, generics); + } + + public static List GetMethodNames(Type type) + { + if (type == null) return new List(); + return type.GetMethods(all).Select(m => m.Name).ToList(); + } + + public static List GetMethodNames(object instance) + { + if (instance == null) return new List(); + return GetMethodNames(instance.GetType()); + } + + public static ConstructorInfo StaticConstructor(Type type) + { + if (type == null) return null; + return type.GetConstructor(all & ~BindingFlags.Instance, null, new Type[0], new ParameterModifier[0]); + } + + public static ConstructorInfo DeclaredConstructor(Type type, Type[] parameters = null) + { + if (type == null) return null; + if (parameters == null) parameters = new Type[0]; + return type.GetConstructor(all & ~BindingFlags.Static, null, parameters, new ParameterModifier[0]); + } + + public static ConstructorInfo Constructor(Type type, Type[] parameters = null) + { + if (type == null) return null; + if (parameters == null) parameters = new Type[0]; + return FindIncludingBaseTypes(type, t => t.GetConstructor(all, null, parameters, new ParameterModifier[] { })); + } + + public static List GetDeclaredConstructors(Type type) + { + return type.GetConstructors(all).Where(method => method.DeclaringType == type).ToList(); + } + + public static List GetDeclaredMethods(Type type) + { + return type.GetMethods(all).Where(method => method.DeclaringType == type).ToList(); + } + + public static List GetDeclaredProperties(Type type) + { + return type.GetProperties(all).Where(property => property.DeclaringType == type).ToList(); + } + + public static List GetDeclaredFields(Type type) + { + return type.GetFields(all).Where(field => field.DeclaringType == type).ToList(); + } + + public static Type GetReturnedType(MethodBase method) + { + var constructor = method as ConstructorInfo; + if (constructor != null) return typeof(void); + return ((MethodInfo)method).ReturnType; + } + + public static Type Inner(Type type, string name) + { + if (type == null || name == null) return null; + return FindIncludingBaseTypes(type, t => t.GetNestedType(name, all)); + } + + public static Type FirstInner(Type type, Func predicate) + { + if (type == null || predicate == null) return null; + return type.GetNestedTypes(all).FirstOrDefault(subType => predicate(subType)); + } + + public static MethodInfo FirstMethod(Type type, Func predicate) + { + if (type == null || predicate == null) return null; + return type.GetMethods(all).FirstOrDefault(method => predicate(method)); + } + + public static ConstructorInfo FirstConstructor(Type type, Func predicate) + { + if (type == null || predicate == null) return null; + return type.GetConstructors(all).FirstOrDefault(constructor => predicate(constructor)); + } + + public static PropertyInfo FirstProperty(Type type, Func predicate) + { + if (type == null || predicate == null) return null; + return type.GetProperties(all).FirstOrDefault(property => predicate(property)); + } + + public static Type[] GetTypes(object[] parameters) + { + if (parameters == null) return new Type[0]; + return parameters.Select(p => p == null ? typeof(object) : p.GetType()).ToArray(); + } + + public static List GetFieldNames(Type type) + { + if (type == null) return new List(); + return type.GetFields(all).Select(f => f.Name).ToList(); + } + + public static List GetFieldNames(object instance) + { + if (instance == null) return new List(); + return GetFieldNames(instance.GetType()); + } + + public static List GetPropertyNames(Type type) + { + if (type == null) return new List(); + return type.GetProperties(all).Select(f => f.Name).ToList(); + } + + public static List GetPropertyNames(object instance) + { + if (instance == null) return new List(); + return GetPropertyNames(instance.GetType()); + } + + public delegate ref U FieldRef(T obj); + public static FieldRef FieldRefAccess(string fieldName) + { + const BindingFlags bf = BindingFlags.NonPublic | + BindingFlags.Instance | + BindingFlags.DeclaredOnly; + + var fi = typeof(T).GetField(fieldName, bf); + if (fi == null) + throw new MissingFieldException(typeof(T).Name, fieldName); + + var s_name = "__refget_" + typeof(T).Name + "_fi_" + fi.Name; + + // workaround for using ref-return with DynamicMethod: + // a.) initialize with dummy return value + var dm = new DynamicMethod(s_name, typeof(U), new[] { typeof(T) }, typeof(T), true); + + // b.) replace with desired 'ByRef' return value + var trv = Traverse.Create(dm); + trv.Field("returnType").SetValue(typeof(U).MakeByRefType()); + trv.Field("m_returnType").SetValue(typeof(U).MakeByRefType()); + + var il = dm.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldflda, fi); + il.Emit(OpCodes.Ret); + return (FieldRef)dm.CreateDelegate(typeof(FieldRef)); + } + + public static ref U FieldRefAccess(T instance, string fieldName) + { + return ref FieldRefAccess(fieldName)(instance); + } + + public static void ThrowMissingMemberException(Type type, params string[] names) + { + var fields = string.Join(",", GetFieldNames(type).ToArray()); + var properties = string.Join(",", GetPropertyNames(type).ToArray()); + throw new MissingMemberException(string.Join(",", names) + "; available fields: " + fields + "; available properties: " + properties); + } + + public static object GetDefaultValue(Type type) + { + if (type == null) return null; + if (type == typeof(void)) return null; + if (type.IsValueType) + return Activator.CreateInstance(type); + return null; + } + + public static object CreateInstance(Type type) + { + if (type == null) + throw new NullReferenceException("Cannot create instance for NULL type"); + var ctor = type.GetConstructor(BindingFlags.Public | BindingFlags.Instance, null, CallingConventions.Any, new Type[0], null); + if (ctor != null) + return Activator.CreateInstance(type); + return FormatterServices.GetUninitializedObject(type); + } + + public static object MakeDeepCopy(object source, Type resultType, Func processor = null, string pathRoot = "") + { + if (source == null) + return null; + + var type = source.GetType(); + + if (type.IsPrimitive) + return source; + + if (type.IsEnum) + return Enum.ToObject(resultType, (int)source); + + if (type.IsGenericType && resultType.IsGenericType) + { + var addOperation = FirstMethod(resultType, m => m.Name == "Add" && m.GetParameters().Count() == 1); + if (addOperation != null) + { + var addableResult = Activator.CreateInstance(resultType); + var addInvoker = MethodInvoker.GetHandler(addOperation); + var newElementType = resultType.GetGenericArguments()[0]; + var i = 0; + foreach (var element in source as IEnumerable) + { + var iStr = (i++).ToString(); + var path = pathRoot.Length > 0 ? pathRoot + "." + iStr : iStr; + var newElement = MakeDeepCopy(element, newElementType, processor, path); + addInvoker(addableResult, new object[] { newElement }); + } + return addableResult; + } + + // TODO: add dictionaries support + // maybe use methods in Dictionary> + } + + if (type.IsArray && resultType.IsArray) + { + var elementType = resultType.GetElementType(); + var length = ((Array)source).Length; + var arrayResult = Activator.CreateInstance(resultType, new object[] { length }) as object[]; + var originalArray = source as object[]; + for (var i = 0; i < length; i++) + { + var iStr = i.ToString(); + var path = pathRoot.Length > 0 ? pathRoot + "." + iStr : iStr; + arrayResult[i] = MakeDeepCopy(originalArray[i], elementType, processor, path); + } + return arrayResult; + } + + var ns = type.Namespace; + if (ns == "System" || (ns?.StartsWith("System.") ?? false)) + return source; + + var result = CreateInstance(resultType); + Traverse.IterateFields(source, result, (name, src, dst) => + { + var path = pathRoot.Length > 0 ? pathRoot + "." + name : name; + var value = processor != null ? processor(path, src, dst) : src.GetValue(); + dst.SetValue(MakeDeepCopy(value, dst.GetValueType(), processor, path)); + }); + return result; + } + + public static void MakeDeepCopy(object source, out T result, Func processor = null, string pathRoot = "") + { + result = (T)MakeDeepCopy(source, typeof(T), processor, pathRoot); + } + + public static bool IsStruct(Type type) + { + return type.IsValueType && !IsValue(type) && !IsVoid(type); + } + + public static bool IsClass(Type type) + { + return !type.IsValueType; + } + + public static bool IsValue(Type type) + { + return type.IsPrimitive || type.IsEnum; + } + + public static bool IsVoid(Type type) + { + return type == typeof(void); + } + } +} \ No newline at end of file diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Tools/DynamicTools.cs b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Tools/DynamicTools.cs new file mode 100644 index 0000000..66fb317 --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Tools/DynamicTools.cs @@ -0,0 +1,226 @@ +using Harmony.ILCopying; +using System; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; + +namespace Harmony +{ + public static class DynamicTools + { + /* TODO add support for functions that return structs larger than 8 bytes + * + * https://github.com/dotnet/coreclr/issues/12503 + * https://stackoverflow.com/questions/44641195/what-could-cause-p-invoke-arguments-to-be-out-of-order-when-passed + * + * ERROR + * Managed Debugging Assistant 'FatalExecutionEngineError' + * The runtime has encountered a fatal error. The address of the error was at 0x72747d0e, on thread 0x9c38. The error code is 0xc0000005. This error may be a bug in the CLR or in the unsafe or non-verifiable portions of user code. Common sources of this bug include user marshaling errors for COM-interop or PInvoke, which may corrupt the stack. + * + * Calling signatures change: + * .NET <4.5 jits to void Func(ref LargeReturnStruct, object this, params) + * .NET 4.5+ jits to Func(object this, ref LargeReturnStruct, params) + * + * // Test case + + [StructLayout(LayoutKind.Sequential, Pack = 8)] + public struct Struct1 + { + public double foo; + public double bar; + } + + public class StructTest1 + { + public Struct1 PatchMe() + { + return default(Struct1); + } + } + * + */ + public static DynamicMethod CreateDynamicMethod(MethodBase original, string suffix, bool isIl2Cpp) + { + if (original == null) throw new ArgumentNullException("original cannot be null"); + var patchName = original.Name + suffix; + patchName = patchName.Replace("<>", ""); + + var parameters = original.GetParameters(); + var result = parameters.Types().ToList(); + if (original.IsStatic == false) + result.Insert(0, isIl2Cpp ? original.DeclaringType : typeof(object)); + var paramTypes = result.ToArray(); + var returnType = AccessTools.GetReturnedType(original); + + // DynamicMethod does not support byref return types + if (returnType == null || returnType.IsByRef) + return null; + + DynamicMethod method; + try + { + method = new DynamicMethod( + patchName, + MethodAttributes.Public | MethodAttributes.Static, + CallingConventions.Standard, + returnType, + paramTypes, + original.DeclaringType, + true + ); + } + catch (Exception) + { + return null; + } + + for (var i = 0; i < parameters.Length; i++) + method.DefineParameter(i + 1, parameters[i].Attributes, parameters[i].Name); + + return method; + } + + public static ILGenerator CreateSaveableMethod(MethodBase original, string suffix, out AssemblyBuilder assemblyBuilder, out TypeBuilder typeBuilder) + { + var assemblyName = new AssemblyName("DebugAssembly"); + var path = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); + assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave, path); + var moduleBuilder = assemblyBuilder.DefineDynamicModule(assemblyName.Name, assemblyName.Name + ".dll"); + typeBuilder = moduleBuilder.DefineType("Debug" + original.DeclaringType.Name, TypeAttributes.Public); + + if (original == null) throw new ArgumentNullException("original cannot be null"); + var patchName = original.Name + suffix; + patchName = patchName.Replace("<>", ""); + + var parameters = original.GetParameters(); + var result = parameters.Types().ToList(); + if (original.IsStatic == false) + result.Insert(0, typeof(object)); + var paramTypes = result.ToArray(); + + var methodBuilder = typeBuilder.DefineMethod( + patchName, + MethodAttributes.Public | MethodAttributes.Static, + CallingConventions.Standard, + AccessTools.GetReturnedType(original), + paramTypes + ); + + return methodBuilder.GetILGenerator(); + } + + public static void SaveMethod(AssemblyBuilder assemblyBuilder, TypeBuilder typeBuilder) + { + var t = typeBuilder.CreateType(); + assemblyBuilder.Save("HarmonyDebugAssembly.dll"); + } + + public static LocalBuilder[] DeclareLocalVariables(MethodBase original, ILGenerator il, bool logOutput = true) + { + var vars = original.GetMethodBody()?.LocalVariables; + if (vars == null) + return new LocalBuilder[0]; + return vars.Select(lvi => + { + var localBuilder = il.DeclareLocal(lvi.LocalType, lvi.IsPinned); + if (logOutput) + Emitter.LogLocalVariable(il, localBuilder); + return localBuilder; + }).ToArray(); + } + + public static LocalBuilder DeclareLocalVariable(ILGenerator il, Type type) + { + if (type.IsByRef) type = type.GetElementType(); + + if (AccessTools.IsClass(type)) + { + var v = il.DeclareLocal(type); + Emitter.LogLocalVariable(il, v); + Emitter.Emit(il, OpCodes.Ldnull); + Emitter.Emit(il, OpCodes.Stloc, v); + return v; + } + if (AccessTools.IsStruct(type)) + { + var v = il.DeclareLocal(type); + Emitter.LogLocalVariable(il, v); + Emitter.Emit(il, OpCodes.Ldloca, v); + Emitter.Emit(il, OpCodes.Initobj, type); + return v; + } + if (AccessTools.IsValue(type)) + { + var v = il.DeclareLocal(type); + Emitter.LogLocalVariable(il, v); + if (type == typeof(float)) + Emitter.Emit(il, OpCodes.Ldc_R4, (float)0); + else if (type == typeof(double)) + Emitter.Emit(il, OpCodes.Ldc_R8, (double)0); + else if (type == typeof(long)) + Emitter.Emit(il, OpCodes.Ldc_I8, (long)0); + else + Emitter.Emit(il, OpCodes.Ldc_I4, 0); + Emitter.Emit(il, OpCodes.Stloc, v); + return v; + } + return null; + } + + public static void PrepareDynamicMethod(DynamicMethod method) + { + var nonPublicInstance = BindingFlags.NonPublic | BindingFlags.Instance; + var nonPublicStatic = BindingFlags.NonPublic | BindingFlags.Static; + + // on mono, just call 'CreateDynMethod' + // + var m_CreateDynMethod = typeof(DynamicMethod).GetMethod("CreateDynMethod", nonPublicInstance); + if (m_CreateDynMethod != null) + { + m_CreateDynMethod.Invoke(method, new object[0]); + return; + } + + // on all .NET Core versions, call 'RuntimeHelpers._CompileMethod' but with a different parameter: + // + var m__CompileMethod = typeof(RuntimeHelpers).GetMethod("_CompileMethod", nonPublicStatic); + + var m_GetMethodDescriptor = typeof(DynamicMethod).GetMethod("GetMethodDescriptor", nonPublicInstance); + var handle = (RuntimeMethodHandle)m_GetMethodDescriptor.Invoke(method, new object[0]); + + // 1) RuntimeHelpers._CompileMethod(handle.GetMethodInfo()) + // + var m_GetMethodInfo = typeof(RuntimeMethodHandle).GetMethod("GetMethodInfo", nonPublicInstance); + if (m_GetMethodInfo != null) + { + var runtimeMethodInfo = m_GetMethodInfo.Invoke(handle, new object[0]); + try + { + // this can throw BadImageFormatException "An attempt was made to load a program with an incorrect format" + m__CompileMethod.Invoke(null, new object[] { runtimeMethodInfo }); + return; + } + catch (Exception) + { + } + } + + // 2) RuntimeHelpers._CompileMethod(handle.Value) + // + if (m__CompileMethod.GetParameters()[0].ParameterType.IsAssignableFrom(handle.Value.GetType())) + { + m__CompileMethod.Invoke(null, new object[] { handle.Value }); + return; + } + + // 3) RuntimeHelpers._CompileMethod(handle) + // + if (m__CompileMethod.GetParameters()[0].ParameterType.IsAssignableFrom(handle.GetType())) + { + m__CompileMethod.Invoke(null, new object[] { handle }); + return; + } + } + } +} \ No newline at end of file diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Tools/Extensions.cs b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Tools/Extensions.cs new file mode 100644 index 0000000..f52491c --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Tools/Extensions.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace Harmony +{ + public static class GeneralExtensions + { + public static string Join(this IEnumerable enumeration, Func converter = null, string delimiter = ", ") + { + if (converter == null) converter = t => t.ToString(); + return enumeration.Aggregate("", (prev, curr) => prev + (prev != "" ? delimiter : "") + converter(curr)); + } + + public static string Description(this Type[] parameters) + { + if (parameters == null) return "NULL"; + var pattern = @", \w+, Version=[0-9.]+, Culture=neutral, PublicKeyToken=[0-9a-f]+"; + return "(" + parameters.Join(p => p?.FullName == null ? "null" : Regex.Replace(p.FullName, pattern, "")) + ")"; + } + + public static string FullDescription(this MethodBase method) + { + var parameters = method.GetParameters().Select(p => p.ParameterType).ToArray(); + return method.DeclaringType.FullName + "." + method.Name + parameters.Description(); + } + + public static Type[] Types(this ParameterInfo[] pinfo) + { + return pinfo.Select(pi => pi.ParameterType).ToArray(); + } + + public static T GetValueSafe(this Dictionary dictionary, S key) + { + T result; + if (dictionary.TryGetValue(key, out result)) + return result; + return default(T); + } + + public static T GetTypedValue(this Dictionary dictionary, string key) + { + object result; + if (dictionary.TryGetValue(key, out result)) + if (result is T) + return (T)result; + return default(T); + } + } + + public static class CollectionExtensions + { + public static void Do(this IEnumerable sequence, Action action) + { + if (sequence == null) return; + var enumerator = sequence.GetEnumerator(); + while (enumerator.MoveNext()) action(enumerator.Current); + } + + public static void DoIf(this IEnumerable sequence, Func condition, Action action) + { + sequence.Where(condition).Do(action); + } + + public static IEnumerable Add(this IEnumerable sequence, T item) + { + return (sequence ?? Enumerable.Empty()).Concat(new[] { item }); + } + + public static T[] AddRangeToArray(this T[] sequence, T[] items) + { + return (sequence ?? Enumerable.Empty()).Concat(items).ToArray(); + } + + public static T[] AddToArray(this T[] sequence, T item) + { + return Add(sequence, item).ToArray(); + } + } +} \ No newline at end of file diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Tools/FileLog.cs b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Tools/FileLog.cs new file mode 100644 index 0000000..1659676 --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Tools/FileLog.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; + +namespace Harmony +{ + public static class FileLog + { + public static string logPath; + public static char indentChar = '\t'; + public static int indentLevel = 0; + static List buffer = new List(); + + [UpgradeToLatestVersion(1)] + static FileLog() + { + logPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + Path.DirectorySeparatorChar + "harmony.log.txt"; + } + + static string IndentString() + { + return new string(indentChar, indentLevel); + } + + public static void ChangeIndent(int delta) + { + indentLevel = Math.Max(0, indentLevel + delta); + } + + // use this method only if you are sure that FlushBuffer will be called + // or else logging information is incomplete in case of a crash + // + public static void LogBuffered(string str) + { + lock (logPath) + { + buffer.Add(IndentString() + str); + } + } + + public static void FlushBuffer() + { + lock (logPath) + { + if (buffer.Count > 0) + { + using (var writer = File.AppendText(logPath)) + { + foreach (var str in buffer) + writer.WriteLine(str); + } + buffer.Clear(); + } + } + } + + // this is the slower method that flushes changes directly to the file + // to prevent missing information in case of a cache + // + public static void Log(string str) + { + lock (logPath) + { + using (var writer = File.AppendText(logPath)) + { + writer.WriteLine(IndentString() + str); + } + } + } + + public static void Reset() + { + lock (logPath) + { + var path = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + Path.DirectorySeparatorChar + "harmony.log.txt"; + File.Delete(path); + } + } + + public static unsafe void LogBytes(long ptr, int len) + { + lock (logPath) + { + var p = (byte*)ptr; + var s = ""; + for (var i = 1; i <= len; i++) + { + if (s == "") s = "# "; + s = s + (*p).ToString("X2") + " "; + if (i > 1 || len == 1) + { + if (i % 8 == 0 || i == len) + { + Log(s); + s = ""; + } + else if (i % 4 == 0) + s = s + " "; + } + p++; + } + + var arr = new byte[len]; + Marshal.Copy((IntPtr)ptr, arr, 0, len); + var md5Hash = MD5.Create(); + var hash = md5Hash.ComputeHash(arr); +#pragma warning disable XS0001 + var sBuilder = new StringBuilder(); +#pragma warning restore XS0001 + for (var i = 0; i < hash.Length; i++) + sBuilder.Append(hash[i].ToString("X2")); + Log("HASH: " + sBuilder); + } + } + } +} \ No newline at end of file diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Tools/PatchTools.cs b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Tools/PatchTools.cs new file mode 100644 index 0000000..8d374dd --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Tools/PatchTools.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Harmony +{ + public static class PatchTools + { + // this holds all the objects we want to keep alive so they don't get garbage-collected + static Dictionary objectReferences = new Dictionary(); + public static void RememberObject(object key, object value) + { + objectReferences[key] = value; + } + + public static MethodInfo GetPatchMethod(Type patchType, string name, Type[] parameters = null) + { + var method = patchType.GetMethods(AccessTools.all) + .FirstOrDefault(m => m.GetCustomAttributes(typeof(T), true).Any()); + if (method == null) + method = AccessTools.Method(patchType, name, parameters); + return method; + } + + public static void GetPatches(Type patchType, out MethodInfo prefix, out MethodInfo postfix, out MethodInfo transpiler) + { + prefix = GetPatchMethod(patchType, "Prefix"); + postfix = GetPatchMethod(patchType, "Postfix"); + transpiler = GetPatchMethod(patchType, "Transpiler"); + } + } +} \ No newline at end of file diff --git a/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Tools/SelfPatching.cs b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Tools/SelfPatching.cs new file mode 100644 index 0000000..c352de3 --- /dev/null +++ b/BepInEx.MelonLoader.Loader/MelonLoader/Harmony/Tools/SelfPatching.cs @@ -0,0 +1,134 @@ +using Harmony.ILCopying; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; + +namespace Harmony.Tools +{ + internal class SelfPatching + { + static readonly int upgradeToLatestVersionFullNameHash = typeof(UpgradeToLatestVersion).FullName.GetHashCode(); + + [UpgradeToLatestVersion(1)] + static int GetVersion(MethodBase method) + { + var attribute = method.GetCustomAttributes(false) + .Where(attr => attr.GetType().FullName.GetHashCode() == upgradeToLatestVersionFullNameHash) + .FirstOrDefault(); + if (attribute == null) + return -1; + return Traverse.Create(attribute).Field("version").GetValue(); + } + + [UpgradeToLatestVersion(1)] + static string MethodKey(MethodBase method) + { + return method.FullDescription(); + } + + [UpgradeToLatestVersion(1)] + static bool IsHarmonyAssembly(Assembly assembly) + { + try + { + return assembly.ReflectionOnly == false && assembly.GetType(typeof(HarmonyInstance).FullName) != null; + } + catch (Exception) + { + return false; + } + } + + private static List GetAllMethods(Assembly assembly) + { + var types = assembly.GetTypes(); + return types + .SelectMany(type => type.GetMethods(AccessTools.all).Cast
+ + Version 2 + A library for patching, replacing and decorating + .NET and Mono methods during runtime. +
+ + + + + + + + + + + + +
+ + + + + + + + + +