diff --git a/.gitmodules b/.gitmodules index f433f06..0629616 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,4 +3,4 @@ url = https://github.com/BepInEx/BepInEx.Harmony [submodule "submodules/MonoMod"] path = submodules/MonoMod - url = https://github.com/0x0ade/MonoMod.git + url = https://github.com/MonoMod/MonoMod.git diff --git a/BepInEx.Patcher/BepInEx.Patcher.csproj b/BepInEx.Patcher/BepInEx.Patcher.csproj index b645247..db8fa92 100644 --- a/BepInEx.Patcher/BepInEx.Patcher.csproj +++ b/BepInEx.Patcher/BepInEx.Patcher.csproj @@ -1,7 +1,6 @@  - Debug @@ -20,7 +19,7 @@ BepInEx.Patcher.Program - + true ..\bin\patcher\ TRACE @@ -30,29 +29,10 @@ prompt MinimumRecommendedRules.ruleset - - true - ..\bin\patcher\ - TRACE;UNITY_2018 - true - embedded - AnyCPU - prompt - MinimumRecommendedRules.ruleset - ..\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.dll - - ..\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.Mdb.dll - - - ..\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.Pdb.dll - - - ..\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.Rocks.dll - @@ -62,6 +42,7 @@ + @@ -73,19 +54,8 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - + - - - - - - - - - - - - + \ No newline at end of file diff --git a/BepInEx.Patcher/ILRepack.targets b/BepInEx.Patcher/ILRepack.targets new file mode 100644 index 0000000..071b3b5 --- /dev/null +++ b/BepInEx.Patcher/ILRepack.targets @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/BepInEx.Patcher/Program.cs b/BepInEx.Patcher/Program.cs index 187168a..509f427 100644 --- a/BepInEx.Patcher/Program.cs +++ b/BepInEx.Patcher/Program.cs @@ -26,6 +26,14 @@ namespace BepInEx.Patcher Console.ResetColor(); } + static string GetUnityEngineAssembly(string managedDir) + { + var path = Path.Combine(managedDir, "UnityEngine.CoreModule.dll"); + if (File.Exists(path)) + return path; + return Path.Combine(managedDir, "UnityEngine.dll"); + } + static void Main(string[] args) { Console.WriteLine($"BepInEx Patcher v{Assembly.GetExecutingAssembly().GetName().Version}"); @@ -43,12 +51,7 @@ namespace BepInEx.Patcher string managedDir = Environment.CurrentDirectory + $@"\{gameName}_Data\Managed"; -#if UNITY_2018 - string unityOutputDLL = Path.GetFullPath($"{managedDir}\\UnityEngine.CoreModule.dll"); -#else - string unityOutputDLL = Path.GetFullPath($"{managedDir}\\UnityEngine.dll"); -#endif - + string unityOutputDLL = GetUnityEngineAssembly(managedDir); if (!Directory.Exists(managedDir) || !File.Exists(unityOutputDLL)) continue; @@ -101,11 +104,7 @@ namespace BepInEx.Patcher AssemblyResolver = defaultResolver }; -#if UNITY_2018 - string unityBackupDLL = Path.GetFullPath($"{managedDir}\\UnityEngine.CoreModule.dll.bak"); -#else - string unityBackupDLL = Path.GetFullPath($"{managedDir}\\UnityEngine.dll.bak"); -#endif + string unityBackupDLL = $"{GetUnityEngineAssembly(managedDir)}.bak"; //determine which assembly to use as a base AssemblyDefinition unity = AssemblyDefinition.ReadAssembly(unityOutputDLL, rp); diff --git a/BepInEx.Patcher/packages.config b/BepInEx.Patcher/packages.config index eed4416..68b8c20 100644 --- a/BepInEx.Patcher/packages.config +++ b/BepInEx.Patcher/packages.config @@ -1,6 +1,7 @@  + - + \ No newline at end of file diff --git a/BepInEx.Preloader/BepInEx.Preloader.csproj b/BepInEx.Preloader/BepInEx.Preloader.csproj index 1be0603..1692a76 100644 --- a/BepInEx.Preloader/BepInEx.Preloader.csproj +++ b/BepInEx.Preloader/BepInEx.Preloader.csproj @@ -13,7 +13,7 @@ 512 true - + none true ..\bin\ @@ -22,36 +22,10 @@ 4 ..\bin\BepInEx.Preloader.xml - - ..\bin\ - TRACE;UNITY_2018 - true - none - AnyCPU - prompt - MinimumRecommendedRules.ruleset - - - bin\Debug\ - TRACE;UNITY_2018 - true - AnyCPU - prompt - MinimumRecommendedRules.ruleset - ..\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.dll - - ..\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.Mdb.dll - - - ..\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.Pdb.dll - - - ..\packages\Mono.Cecil.0.10.3\lib\net35\Mono.Cecil.Rocks.dll - diff --git a/BepInEx.Preloader/Entrypoint.cs b/BepInEx.Preloader/Entrypoint.cs index ebb1118..ec2ec3e 100644 --- a/BepInEx.Preloader/Entrypoint.cs +++ b/BepInEx.Preloader/Entrypoint.cs @@ -5,38 +5,18 @@ using System.Reflection; namespace BepInEx.Preloader { - internal static class Entrypoint + internal static class PreloaderRunner { - /// - /// The main entrypoint of BepInEx, called from Doorstop. - /// - /// - /// The arguments passed in from Doorstop. First argument is the path of the currently executing - /// process. - /// - public static void Main(string[] args) + public static void PreloaderMain(string[] args) { - EnvVars.LoadVars(); - string bepinPath = Path.GetDirectoryName(Path.GetDirectoryName(Path.GetFullPath(EnvVars.DOORSTOP_INVOKE_DLL_PATH))); Paths.SetExecutablePath(args[0], bepinPath, EnvVars.DOORSTOP_MANAGED_FOLDER_DIR); AppDomain.CurrentDomain.AssemblyResolve += LocalResolve; - Preloader.Run(); } - /// - /// A handler for to perform some special handling. - /// - /// It attempts to check currently loaded assemblies (ignoring the version), and then checks the BepInEx/core path, - /// BepInEx/patchers path and the BepInEx folder, all in that order. - /// - /// - /// - /// - /// - internal static Assembly LocalResolve(object sender, ResolveEventArgs args) + private static Assembly LocalResolve(object sender, ResolveEventArgs args) { var assemblyName = new AssemblyName(args.Name); @@ -54,4 +34,48 @@ namespace BepInEx.Preloader return null; } } + + internal static class Entrypoint + { + private static string preloaderPath; + + /// + /// The main entrypoint of BepInEx, called from Doorstop. + /// + /// + /// The arguments passed in from Doorstop. First argument is the path of the currently executing + /// process. + /// + public static void Main(string[] args) + { + EnvVars.LoadVars(); + + // Get the path of this DLL via Doorstop env var because Assembly.Location mangles non-ASCII characters on some versions of Mono for unknown reasons + preloaderPath = Path.GetDirectoryName(Path.GetFullPath(EnvVars.DOORSTOP_INVOKE_DLL_PATH)); + + AppDomain.CurrentDomain.AssemblyResolve += ResolveCurrentDirectory; + + // In some versions of Unity 4, Mono tries to resolve BepInEx.dll prematurely because of the call to Paths.SetExecutablePath + // To prevent that, we have to use reflection and a separate startup class so that we can install required assembly resolvers before the main code + typeof(Entrypoint).Assembly.GetType($"BepInEx.Preloader.{nameof(PreloaderRunner)}") + ?.GetMethod(nameof(PreloaderRunner.PreloaderMain)) + ?.Invoke(null, new object[] { args }); + + AppDomain.CurrentDomain.AssemblyResolve -= ResolveCurrentDirectory; + } + + private static Assembly ResolveCurrentDirectory(object sender, ResolveEventArgs args) + { + var name = new AssemblyName(args.Name); + + try + { + return Assembly.LoadFile(Path.Combine(preloaderPath, $"{name.Name}.dll")); + } + catch (Exception) + { + return null; + } + } + } } \ No newline at end of file diff --git a/BepInEx.Preloader/Logger/PreloaderLogWriter.cs b/BepInEx.Preloader/Logger/PreloaderLogWriter.cs index 2b23fa4..b73d781 100644 --- a/BepInEx.Preloader/Logger/PreloaderLogWriter.cs +++ b/BepInEx.Preloader/Logger/PreloaderLogWriter.cs @@ -56,7 +56,6 @@ namespace BepInEx.Preloader public void Dispose() { - if (LoggerSource != null) { Console.SetOut(StandardOut); diff --git a/BepInEx.Preloader/Patching/AssemblyPatcher.cs b/BepInEx.Preloader/Patching/AssemblyPatcher.cs index 249b8db..e01aa5f 100644 --- a/BepInEx.Preloader/Patching/AssemblyPatcher.cs +++ b/BepInEx.Preloader/Patching/AssemblyPatcher.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; +using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using BepInEx.Preloader.RuntimeFixes; @@ -23,6 +24,8 @@ namespace BepInEx.Preloader.Patching /// internal static class AssemblyPatcher { + private const BindingFlags ALL = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.IgnoreCase; + public static List PatcherPlugins { get; } = new List(); private static readonly string DumpedAssembliesPath = Path.Combine(Paths.BepInExRootPath, "DumpedAssemblies"); @@ -36,29 +39,108 @@ namespace BepInEx.Preloader.Patching PatcherPlugins.Add(patcher); } + private static T CreateDelegate(MethodInfo method) where T : class => method != null ? Delegate.CreateDelegate(typeof(T), method) as T : null; + + private static PatcherPlugin ToPatcherPlugin(TypeDefinition type) + { + if (type.IsInterface || type.IsAbstract && !type.IsSealed) + return null; + + var targetDlls = type.Methods.FirstOrDefault(m => m.Name.Equals("get_TargetDLLs", StringComparison.InvariantCultureIgnoreCase) && + m.IsPublic && + m.IsStatic); + + if (targetDlls == null || + targetDlls.ReturnType.FullName != "System.Collections.Generic.IEnumerable`1") + return null; + + var patch = type.Methods.FirstOrDefault(m => m.Name.Equals("Patch") && + m.IsPublic && + m.IsStatic && + m.ReturnType.FullName == "System.Void" && + m.Parameters.Count == 1 && + (m.Parameters[0].ParameterType.FullName == "Mono.Cecil.AssemblyDefinition&" || + m.Parameters[0].ParameterType.FullName == "Mono.Cecil.AssemblyDefinition")); + + if (patch == null) + return null; + + return new PatcherPlugin + { + TypeName = type.FullName + }; + } + /// /// Adds all patchers from all managed assemblies specified in a directory. /// /// Directory to search patcher DLLs from. /// A function that locates assembly patchers in a given managed assembly. - public static void AddPatchersFromDirectory(string directory, - Func> patcherLocator) + public static void AddPatchersFromDirectory(string directory) { if (!Directory.Exists(directory)) return; var sortedPatchers = new SortedDictionary(); - foreach (string assemblyPath in Directory.GetFiles(directory, "*.dll", SearchOption.AllDirectories)) - try - { - var assembly = Assembly.LoadFrom(assemblyPath); + var patchers = TypeLoader.FindPluginTypes(directory, ToPatcherPlugin); - foreach (var patcher in patcherLocator(assembly)) - sortedPatchers.Add(patcher.Name, patcher); + foreach (var keyValuePair in patchers) + { + var assemblyPath = keyValuePair.Key; + var patcherCollection = keyValuePair.Value; + + var ass = Assembly.LoadFile(assemblyPath); + + foreach (var patcherPlugin in patcherCollection) + { + try + { + var type = ass.GetType(patcherPlugin.TypeName); + + var methods = type.GetMethods(ALL); + + patcherPlugin.Initializer = CreateDelegate(methods.FirstOrDefault(m => m.Name.Equals("Initialize", StringComparison.InvariantCultureIgnoreCase) && + m.GetParameters().Length == 0 && + m.ReturnType == typeof(void))); + + patcherPlugin.Finalizer = CreateDelegate(methods.FirstOrDefault(m => m.Name.Equals("Finish", StringComparison.InvariantCultureIgnoreCase) && + m.GetParameters().Length == 0 && + m.ReturnType == typeof(void))); + + patcherPlugin.TargetDLLs = CreateDelegate>>(type.GetProperty("TargetDLLs", ALL).GetGetMethod()); + + var patcher = methods.FirstOrDefault(m => m.Name.Equals("Patch", StringComparison.CurrentCultureIgnoreCase) && + m.ReturnType == typeof(void) && + m.GetParameters().Length == 1 && + (m.GetParameters()[0].ParameterType == typeof(AssemblyDefinition) || + m.GetParameters()[0].ParameterType == typeof(AssemblyDefinition).MakeByRefType())); + + patcherPlugin.Patcher = (ref AssemblyDefinition pAss) => + { + //we do the array fuckery here to get the ref result out + object[] args = { pAss }; + + patcher.Invoke(null, args); + + pAss = (AssemblyDefinition)args[0]; + }; + + sortedPatchers.Add($"{ass.GetName().Name}/{type.FullName}", patcherPlugin); + } + catch (Exception e) + { + Logger.LogError($"Failed to load patcher [{patcherPlugin.TypeName}]: {e.Message}"); + if (e is ReflectionTypeLoadException re) + Logger.LogDebug(TypeLoader.TypeLoadExceptionToString(re)); + else + Logger.LogDebug(e.ToString()); + } } - catch (BadImageFormatException) { } //unmanaged DLL - catch (ReflectionTypeLoadException) { } //invalid references + + Logger.Log(patcherCollection.Any() ? LogLevel.Info : LogLevel.Debug, + $"Loaded {patcherCollection.Count} patcher methods from {ass.GetName().FullName}"); + } foreach (KeyValuePair patcher in sortedPatchers) AddPatcher(patcher.Value); @@ -123,10 +205,10 @@ namespace BepInEx.Preloader.Patching // Then, perform the actual patching var patchedAssemblies = new HashSet(); foreach (var assemblyPatcher in PatcherPlugins) - foreach (string targetDll in assemblyPatcher.TargetDLLs) + foreach (string targetDll in assemblyPatcher.TargetDLLs()) if (assemblies.TryGetValue(targetDll, out var assembly)) { - Logger.LogInfo($"Patching [{assembly.Name.Name}] with [{assemblyPatcher.Name}]"); + Logger.LogInfo($"Patching [{assembly.Name.Name}] with [{assemblyPatcher.TypeName}]"); assemblyPatcher.Patcher?.Invoke(ref assembly); assemblies[targetDll] = assembly; @@ -135,7 +217,6 @@ namespace BepInEx.Preloader.Patching // Finally, load patched assemblies into memory - if (ConfigDumpAssemblies.Value || ConfigLoadDumpedAssemblies.Value) { if (!Directory.Exists(DumpedAssembliesPath)) @@ -153,7 +234,7 @@ namespace BepInEx.Preloader.Patching if (ConfigBreakBeforeLoadAssemblies.Value) { - Logger.LogInfo($"BepInEx is about load the following assemblies:\n{string.Join("\n", patchedAssemblies.ToArray())}"); + Logger.LogInfo(data: $"BepInEx is about load the following assemblies:\n{String.Join("\n", patchedAssemblies.ToArray())}"); Logger.LogInfo($"The assemblies were dumped into {DumpedAssembliesPath}"); Logger.LogInfo("Load any assemblies into the debugger, set breakpoints and continue execution."); Debugger.Break(); diff --git a/BepInEx.Preloader/Patching/PatcherPlugin.cs b/BepInEx.Preloader/Patching/PatcherPlugin.cs index b602d4b..390cc88 100644 --- a/BepInEx.Preloader/Patching/PatcherPlugin.cs +++ b/BepInEx.Preloader/Patching/PatcherPlugin.cs @@ -1,17 +1,19 @@ using System; using System.Collections.Generic; +using System.IO; +using BepInEx.Bootstrap; namespace BepInEx.Preloader.Patching { /// /// A single assembly patcher. /// - internal class PatcherPlugin + internal class PatcherPlugin : ICacheable { /// /// Target assemblies to patch. /// - public IEnumerable TargetDLLs { get; set; } = null; + public Func> TargetDLLs { get; set; } = null; /// /// Initializer method that is run before any patching occurs. @@ -29,8 +31,18 @@ namespace BepInEx.Preloader.Patching public AssemblyPatcherDelegate Patcher { get; set; } = null; /// - /// Name of the patcher. + /// Type name of the patcher. /// - public string Name { get; set; } = string.Empty; + public string TypeName { get; set; } = string.Empty; + + public void Save(BinaryWriter bw) + { + bw.Write(TypeName); + } + + public void Load(BinaryReader br) + { + TypeName = br.ReadString(); + } } } \ No newline at end of file diff --git a/BepInEx.Preloader/Preloader.cs b/BepInEx.Preloader/Preloader.cs index a30b0a1..fd90677 100644 --- a/BepInEx.Preloader/Preloader.cs +++ b/BepInEx.Preloader/Preloader.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; -using System.Reflection; using System.Text; using BepInEx.Configuration; using BepInEx.Logging; @@ -11,6 +10,7 @@ using BepInEx.Preloader.Patching; using BepInEx.Preloader.RuntimeFixes; using Mono.Cecil; using Mono.Cecil.Cil; +using MonoMod.RuntimeDetour; using UnityInjector.ConsoleUtil; using MethodAttributes = Mono.Cecil.MethodAttributes; @@ -26,24 +26,32 @@ namespace BepInEx.Preloader /// private static PreloaderConsoleListener PreloaderLog { get; set; } + public static bool IsPostUnity2017 { get; } = File.Exists(Path.Combine(Paths.ManagedPath, "UnityEngine.CoreModule.dll")); + public static void Run() { try { AllocateConsole(); - if (ConfigApplyRuntimePatches.Value) - UnityPatches.Apply(); + Utility.TryDo(() => + { + if (ConfigShimHarmony.Value) + HarmonyDetourBridge.Init(); + }, out var harmonyBridgeException); + + Utility.TryDo(() => + { + if (ConfigApplyRuntimePatches.Value) + UnityPatches.Apply(); + }, out var runtimePatchException); Logger.Sources.Add(TraceLogSource.CreateSource()); PreloaderLog = new PreloaderConsoleListener(ConfigPreloaderCOutLogging.Value); - Logger.Listeners.Add(PreloaderLog); - - string consoleTile = $"BepInEx {typeof(Paths).Assembly.GetName().Version} RC1 - {Process.GetCurrentProcess().ProcessName}"; - + string consoleTile = $"BepInEx {typeof(Paths).Assembly.GetName().Version} - {Process.GetCurrentProcess().ProcessName}"; ConsoleWindow.Title = consoleTile; Logger.LogMessage(consoleTile); @@ -53,35 +61,33 @@ namespace BepInEx.Preloader if (attributes.Length > 0) { var attribute = (BuildInfoAttribute)attributes[0]; - Logger.LogMessage(attribute.Info); } -#if UNITY_2018 - Logger.LogMessage("Compiled in Unity v2018 mode"); -#else - Logger.LogMessage("Compiled in Legacy Unity mode"); -#endif + Logger.LogInfo($"Running under Unity v{FileVersionInfo.GetVersionInfo(Paths.ExecutablePath).FileVersion}"); + Logger.LogInfo($"CLR runtime version: {Environment.Version}"); + Logger.LogInfo($"Supports SRE: {Utility.CLRSupportsDynamicAssemblies}"); - Logger.LogInfo($"Running under Unity v{Process.GetCurrentProcess().MainModule.FileVersionInfo.FileVersion}"); + if (harmonyBridgeException != null) + Logger.LogWarning($"Failed to enable fix for Harmony for .NET Standard API. Error message: {harmonyBridgeException.Message}"); + + if (runtimePatchException != null) + Logger.LogWarning($"Failed to apply runtime patches for Mono. See more info in the output log. Error message: {runtimePatchException.Message}"); Logger.LogMessage("Preloader started"); - AssemblyPatcher.AddPatcher(new PatcherPlugin - { TargetDLLs = new[] { ConfigEntrypointAssembly.Value }, - Patcher = PatchEntrypoint, - Name = "BepInEx.Chainloader" - }); + { + TargetDLLs = () => new[] { ConfigEntrypointAssembly.Value }, + Patcher = PatchEntrypoint, + TypeName = "BepInEx.Chainloader" + }); - AssemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath, GetPatcherMethods); + AssemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath); Logger.LogInfo($"{AssemblyPatcher.PatcherPlugins.Count} patcher plugin(s) loaded"); - AssemblyPatcher.PatchAndLoad(Paths.ManagedPath); - - AssemblyPatcher.DisposePatchers(); @@ -122,97 +128,6 @@ namespace BepInEx.Preloader } } - /// - /// Scans the assembly for classes that use the patcher contract, and returns a list of valid patchers. - /// - /// The assembly to scan. - /// A list of assembly patchers that were found in the assembly. - public static List GetPatcherMethods(Assembly assembly) - { - var patcherMethods = new List(); - var flags = BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase; - - foreach (var type in assembly.GetExportedTypes()) - try - { - if (type.IsInterface) - continue; - - var targetsProperty = type.GetProperty("TargetDLLs", - flags, - null, - typeof(IEnumerable), - Type.EmptyTypes, - null); - - //first try get the ref patcher method - var patcher = type.GetMethod("Patch", - flags, - null, - CallingConventions.Any, - new[] { typeof(AssemblyDefinition).MakeByRefType() }, - null); - - if (patcher == null) //otherwise try getting the non-ref patcher method - patcher = type.GetMethod("Patch", - flags, - null, - CallingConventions.Any, - new[] { typeof(AssemblyDefinition) }, - null); - - if (targetsProperty == null || !targetsProperty.CanRead || patcher == null) - continue; - - var assemblyPatcher = new PatcherPlugin(); - - assemblyPatcher.Name = $"{assembly.GetName().Name}/{type.FullName}"; - assemblyPatcher.Patcher = (ref AssemblyDefinition ass) => - { - //we do the array fuckery here to get the ref result out - object[] args = { ass }; - - patcher.Invoke(null, args); - - ass = (AssemblyDefinition)args[0]; - }; - - assemblyPatcher.TargetDLLs = (IEnumerable)targetsProperty.GetValue(null, null); - - var initMethod = type.GetMethod("Initialize", - flags, - null, - CallingConventions.Any, - Type.EmptyTypes, - null); - - if (initMethod != null) - assemblyPatcher.Initializer = () => initMethod.Invoke(null, null); - - var finalizeMethod = type.GetMethod("Finish", - flags, - null, - CallingConventions.Any, - Type.EmptyTypes, - null); - - if (finalizeMethod != null) - assemblyPatcher.Finalizer = () => finalizeMethod.Invoke(null, null); - - patcherMethods.Add(assemblyPatcher); - } - catch (Exception ex) - { - Logger.LogWarning($"Could not load patcher methods from {assembly.GetName().Name}"); - Logger.LogWarning(ex); - } - - Logger.Log(patcherMethods.Count > 0 ? LogLevel.Info : LogLevel.Debug, - $"Loaded {patcherMethods.Count} patcher methods from {assembly.GetName().Name}"); - - return patcherMethods; - } - /// /// Inserts BepInEx's own chainloader entrypoint into UnityEngine. /// @@ -281,9 +196,9 @@ namespace BepInEx.Preloader il.InsertBefore(ins, il.Create(OpCodes.Ldnull)); // gameExePath (always null, we initialize the Paths class in Entrypoint il.InsertBefore(ins, - il.Create(OpCodes.Ldc_I4_0)); // startConsole (always false, we already load the console in Preloader) + il.Create(OpCodes.Ldc_I4_0)); //startConsole (always false, we already load the console in Preloader) il.InsertBefore(ins, - il.Create(OpCodes.Call, initMethod)); // Chainloader.Initialize(string gameExePath, bool startConsole = true) + il.Create(OpCodes.Call, initMethod)); // Chainloader.Initialize(string gamePath, string managedPath = null, bool startConsole = true) il.InsertBefore(ins, il.Create(OpCodes.Call, startMethod)); } @@ -323,12 +238,8 @@ namespace BepInEx.Preloader "Preloader.Entrypoint", "Assembly", "The local filename of the assembly to target.", -#if UNITY_2018 - "UnityEngine.CoreModule.dll" -#else - "UnityEngine.dll" -#endif - ); + IsPostUnity2017 ? "UnityEngine.CoreModule.dll" : "UnityEngine.dll" + ); private static readonly ConfigWrapper ConfigEntrypointType = ConfigFile.CoreConfig.Wrap( "Preloader.Entrypoint", @@ -348,6 +259,12 @@ namespace BepInEx.Preloader "Enables or disables runtime patches.\nThis should always be true, unless you cannot start the game due to a Harmony related issue (such as running .NET Standard runtime) or you know what you're doing.", true); + private static readonly ConfigWrapper ConfigShimHarmony = ConfigFile.CoreConfig.Wrap( + "Preloader", + "ShimHarmonySupport", + "If enabled, basic Harmony functionality is patched to use MonoMod's RuntimeDetour instead.\nTry using this if Harmony does not work in a game.", + !Utility.CLRSupportsDynamicAssemblies); + private static readonly ConfigWrapper ConfigPreloaderCOutLogging = ConfigFile.CoreConfig.Wrap( "Logging", "PreloaderConsoleOutRedirection", diff --git a/BepInEx.Preloader/RuntimeFixes/UnityPatches.cs b/BepInEx.Preloader/RuntimeFixes/UnityPatches.cs index a6a8c4d..6d406ac 100644 --- a/BepInEx.Preloader/RuntimeFixes/UnityPatches.cs +++ b/BepInEx.Preloader/RuntimeFixes/UnityPatches.cs @@ -39,18 +39,5 @@ namespace BepInEx.Preloader.RuntimeFixes if (AssemblyLocations.TryGetValue(__instance.FullName, out string location)) __result = $"file://{location.Replace('\\', '/')}"; } - -#if UNITY_2018 -/* - * DESC: Workaround for Trace class not working because of missing .config file - * AFFECTS: Unity 2018+ (not .NET Standard / MonoBleedingEdge runtimes) - */ - [HarmonyPostfix, HarmonyPatch(typeof(AppDomain), nameof(AppDomain.SetupInformation), MethodType.Getter)] - public static void GetExeConfigName(AppDomainSetup __result) - { - __result.ApplicationBase = $"file://{Paths.GameRootPath}"; - __result.ConfigurationFile = "app.config"; - } -#endif } } \ No newline at end of file diff --git a/BepInEx.Preloader/packages.config b/BepInEx.Preloader/packages.config index 020bed6..2fbe0b1 100644 --- a/BepInEx.Preloader/packages.config +++ b/BepInEx.Preloader/packages.config @@ -1,4 +1,5 @@  + \ No newline at end of file diff --git a/BepInEx.sln b/BepInEx.sln index 2538f19..3c6a5ae 100644 --- a/BepInEx.sln +++ b/BepInEx.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.27130.2027 +VisualStudioVersion = 15.0.28307.757 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BepInEx", "BepInEx\BepInEx.csproj", "{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}" EndProject @@ -35,82 +35,44 @@ EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU - Legacy|Any CPU = Legacy|Any CPU Release|Any CPU = Release|Any CPU - v2018|Any CPU = v2018|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Legacy|Any CPU.ActiveCfg = Legacy|Any CPU - {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Legacy|Any CPU.Build.0 = Legacy|Any CPU - {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Release|Any CPU.ActiveCfg = v2018|Any CPU - {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Release|Any CPU.Build.0 = v2018|Any CPU - {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.v2018|Any CPU.ActiveCfg = v2018|Any CPU - {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.v2018|Any CPU.Build.0 = v2018|Any CPU - {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Debug|Any CPU.ActiveCfg = v2018|Any CPU - {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Debug|Any CPU.Build.0 = v2018|Any CPU - {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Legacy|Any CPU.ActiveCfg = Legacy|Any CPU - {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Legacy|Any CPU.Build.0 = Legacy|Any CPU - {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Release|Any CPU.ActiveCfg = v2018|Any CPU - {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Release|Any CPU.Build.0 = v2018|Any CPU - {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.v2018|Any CPU.ActiveCfg = v2018|Any CPU - {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.v2018|Any CPU.Build.0 = v2018|Any CPU + {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Release|Any CPU.Build.0 = Release|Any CPU + {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Debug|Any CPU.ActiveCfg = Release|Any CPU + {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Debug|Any CPU.Build.0 = Release|Any CPU + {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Release|Any CPU.Build.0 = Release|Any CPU {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Debug|Any CPU.ActiveCfg = Release|Any CPU {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Debug|Any CPU.Build.0 = Release|Any CPU - {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Legacy|Any CPU.ActiveCfg = Release|Any CPU - {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Legacy|Any CPU.Build.0 = Release|Any CPU {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Release|Any CPU.ActiveCfg = Release|Any CPU {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Release|Any CPU.Build.0 = Release|Any CPU - {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.v2018|Any CPU.ActiveCfg = Release|Any CPU - {6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.v2018|Any CPU.Build.0 = Release|Any CPU {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Legacy|Any CPU.ActiveCfg = Release|Any CPU - {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Legacy|Any CPU.Build.0 = Release|Any CPU {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Release|Any CPU.ActiveCfg = Release|Any CPU {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Release|Any CPU.Build.0 = Release|Any CPU - {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.v2018|Any CPU.ActiveCfg = Release|Any CPU - {54161CFE-FF42-4DDE-B161-3A49545DB5CD}.v2018|Any CPU.Build.0 = Release|Any CPU {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Legacy|Any CPU.ActiveCfg = Release|Any CPU - {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Legacy|Any CPU.Build.0 = Release|Any CPU {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Release|Any CPU.ActiveCfg = Release|Any CPU {A15D6EE6-F954-415B-8605-8A8470CC87DC}.Release|Any CPU.Build.0 = Release|Any CPU - {A15D6EE6-F954-415B-8605-8A8470CC87DC}.v2018|Any CPU.ActiveCfg = Release|Any CPU - {A15D6EE6-F954-415B-8605-8A8470CC87DC}.v2018|Any CPU.Build.0 = Release|Any CPU - {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Legacy|Any CPU.ActiveCfg = Legacy|Any CPU - {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Legacy|Any CPU.Build.0 = Legacy|Any CPU - {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Release|Any CPU.ActiveCfg = v2018|Any CPU - {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Release|Any CPU.Build.0 = v2018|Any CPU - {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.v2018|Any CPU.ActiveCfg = v2018|Any CPU - {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.v2018|Any CPU.Build.0 = v2018|Any CPU + {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Debug|Any CPU.ActiveCfg = Release|Any CPU + {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Debug|Any CPU.Build.0 = Release|Any CPU + {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Release|Any CPU.Build.0 = Release|Any CPU {D0C584C0-81D7-486E-B70E-D7F9256E0909}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D0C584C0-81D7-486E-B70E-D7F9256E0909}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D0C584C0-81D7-486E-B70E-D7F9256E0909}.Legacy|Any CPU.ActiveCfg = Release|Any CPU - {D0C584C0-81D7-486E-B70E-D7F9256E0909}.Legacy|Any CPU.Build.0 = Release|Any CPU {D0C584C0-81D7-486E-B70E-D7F9256E0909}.Release|Any CPU.ActiveCfg = Release|Any CPU {D0C584C0-81D7-486E-B70E-D7F9256E0909}.Release|Any CPU.Build.0 = Release|Any CPU - {D0C584C0-81D7-486E-B70E-D7F9256E0909}.v2018|Any CPU.ActiveCfg = Release|Any CPU - {D0C584C0-81D7-486E-B70E-D7F9256E0909}.v2018|Any CPU.Build.0 = Release|Any CPU {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Legacy|Any CPU.ActiveCfg = Release|Any CPU - {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Legacy|Any CPU.Build.0 = Release|Any CPU {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Release|Any CPU.ActiveCfg = Release|Any CPU {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.Release|Any CPU.Build.0 = Release|Any CPU - {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.v2018|Any CPU.ActiveCfg = Release|Any CPU - {1839CFE2-3DB0-45A8-B03D-9AA797479A3A}.v2018|Any CPU.Build.0 = Release|Any CPU {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Debug|Any CPU.ActiveCfg = Debug|x86 {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Debug|Any CPU.Build.0 = Debug|x86 - {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Legacy|Any CPU.ActiveCfg = Release|x86 - {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Legacy|Any CPU.Build.0 = Release|x86 {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.Release|Any CPU.ActiveCfg = Release|x86 - {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.v2018|Any CPU.ActiveCfg = Release|x86 - {E7CD429A-D057-48E3-8C51-E5C934E8E07B}.v2018|Any CPU.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/BepInEx/BepInEx.csproj b/BepInEx/BepInEx.csproj index fdb963c..0c61db8 100644 --- a/BepInEx/BepInEx.csproj +++ b/BepInEx/BepInEx.csproj @@ -13,7 +13,7 @@ true - + AnyCPU none true @@ -37,7 +37,7 @@ bin\Debug\ - DEBUG;TRACE;UNITY_2018 + TRACE;DEBUG false @@ -70,6 +70,7 @@ + diff --git a/BepInEx/Bootstrap/Chainloader.cs b/BepInEx/Bootstrap/Chainloader.cs index ab3bb79..d877cc1 100644 --- a/BepInEx/Bootstrap/Chainloader.cs +++ b/BepInEx/Bootstrap/Chainloader.cs @@ -8,6 +8,8 @@ using System.Linq; using System.Reflection; using System.Text; using System.Text.RegularExpressions; +using BepInEx.Contract; +using Mono.Cecil; using UnityEngine; using UnityInjector.ConsoleUtil; using Logger = BepInEx.Logging.Logger; @@ -22,7 +24,9 @@ namespace BepInEx.Bootstrap /// /// The loaded and initialized list of plugins. /// - public static List Plugins { get; private set; } = new List(); + public static Dictionary PluginInfos { get; } = new Dictionary(); + + public static List Plugins { get; } = new List(); /// /// The GameObject that all plugins are attached to as components. @@ -34,15 +38,14 @@ namespace BepInEx.Bootstrap private static bool _initialized = false; /// - /// Initializes BepInEx to be able to start the chainloader. - /// - public static void Initialize(string gameExePath, bool startConsole = true) + /// Initializes BepInEx to be able to start the chainloader. + /// + public static void Initialize(string gameExePath, bool startConsole = true) { if (_initialized) return; // Set vitals - if (gameExePath != null) { // Checking for null allows a more advanced initialization workflow, where the Paths class has been initialized before calling Chainloader.Initialize @@ -50,14 +53,12 @@ namespace BepInEx.Bootstrap Paths.SetExecutablePath(gameExePath); } - Paths.SetPluginPath(ConfigPluginsDirectory.Value); - - // Start logging - if (ConsoleWindow.ConfigConsoleEnabled.Value && startConsole) + // Start logging + if (ConsoleWindow.ConfigConsoleEnabled.Value && startConsole) { ConsoleWindow.Attach(); Logger.Listeners.Add(new ConsoleLogListener()); - } + } // Fix for standard output getting overwritten by UnityLogger if (ConsoleWindow.StandardOut != null) @@ -69,7 +70,7 @@ namespace BepInEx.Bootstrap Console.OutputEncoding = ConsoleEncoding.GetEncoding(encoding); } - Logger.Listeners.Add(new UnityLogListener()); + Logger.Listeners.Add(new UnityLogListener()); Logger.Listeners.Add(new DiskLogListener()); if (!TraceLogSource.IsListening) @@ -84,7 +85,71 @@ namespace BepInEx.Bootstrap _initialized = true; } - private static Regex allowedGuidRegex { get; } = new Regex(@"^[a-zA-Z0-9\._]+$"); + private static Regex allowedGuidRegex { get; } = new Regex(@"^[a-zA-Z0-9\._\-]+$"); + + public static PluginInfo ToPluginInfo(TypeDefinition type) + { + if (type.IsInterface || type.IsAbstract || !type.IsSubtypeOf(typeof(BaseUnityPlugin))) + return null; + + var metadata = BepInPlugin.FromCecilType(type); + + if (metadata == null) + { + Logger.LogWarning($"Skipping over type [{type.FullName}] as no metadata attribute is specified"); + return null; + } + + if (string.IsNullOrEmpty(metadata.GUID) || !allowedGuidRegex.IsMatch(metadata.GUID)) + { + Logger.LogWarning($"Skipping type [{type.FullName}] because its GUID [{metadata.GUID}] is of an illegal format."); + return null; + } + + if (metadata.Version == null) + { + Logger.LogWarning($"Skipping type [{type.FullName}] because its version is invalid."); + return null; + } + + if (metadata.Name == null) + { + Logger.LogWarning($"Skipping type [{type.FullName}] because its name is null."); + return null; + } + + //Perform a filter for currently running process + var filters = BepInProcess.FromCecilType(type); + bool invalidProcessName = filters.Count != 0 && filters.All(x => !string.Equals(x.ProcessName.Replace(".exe", ""), Paths.ProcessName, StringComparison.InvariantCultureIgnoreCase)); + + if (invalidProcessName) + { + Logger.LogWarning($"Skipping over plugin [{metadata.GUID}] due to process filter"); + return null; + } + + var dependencies = BepInDependency.FromCecilType(type); + + return new PluginInfo + { + Metadata = metadata, + Processes = filters, + Dependencies = dependencies, + TypeName = type.FullName + }; + } + + private static readonly string CurrentAssemblyName = Assembly.GetExecutingAssembly().GetName().Name; + + private static bool HasBepinPlugins(AssemblyDefinition ass) + { + if (ass.MainModule.AssemblyReferences.All(r => r.Name != CurrentAssemblyName)) + return false; + if (ass.MainModule.GetTypeReferences().All(r => r.FullName != typeof(BaseUnityPlugin).FullName)) + return false; + + return true; + } /// /// The entrypoint for the BepInEx plugin system. @@ -107,8 +172,7 @@ namespace BepInEx.Bootstrap { var productNameProp = typeof(Application).GetProperty("productName", BindingFlags.Public | BindingFlags.Static); if (productNameProp != null) - ConsoleWindow.Title = - $"BepInEx {Assembly.GetExecutingAssembly().GetName().Version} - {productNameProp.GetValue(null, null)}"; + ConsoleWindow.Title = $"BepInEx {Assembly.GetExecutingAssembly().GetName().Version} - {productNameProp.GetValue(null, null)}"; Logger.LogMessage("Chainloader started"); @@ -116,76 +180,34 @@ namespace BepInEx.Bootstrap UnityEngine.Object.DontDestroyOnLoad(ManagerObject); + var pluginsToLoad = TypeLoader.FindPluginTypes(Paths.PluginPath, ToPluginInfo, HasBepinPlugins, "chainloader"); + foreach (var keyValuePair in pluginsToLoad) + foreach (var pluginInfo in keyValuePair.Value) + pluginInfo.Location = keyValuePair.Key; + var pluginInfos = pluginsToLoad.SelectMany(p => p.Value).ToList(); + var loadedAssemblies = new Dictionary(); - string currentProcess = Process.GetCurrentProcess().ProcessName.ToLower(); - - var globalPluginTypes = TypeLoader.LoadTypes(Paths.PluginPath).ToList(); - - Dictionary selectedPluginTypes = new Dictionary(globalPluginTypes.Count); - - foreach (var pluginType in globalPluginTypes) - { - //Ensure metadata exists - var metadata = MetadataHelper.GetMetadata(pluginType); - - if (metadata == null) - { - Logger.LogWarning($"Skipping type [{pluginType.FullName}] as no metadata attribute is specified"); - continue; - } - - if (string.IsNullOrEmpty(metadata.GUID) || !allowedGuidRegex.IsMatch(metadata.GUID)) - { - Logger.LogWarning($"Skipping type [{pluginType.FullName}] because its GUID [{metadata.GUID}] is of an illegal format."); - continue; - } - - if (selectedPluginTypes.Any(x => x.Value.GUID.Equals(metadata.GUID, StringComparison.OrdinalIgnoreCase))) - { - Logger.LogWarning($"Skipping type [{pluginType.FullName}] because its GUID [{metadata.GUID}] is already used by another plugin."); - continue; - } - - if (metadata.Version == null) - { - Logger.LogWarning($"Skipping type [{pluginType.FullName}] because its version is invalid."); - continue; - } - - if (metadata.Name == null) - { - Logger.LogWarning($"Skipping type [{pluginType.FullName}] because its name is null."); - continue; - } - - //Perform a filter for currently running process - var filters = MetadataHelper.GetAttributes(pluginType); - - if (filters.Length != 0) - { - var result = filters.Any(x => x.ProcessName.ToLower().Replace(".exe", "") == currentProcess); - - if (!result) - { - Logger.LogInfo($"Skipping over plugin [{metadata.GUID}] due to process filter"); - continue; - } - } - - selectedPluginTypes.Add(pluginType, metadata); - } - - Logger.LogInfo($"{selectedPluginTypes.Count} / {globalPluginTypes.Count} plugins to load"); + Logger.LogInfo($"{pluginInfos.Count} plugins to load"); var dependencyDict = new Dictionary>(); - var pluginsByGUID = new Dictionary(); + var pluginsByGUID = new Dictionary(); - foreach (var kv in selectedPluginTypes) + foreach (var pluginInfo in pluginInfos) { - var dependencies = MetadataHelper.GetDependencies(kv.Key, selectedPluginTypes.Keys); + if (pluginInfo.Metadata.GUID == null) + { + Logger.LogWarning($"Skipping [{pluginInfo.Metadata.Name}] because it does not have a valid GUID."); + continue; + } - dependencyDict[kv.Value.GUID] = dependencies.Select(d => d.DependencyGUID); - pluginsByGUID[kv.Value.GUID] = kv.Key; + if (dependencyDict.ContainsKey(pluginInfo.Metadata.GUID)) + { + Logger.LogWarning($"Skipping [{pluginInfo.Metadata.Name}] because its GUID ({pluginInfo.Metadata.GUID}) is already used by another plugin."); + continue; + } + + dependencyDict[pluginInfo.Metadata.GUID] = pluginInfo.Dependencies.Select(d => d.DependencyGUID); + pluginsByGUID[pluginInfo.Metadata.GUID] = pluginInfo; } var emptyDependencies = new string[0]; @@ -200,14 +222,12 @@ namespace BepInEx.Bootstrap foreach (var pluginGUID in sortedPlugins) { // If the plugin is missing, don't process it - if (!pluginsByGUID.TryGetValue(pluginGUID, out var pluginType)) + if (!pluginsByGUID.TryGetValue(pluginGUID, out var pluginInfo)) continue; - var metadata = MetadataHelper.GetMetadata(pluginType); - var dependencies = MetadataHelper.GetDependencies(pluginType, selectedPluginTypes.Keys); var dependsOnInvalidPlugin = false; var missingDependencies = new List(); - foreach (var dependency in dependencies) + foreach (var dependency in pluginInfo.Dependencies) { // If the depenency wasn't already processed, it's missing altogether if (!processedPlugins.Contains(dependency.DependencyGUID)) @@ -230,14 +250,14 @@ namespace BepInEx.Bootstrap if (dependsOnInvalidPlugin) { - Logger.LogWarning($"Skipping [{metadata.Name}] because it has a dependency that was not loaded. See above errors for details."); + Logger.LogWarning($"Skipping [{pluginInfo.Metadata.Name}] because it has a dependency that was not loaded. See above errors for details."); continue; } if (missingDependencies.Count != 0) { - Logger.LogError($@"Missing the following dependencies for [{metadata.Name}]: {"\r\n"}{ - string.Join("\r\n", missingDependencies.Select(s => $"- {s}").ToArray()) + Logger.LogError($@"Missing the following dependencies for [{pluginInfo.Metadata.Name}]: {"\r\n"}{ + string.Join("\r\n", missingDependencies.Select(s => $"- {s}").ToArray()) }{"\r\n"}Loading will be skipped; expect further errors and unstabilities."); invalidPlugins.Add(pluginGUID); @@ -246,14 +266,26 @@ namespace BepInEx.Bootstrap try { - Logger.LogInfo($"Loading [{metadata.Name} {metadata.Version}]"); - Plugins.Add((BaseUnityPlugin)ManagerObject.AddComponent(pluginType)); + Logger.LogInfo($"Loading [{pluginInfo.Metadata.Name} {pluginInfo.Metadata.Version}]"); + + if (!loadedAssemblies.TryGetValue(pluginInfo.Location, out var ass)) + loadedAssemblies[pluginInfo.Location] = ass = Assembly.LoadFile(pluginInfo.Location); + + PluginInfos[pluginGUID] = pluginInfo; + pluginInfo.Instance = (BaseUnityPlugin)ManagerObject.AddComponent(ass.GetType(pluginInfo.TypeName)); + + Plugins.Add(pluginInfo.Instance); } catch (Exception ex) { invalidPlugins.Add(pluginGUID); - Logger.LogError($"Error loading [{metadata.Name}] : {ex.Message}"); - Logger.LogDebug(ex); + PluginInfos.Remove(pluginGUID); + + Logger.LogError($"Error loading [{pluginInfo.Metadata.Name}] : {ex.Message}"); + if (ex is ReflectionTypeLoadException re) + Logger.LogDebug(TypeLoader.TypeLoadExceptionToString(re)); + else + Logger.LogDebug(ex); } } } @@ -272,17 +304,9 @@ namespace BepInEx.Bootstrap #region Config - private static readonly ConfigWrapper ConfigPluginsDirectory = ConfigFile.CoreConfig.Wrap( - "Paths", - "PluginsDirectory", - "The relative directory to the BepInEx folder where plugins are loaded.", - "plugins"); + private static readonly ConfigWrapper ConfigPluginsDirectory = ConfigFile.CoreConfig.Wrap("Paths", "PluginsDirectory", "The relative directory to the BepInEx folder where plugins are loaded.", "plugins"); - private static readonly ConfigWrapper ConfigUnityLogging = ConfigFile.CoreConfig.Wrap( - "Logging", - "UnityLogListening", - "Enables showing unity log messages in the BepInEx logging system.", - true); + private static readonly ConfigWrapper ConfigUnityLogging = ConfigFile.CoreConfig.Wrap("Logging", "UnityLogListening", "Enables showing unity log messages in the BepInEx logging system.", true); #endregion } diff --git a/BepInEx/Bootstrap/TypeLoader.cs b/BepInEx/Bootstrap/TypeLoader.cs index 180e037..6bec362 100644 --- a/BepInEx/Bootstrap/TypeLoader.cs +++ b/BepInEx/Bootstrap/TypeLoader.cs @@ -1,56 +1,235 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Reflection; using System.Text; +using BepInEx.Configuration; using BepInEx.Logging; +using Mono.Cecil; namespace BepInEx.Bootstrap { /// - /// Provides methods for loading specified types from an assembly. + /// A cacheable metadata item. Can be used with and to cache plugin metadata. + /// + public interface ICacheable + { + /// + /// Serialize the object into a binary format. + /// + /// + void Save(BinaryWriter bw); + + /// + /// Loads the object from binary format. + /// + /// + void Load(BinaryReader br); + } + + /// + /// A cached assembly. + /// + /// + public class CachedAssembly where T : ICacheable + { + /// + /// List of cached items inside the assembly. + /// + public List CacheItems { get; set; } + + + /// + /// Timestamp of the assembly. Used to check the age of the cache. + /// + public long Timestamp { get; set; } + } + + /// + /// Provides methods for loading specified types from an assembly. /// public static class TypeLoader { + private static readonly DefaultAssemblyResolver resolver; + private static readonly ReaderParameters readerParameters; + + static TypeLoader() + { + resolver = new DefaultAssemblyResolver(); + readerParameters = new ReaderParameters { AssemblyResolver = resolver }; + + resolver.ResolveFailure += (sender, reference) => + { + var name = new AssemblyName(reference.FullName); + + if (Utility.TryResolveDllAssembly(name, Paths.BepInExAssemblyDirectory, readerParameters, out var assembly) || + Utility.TryResolveDllAssembly(name, Paths.PluginPath, readerParameters, out assembly) || + Utility.TryResolveDllAssembly(name, Paths.ManagedPath, readerParameters, out assembly)) + return assembly; + + return AssemblyResolve?.Invoke(sender, reference); + }; + } + + public static event AssemblyResolveEventHandler AssemblyResolve; + /// - /// Loads a list of types from a directory containing assemblies, that derive from a base type. + /// Looks up assemblies in the given directory and locates all types that can be loaded and collects their metadata. /// /// The specific base type to search for. /// The directory to search for assemblies. - /// Returns a list of found derivative types. - public static IEnumerable LoadTypes(string directory) + /// A function to check if a type should be selected and to build the type metadata. + /// A filter function to quickly determine if the assembly can be loaded. + /// The name of the cache to get cached types from. + /// A list of all loadable type metadatas indexed by the full path to the assembly that contains the types. + public static Dictionary> FindPluginTypes(string directory, Func typeSelector, Func assemblyFilter = null, string cacheName = null) where T : ICacheable, new() { - List types = new List(); - Type pluginType = typeof(T); + var result = new Dictionary>(); + Dictionary> cache = null; + + if (cacheName != null) + cache = LoadAssemblyCache(cacheName); foreach (string dll in Directory.GetFiles(Path.GetFullPath(directory), "*.dll", SearchOption.AllDirectories)) - { try { - AssemblyName an = AssemblyName.GetAssemblyName(dll); - Assembly assembly = Assembly.Load(an); - - foreach (Type type in assembly.GetTypes()) + if (cache != null && cache.TryGetValue(dll, out var cacheEntry)) { - if (!type.IsInterface && !type.IsAbstract && pluginType.IsAssignableFrom(type)) - types.Add(type); + long lastWrite = File.GetLastWriteTimeUtc(dll).Ticks; + if (lastWrite == cacheEntry.Timestamp) + { + result[dll] = cacheEntry.CacheItems; + continue; + } } - } - catch (BadImageFormatException) { } //unmanaged DLL - catch (ReflectionTypeLoadException ex) - { - Logger.LogError($"Could not load \"{Path.GetFileName(dll)}\" as a plugin!"); - Logger.LogDebug(TypeLoadExceptionToString(ex)); - } - } - return types; + var ass = AssemblyDefinition.ReadAssembly(dll, readerParameters); + + if (!assemblyFilter?.Invoke(ass) ?? false) + { + ass.Dispose(); + continue; + } + + var matches = ass.MainModule.Types.Select(typeSelector).Where(t => t != null).ToList(); + + if (matches.Count == 0) + { + ass.Dispose(); + continue; + } + + result[dll] = matches; + ass.Dispose(); + } + catch (Exception e) + { + Logger.LogError(e.ToString()); + } + + if (cacheName != null) + SaveAssemblyCache(cacheName, result); + + return result; } - private static string TypeLoadExceptionToString(ReflectionTypeLoadException ex) + /// + /// Loads an index of type metadatas from a cache. + /// + /// Name of the cache + /// Cacheable item + /// Cached type metadatas indexed by the path of the assembly that defines the type. If no cache is defined, return null. + public static Dictionary> LoadAssemblyCache(string cacheName) where T : ICacheable, new() { - StringBuilder sb = new StringBuilder(); - foreach (Exception exSub in ex.LoaderExceptions) + if (!EnableAssemblyCache.Value) + return null; + + var result = new Dictionary>(); + try + { + string path = Path.Combine(Paths.CachePath, $"{cacheName}_typeloader.dat"); + if (!File.Exists(path)) + return null; + + using (var br = new BinaryReader(File.OpenRead(path))) + { + int entriesCount = br.ReadInt32(); + + for (var i = 0; i < entriesCount; i++) + { + string entryIdentifier = br.ReadString(); + long entryDate = br.ReadInt64(); + int itemsCount = br.ReadInt32(); + var items = new List(); + + for (var j = 0; j < itemsCount; j++) + { + var entry = new T(); + entry.Load(br); + items.Add(entry); + } + + result[entryIdentifier] = new CachedAssembly { Timestamp = entryDate, CacheItems = items }; + } + } + } + catch (Exception e) + { + Logger.LogWarning($"Failed to load cache \"{cacheName}\"; skipping loading cache. Reason: {e.Message}."); + } + + return result; + } + + /// + /// Saves indexed type metadata into a cache. + /// + /// Name of the cache + /// List of plugin metadatas indexed by the path to the assembly that contains the types + /// Cacheable item + public static void SaveAssemblyCache(string cacheName, Dictionary> entries) where T : ICacheable + { + if (!EnableAssemblyCache.Value) + return; + + try + { + if (!Directory.Exists(Paths.CachePath)) + Directory.CreateDirectory(Paths.CachePath); + + string path = Path.Combine(Paths.CachePath, $"{cacheName}_typeloader.dat"); + + using (var bw = new BinaryWriter(File.OpenWrite(path))) + { + bw.Write(entries.Count); + + foreach (var kv in entries) + { + bw.Write(kv.Key); + bw.Write(File.GetLastWriteTimeUtc(kv.Key).Ticks); + bw.Write(kv.Value.Count); + + foreach (var item in kv.Value) + item.Save(bw); + } + } + } + catch (Exception e) + { + Logger.LogWarning($"Failed to save cache \"{cacheName}\"; skipping saving cache. Reason: {e.Message}."); + } + } + + /// + /// Converts TypeLoadException to a readable string. + /// + /// TypeLoadException + /// Readable representation of the exception + public static string TypeLoadExceptionToString(ReflectionTypeLoadException ex) + { + var sb = new StringBuilder(); + foreach (var exSub in ex.LoaderExceptions) { sb.AppendLine(exSub.Message); if (exSub is FileNotFoundException exFileNotFound) @@ -75,5 +254,15 @@ namespace BepInEx.Bootstrap return sb.ToString(); } + + #region Config + + private static ConfigWrapper EnableAssemblyCache = ConfigFile.CoreConfig.Wrap( + "Caching", + "EnableAssemblyCache", + "Enable/disable assembly metadata cache\nEnabling this will speed up discovery of plugins and patchers by caching the metadata of all types BepInEx discovers.", + true); + + #endregion } } \ No newline at end of file diff --git a/BepInEx/Configuration/ConfigFile.cs b/BepInEx/Configuration/ConfigFile.cs index 32b2478..6c4f33d 100644 --- a/BepInEx/Configuration/ConfigFile.cs +++ b/BepInEx/Configuration/ConfigFile.cs @@ -54,7 +54,7 @@ namespace BepInEx.Configuration /// The plugin that owns this setting. public ConfigFile(string configPath, bool saveOnInit, BaseUnityPlugin owner = null) { - _ownerMetadata = owner?.Metadata; + _ownerMetadata = owner?.Info.Metadata; if (configPath == null) throw new ArgumentNullException(nameof(configPath)); configPath = Path.GetFullPath(configPath); diff --git a/BepInEx/Configuration/TomlTypeConverter.cs b/BepInEx/Configuration/TomlTypeConverter.cs index 2376653..9f41b06 100644 --- a/BepInEx/Configuration/TomlTypeConverter.cs +++ b/BepInEx/Configuration/TomlTypeConverter.cs @@ -150,4 +150,4 @@ namespace BepInEx.Configuration return TypeConverters.Keys; } } -} +} \ No newline at end of file diff --git a/BepInEx/ConsoleUtil/ConsoleWindow.cs b/BepInEx/ConsoleUtil/ConsoleWindow.cs index 87682bf..9c9e9b6 100644 --- a/BepInEx/ConsoleUtil/ConsoleWindow.cs +++ b/BepInEx/ConsoleUtil/ConsoleWindow.cs @@ -25,7 +25,7 @@ namespace UnityInjector.ConsoleUtil "If true, console is set to the Shift-JIS encoding, otherwise UTF-8 encoding.", false); - public static bool IsAttached { get; private set; } + public static bool IsAttached { get; private set; } private static IntPtr _cOut; private static IntPtr _oOut; diff --git a/BepInEx/Contract/Attributes.cs b/BepInEx/Contract/Attributes.cs index 3e2eddf..eff3e92 100644 --- a/BepInEx/Contract/Attributes.cs +++ b/BepInEx/Contract/Attributes.cs @@ -1,6 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; +using BepInEx.Logging; +using Mono.Cecil; +using Mono.Collections.Generic; namespace BepInEx { @@ -46,6 +50,16 @@ namespace BepInEx Version = null; } } + + internal static BepInPlugin FromCecilType(TypeDefinition td) + { + var attr = MetadataHelper.GetCustomAttributes(td, false).FirstOrDefault(); + + if (attr == null) + return null; + + return new BepInPlugin((string)attr.ConstructorArguments[0].Value, (string)attr.ConstructorArguments[1].Value, (string)attr.ConstructorArguments[2].Value); + } } /// @@ -84,6 +98,12 @@ namespace BepInEx this.DependencyGUID = DependencyGUID; this.Flags = Flags; } + + internal static IEnumerable FromCecilType(TypeDefinition td) + { + var attrs = MetadataHelper.GetCustomAttributes(td, true); + return attrs.Select(customAttribute => new BepInDependency((string)customAttribute.ConstructorArguments[0].Value, (DependencyFlags)customAttribute.ConstructorArguments[1].Value)).ToList(); + } } /// @@ -102,6 +122,12 @@ namespace BepInEx { this.ProcessName = ProcessName; } + + internal static List FromCecilType(TypeDefinition td) + { + var attrs = MetadataHelper.GetCustomAttributes(td, true); + return attrs.Select(customAttribute => new BepInProcess((string)customAttribute.ConstructorArguments[0].Value)).ToList(); + } } #endregion @@ -113,6 +139,22 @@ namespace BepInEx /// public static class MetadataHelper { + internal static IEnumerable GetCustomAttributes(TypeDefinition td, bool inherit) where T : Attribute + { + var result = new List(); + var type = typeof(T); + var currentType = td; + + do + { + result.AddRange(currentType.CustomAttributes.Where(ca => ca.AttributeType.FullName == type.FullName)); + currentType = currentType.BaseType?.Resolve(); + } while (inherit && currentType?.FullName != "System.Object"); + + + return result; + } + /// /// Retrieves the BepInPlugin metadata from a plugin type. /// @@ -159,10 +201,9 @@ namespace BepInEx /// /// Retrieves the dependencies of the specified plugin type. /// - /// The plugin type. - /// All currently loaded plugin types. + /// The plugin type. /// A list of all plugin types that the specified plugin type depends upon. - public static IEnumerable GetDependencies(Type plugin, IEnumerable allPlugins) + public static IEnumerable GetDependencies(Type plugin) { return plugin.GetCustomAttributes(typeof(BepInDependency), true).Cast(); } diff --git a/BepInEx/Contract/BaseUnityPlugin.cs b/BepInEx/Contract/BaseUnityPlugin.cs index 7781a46..7a74859 100644 --- a/BepInEx/Contract/BaseUnityPlugin.cs +++ b/BepInEx/Contract/BaseUnityPlugin.cs @@ -1,4 +1,6 @@ -using BepInEx.Configuration; +using BepInEx.Bootstrap; +using BepInEx.Configuration; +using BepInEx.Contract; using BepInEx.Logging; using UnityEngine; @@ -12,8 +14,7 @@ namespace BepInEx /// /// Information about this plugin as it was loaded. /// - public BepInPlugin Metadata { get; } - + public PluginInfo Info { get; } /// /// Logger instance tied to this plugin. /// @@ -30,11 +31,25 @@ namespace BepInEx /// protected BaseUnityPlugin() { - Metadata = MetadataHelper.GetMetadata(this); + var metadata = MetadataHelper.GetMetadata(this); - Logger = Logging.Logger.CreateLogSource(Metadata.Name); + if (Chainloader.PluginInfos.TryGetValue(metadata.GUID, out var info)) + Info = info; + else + { + Info = new PluginInfo + { + Metadata = metadata, + Instance = this, + Dependencies = MetadataHelper.GetDependencies(GetType()), + Processes = MetadataHelper.GetAttributes(GetType()), + Location = GetType().Assembly.Location + }; + } - Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, Metadata.GUID + ".cfg"), false, this); + Logger = Logging.Logger.CreateLogSource(metadata.Name); + + Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, metadata.GUID + ".cfg"), false, this); } } } \ No newline at end of file diff --git a/BepInEx/Contract/PluginInfo.cs b/BepInEx/Contract/PluginInfo.cs new file mode 100644 index 0000000..5cf8506 --- /dev/null +++ b/BepInEx/Contract/PluginInfo.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using BepInEx.Bootstrap; + +namespace BepInEx.Contract +{ + public class PluginInfo : ICacheable + { + public BepInPlugin Metadata { get; internal set; } + + public IEnumerable Processes { get; internal set; } + + public IEnumerable Dependencies { get; internal set; } + + public string Location { get; internal set; } + + public BaseUnityPlugin Instance { get; internal set; } + + internal string TypeName { get; set; } + + public void Save(BinaryWriter bw) + { + bw.Write(TypeName); + + bw.Write(Metadata.GUID); + bw.Write(Metadata.Name); + bw.Write(Metadata.Version.ToString()); + + var processList = Processes.ToList(); + bw.Write(processList.Count); + foreach (var bepInProcess in processList) + bw.Write(bepInProcess.ProcessName); + + var depList = Dependencies.ToList(); + bw.Write(depList.Count); + foreach (var bepInDependency in depList) + { + bw.Write(bepInDependency.DependencyGUID); + bw.Write((int)bepInDependency.Flags); + } + } + + public void Load(BinaryReader br) + { + TypeName = br.ReadString(); + + Metadata = new BepInPlugin(br.ReadString(), br.ReadString(), br.ReadString()); + + var processListCount = br.ReadInt32(); + var processList = new List(processListCount); + for (int i = 0; i < processListCount; i++) + processList.Add(new BepInProcess(br.ReadString())); + Processes = processList; + + var depCount = br.ReadInt32(); + var depList = new List(depCount); + for (int i = 0; i < depCount; i++) + depList.Add(new BepInDependency(br.ReadString(), (BepInDependency.DependencyFlags) br.ReadInt32())); + Dependencies = depList; + } + } +} \ No newline at end of file diff --git a/BepInEx/Logging/ConsoleLogListener.cs b/BepInEx/Logging/ConsoleLogListener.cs index 86aee3f..ee9d9c2 100644 --- a/BepInEx/Logging/ConsoleLogListener.cs +++ b/BepInEx/Logging/ConsoleLogListener.cs @@ -16,7 +16,7 @@ namespace BepInEx.Logging if (eventArgs.Level.GetHighestLevel() > DisplayedLogLevel) return; - string log = $"[{eventArgs.Level, -7}:{((ILogSource)sender).SourceName, 10}] {eventArgs.Data}\r\n"; + string log = $"[{eventArgs.Level,-7}:{((ILogSource)sender).SourceName,10}] {eventArgs.Data}\r\n"; Kon.ForegroundColor = eventArgs.Level.GetConsoleColor(); Console.Write(log); diff --git a/BepInEx/Logging/DiskLogListener.cs b/BepInEx/Logging/DiskLogListener.cs index 65968a2..e2b94cd 100644 --- a/BepInEx/Logging/DiskLogListener.cs +++ b/BepInEx/Logging/DiskLogListener.cs @@ -39,12 +39,9 @@ namespace BepInEx.Logging } LogWriter = TextWriter.Synchronized(new StreamWriter(fileStream, Encoding.UTF8)); - - FlushTimer = new Timer(o => - { - LogWriter?.Flush(); - }, null, 2000, 2000); + + FlushTimer = new Timer(o => { LogWriter?.Flush(); }, null, 2000, 2000); } public void LogEvent(object sender, LogEventArgs eventArgs) diff --git a/BepInEx/Logging/TraceLogSource.cs b/BepInEx/Logging/TraceLogSource.cs index 1d03e91..571b8a8 100644 --- a/BepInEx/Logging/TraceLogSource.cs +++ b/BepInEx/Logging/TraceLogSource.cs @@ -76,7 +76,7 @@ namespace BepInEx.Logging level = LogLevel.Debug; break; } - + LogSource.Log(level, $"{message}".Trim()); } } diff --git a/BepInEx/Logging/UnityLogListener.cs b/BepInEx/Logging/UnityLogListener.cs index 0cc4adc..bcf802e 100644 --- a/BepInEx/Logging/UnityLogListener.cs +++ b/BepInEx/Logging/UnityLogListener.cs @@ -27,7 +27,7 @@ namespace BepInEx.Logging WriteStringToUnityLog = (Action)Delegate.CreateDelegate(typeof(Action), methodInfo); break; } - + if (WriteStringToUnityLog == null) Logger.LogError("Unable to start Unity log writer"); } diff --git a/BepInEx/Logging/UnityLogSource.cs b/BepInEx/Logging/UnityLogSource.cs index 407a07d..fe04cf3 100644 --- a/BepInEx/Logging/UnityLogSource.cs +++ b/BepInEx/Logging/UnityLogSource.cs @@ -24,6 +24,7 @@ namespace BepInEx.Logging } private bool disposed = false; + public void Dispose() { if (!disposed) diff --git a/BepInEx/Paths.cs b/BepInEx/Paths.cs index 227d61f..b53616c 100644 --- a/BepInEx/Paths.cs +++ b/BepInEx/Paths.cs @@ -21,6 +21,14 @@ namespace BepInEx PatcherPluginPath = Path.Combine(BepInExRootPath, "patchers"); BepInExAssemblyDirectory = Path.Combine(BepInExRootPath, "core"); BepInExAssemblyPath = Path.Combine(BepInExAssemblyDirectory, $"{Assembly.GetExecutingAssembly().GetName().Name}.dll"); + CachePath = Path.Combine(BepInExRootPath, "cache"); + } + + internal static void SetManagedPath(string managedPath) + { + if (managedPath == null) + return; + ManagedPath = managedPath; } internal static void SetPluginPath(string pluginPath) @@ -68,6 +76,11 @@ namespace BepInEx /// public static string BepInExConfigPath { get; private set; } + /// + /// The path to temporary cache files. + /// + public static string CachePath { get; private set; } + /// /// The path to the patcher plugin folder which resides in the BepInEx folder. /// diff --git a/BepInEx/Utility.cs b/BepInEx/Utility.cs index a88e0cd..67eaa1c 100644 --- a/BepInEx/Utility.cs +++ b/BepInEx/Utility.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +using System.Reflection.Emit; +using Mono.Cecil; namespace BepInEx { @@ -12,11 +14,50 @@ namespace BepInEx public static class Utility { /// - /// Combines multiple paths together, as the specific method is not available in .NET 3.5. + /// Whether current Common Language Runtime supports dynamic method generation using namespace. /// - /// The multiple paths to combine together. - /// A combined path. - public static string CombinePaths(params string[] parts) => parts.Aggregate(Path.Combine); + public static bool CLRSupportsDynamicAssemblies { get; } + + static Utility() + { + try + { + var m = new DynamicMethod("SRE_Test", null, null); + CLRSupportsDynamicAssemblies = true; + } + catch (PlatformNotSupportedException) + { + CLRSupportsDynamicAssemblies = false; + } + } + + /// + /// Try to perform an action. + /// + /// Action to perform. + /// Possible exception that gets returned. + /// True, if action succeeded, false if an exception occured. + public static bool TryDo(Action action, out Exception exception) + { + exception = null; + try + { + action(); + return true; + } + catch (Exception e) + { + exception = e; + return false; + } + } + + /// + /// Combines multiple paths together, as the specific method is not available in .NET 3.5. + /// + /// The multiple paths to combine together. + /// A combined path. + public static string CombinePaths(params string[] parts) => parts.Aggregate(Path.Combine); /// /// Tries to parse a bool, with a default value if unable to parse. @@ -26,7 +67,7 @@ namespace BepInEx /// Boolean value of input if able to be parsed, otherwise default value. public static bool SafeParseBool(string input, bool defaultValue = false) { - return bool.TryParse(input, out bool result) ? result : defaultValue; + return Boolean.TryParse(input, out bool result) ? result : defaultValue; } /// @@ -46,7 +87,7 @@ namespace BepInEx /// True if the value parameter is null or empty, or if value consists exclusively of white-space characters. public static bool IsNullOrWhiteSpace(this string self) { - return self == null || self.All(char.IsWhiteSpace); + return self == null || self.All(Char.IsWhiteSpace); } public static IEnumerable TopologicalSort(IEnumerable nodes, Func> dependencySelector) @@ -61,9 +102,8 @@ namespace BepInEx Stack currentStack = new Stack(); if (!Visit(input, currentStack)) { - throw new Exception("Cyclic Dependency:\r\n" + currentStack - .Select(x => $" - {x}") //append dashes - .Aggregate((a, b) => $"{a}\r\n{b}")); //add new lines inbetween + throw new Exception("Cyclic Dependency:\r\n" + currentStack.Select(x => $" - {x}") //append dashes + .Aggregate((a, b) => $"{a}\r\n{b}")); //add new lines inbetween } } @@ -107,7 +147,7 @@ namespace BepInEx /// Directory to search the assembly from. /// The loaded assembly. /// True, if the assembly was found and loaded. Otherwise, false. - public static bool TryResolveDllAssembly(AssemblyName assemblyName, string directory, out Assembly assembly) + private static bool TryResolveDllAssembly(AssemblyName assemblyName, string directory, Func loader, out T assembly) where T : class { assembly = null; @@ -124,7 +164,7 @@ namespace BepInEx try { - assembly = Assembly.LoadFile(path); + assembly = loader(path); } catch (Exception) { @@ -137,7 +177,47 @@ namespace BepInEx return false; } - public static bool TryOpenFileStream(string path, FileMode mode, out FileStream fileStream, FileAccess access = FileAccess.ReadWrite, FileShare share = FileShare.None) + public static bool IsSubtypeOf(this TypeDefinition self, Type td) + { + if (self.FullName == td.FullName) + return true; + return self.FullName != "System.Object" && (self.BaseType?.Resolve()?.IsSubtypeOf(td) ?? false); + } + + /// + /// Try to resolve and load the given assembly DLL. + /// + /// Name of the assembly, of the type . + /// Directory to search the assembly from. + /// The loaded assembly. + /// True, if the assembly was found and loaded. Otherwise, false. + public static bool TryResolveDllAssembly(AssemblyName assemblyName, string directory, out Assembly assembly) + { + return TryResolveDllAssembly(assemblyName, directory, Assembly.LoadFile, out assembly); + } + + /// + /// Try to resolve and load the given assembly DLL. + /// + /// Name of the assembly, of the type . + /// Directory to search the assembly from. + /// The loaded assembly. + /// True, if the assembly was found and loaded. Otherwise, false. + public static bool TryResolveDllAssembly(AssemblyName assemblyName, string directory, ReaderParameters readerParameters, out AssemblyDefinition assembly) + { + return TryResolveDllAssembly(assemblyName, directory, s => AssemblyDefinition.ReadAssembly(s, readerParameters), out assembly); + } + + /// + /// Tries to create a file with the given name + /// + /// Path of the file to create + /// File open mode + /// Resulting filestream + /// File access options + /// File share options + /// + public static bool TryOpenFileStream(string path, FileMode mode, out FileStream fileStream, FileAccess access = FileAccess.ReadWrite, FileShare share = FileShare.Read) { try { diff --git a/BepInEx/packages.config b/BepInEx/packages.config index 020bed6..2fbe0b1 100644 --- a/BepInEx/packages.config +++ b/BepInEx/packages.config @@ -1,4 +1,5 @@  + \ No newline at end of file diff --git a/README.md b/README.md index 8fa2d00..0661f24 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,9 @@ Unity plugin framework **[User and developer guides](https://github.com/BepInEx/BepInEx/wiki)** ## Used libraries -- [NeighTools/UnityDoorstop](https://github.com/NeighTools/UnityDoorstop) - 2.7.1.0 ([df7d636](https://github.com/NeighTools/UnityDoorstop/commit/df7d6366d8dc69f024c61cd31e6f690eb44ce57a)) +- [NeighTools/UnityDoorstop](https://github.com/NeighTools/UnityDoorstop) - 2.11.0.0 ([68a4e2d](https://github.com/NeighTools/UnityDoorstop/commit/68a4e2db1f09e5f5cc2f479d293b09764d09c80b)) - [pardeike/Harmony](https://github.com/pardeike/Harmony) - pre-2.0 ([443f551](https://github.com/pardeike/Harmony/commit/443f551ec45ecf409755b5979a4466343197de03)) -- [0x0ade/MonoMod](https://github.com/0x0ade/MonoMod) - v19.05.01.01 ([934a8ae](https://github.com/0x0ade/MonoMod/commit/934a8ae921affac0093757d23c6f3ead34e996ac)) +- [0x0ade/MonoMod](https://github.com/0x0ade/MonoMod) - v19.08.02.03 ([a70072c](https://github.com/MonoMod/MonoMod/tree/a70072cdf759ac0cfa80991fcd2cca67d3eec130)) - [jbevain/cecil](https://github.com/jbevain/cecil) - 0.10.3 ([fb289a7](https://github.com/jbevain/cecil/commit/fb289a7cd80ceb6af5c86e7c7ecce9bf1e98b8fe)) ## Credits diff --git a/scripts/jenkins_master.groovy b/scripts/jenkins_master.groovy index dd6b896..4d19411 100644 --- a/scripts/jenkins_master.groovy +++ b/scripts/jenkins_master.groovy @@ -46,12 +46,12 @@ Changes since ${latestTag}: } dir('Doorstop') { - sh ''' tag="v2.7.1.0"; - version="2.7.1.0"; + sh ''' tag="v2.11.0.0"; + version="2.11.0.0"; wget https://github.com/NeighTools/UnityDoorstop/releases/download/$tag/Doorstop_x64_$version.zip; wget https://github.com/NeighTools/UnityDoorstop/releases/download/$tag/Doorstop_x86_$version.zip; - unzip -o Doorstop_x86_$version.zip winhttp.dll -d x86; - unzip -o Doorstop_x64_$version.zip winhttp.dll -d x64;''' + unzip -o Doorstop_x86_$version.zip version.dll -d x86; + unzip -o Doorstop_x64_$version.zip version.dll -d x64;''' } } } @@ -120,7 +120,7 @@ Changes since ${latestTag}: sh 'rm -rf BepInEx/core/patcher' sh 'cp -f ../../../BepInEx/doorstop/doorstop_config.ini doorstop_config.ini' - sh 'cp -f ../../../Doorstop/x86/winhttp.dll winhttp.dll' + sh 'cp -f ../../../Doorstop/x86/version.dll version.dll' script { if(params.IS_BE) { @@ -134,7 +134,7 @@ Changes since ${latestTag}: sh "zip -r9 BepInEx_Legacy_x86${commitPrefix}${versionNumber}.zip ./*" - sh 'cp -f ../../../Doorstop/x64/winhttp.dll winhttp.dll' + sh 'cp -f ../../../Doorstop/x64/version.dll version.dll' sh 'unix2dos doorstop_config.ini' @@ -167,7 +167,7 @@ Changes since ${latestTag}: sh 'rm -f BepInEx/core/UnityEngine.CoreModule.dll' sh 'cp -f ../../../BepInEx/doorstop/doorstop_config.ini doorstop_config.ini' - sh 'cp -f ../../../Doorstop/x86/winhttp.dll winhttp.dll' + sh 'cp -f ../../../Doorstop/x86/version.dll version.dll' script { if(params.IS_BE) { @@ -181,7 +181,7 @@ Changes since ${latestTag}: sh "zip -r9 BepInEx_v2018_x86${commitPrefix}${versionNumber}.zip ./*" - sh 'cp -f ../../../Doorstop/x64/winhttp.dll winhttp.dll' + sh 'cp -f ../../../Doorstop/x64/version.dll version.dll' sh 'unix2dos doorstop_config.ini' diff --git a/submodules/MonoMod b/submodules/MonoMod index 934a8ae..a70072c 160000 --- a/submodules/MonoMod +++ b/submodules/MonoMod @@ -1 +1 @@ -Subproject commit 934a8ae921affac0093757d23c6f3ead34e996ac +Subproject commit a70072cdf759ac0cfa80991fcd2cca67d3eec130