From a23612f4fe59b309d6c8d65a7e7d3b1d9041c1b8 Mon Sep 17 00:00:00 2001 From: Kasuromi <29818914+Kasuromi@users.noreply.github.com> Date: Thu, 16 May 2024 23:29:50 +0300 Subject: [PATCH 01/65] Bump Cpp2IL & Il2CppInterop (#869) --- .../BepInEx.Unity.Common/BepInEx.Unity.Common.csproj | 2 +- Runtimes/Unity/BepInEx.Unity.Common/UnityInfo.cs | 4 ++-- .../BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj | 12 ++++++------ .../BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs | 2 +- .../EntrypointPatcher.cs | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Runtimes/Unity/BepInEx.Unity.Common/BepInEx.Unity.Common.csproj b/Runtimes/Unity/BepInEx.Unity.Common/BepInEx.Unity.Common.csproj index 1abdd26..77791ba 100644 --- a/Runtimes/Unity/BepInEx.Unity.Common/BepInEx.Unity.Common.csproj +++ b/Runtimes/Unity/BepInEx.Unity.Common/BepInEx.Unity.Common.csproj @@ -6,7 +6,7 @@ - + diff --git a/Runtimes/Unity/BepInEx.Unity.Common/UnityInfo.cs b/Runtimes/Unity/BepInEx.Unity.Common/UnityInfo.cs index ae1ad3d..663a736 100644 --- a/Runtimes/Unity/BepInEx.Unity.Common/UnityInfo.cs +++ b/Runtimes/Unity/BepInEx.Unity.Common/UnityInfo.cs @@ -1,9 +1,9 @@ -using System; +using System; using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; using System.Text; -using AssetRipper.VersionUtilities; +using AssetRipper.Primitives; using MonoMod.Utils; [assembly: InternalsVisibleTo("BepInEx.Unity.Mono.Preloader")] diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj index 79a5ee4..d902167 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj @@ -1,4 +1,4 @@ - + BepInEx support library for Il2Cpp games net6.0 @@ -14,15 +14,15 @@ - - - + + + - + - + diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs b/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs index 063f951..cc43fac 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs @@ -316,7 +316,7 @@ internal static partial class Il2CppInteropManager cpp2IlProcessingLayer.Process(Cpp2IlApi.CurrentAppContext); } - var assemblies = new AsmResolverDummyDllOutputFormat().BuildAssemblies(Cpp2IlApi.CurrentAppContext); + var assemblies = new AsmResolverDllOutputFormatDefault().BuildAssemblies(Cpp2IlApi.CurrentAppContext); LibCpp2IlMain.Reset(); Cpp2IlApi.CurrentAppContext = null; diff --git a/Runtimes/Unity/BepInEx.Unity.Mono.Preloader/EntrypointPatcher.cs b/Runtimes/Unity/BepInEx.Unity.Mono.Preloader/EntrypointPatcher.cs index bfb52f0..fb2f834 100644 --- a/Runtimes/Unity/BepInEx.Unity.Mono.Preloader/EntrypointPatcher.cs +++ b/Runtimes/Unity/BepInEx.Unity.Mono.Preloader/EntrypointPatcher.cs @@ -31,9 +31,9 @@ internal class EntrypointPatcher : BasePatcher "The name of the method in the specified entrypoint assembly and type to hook and load Chainloader from."); private static string DefaultEntrypointAssembly => - UnityInfo.Version.IsLess(2017) ? "UnityEngine.dll" : "UnityEngine.CoreModule.dll"; + UnityInfo.Version.LessThan(2017) ? "UnityEngine.dll" : "UnityEngine.CoreModule.dll"; - private static string DefaultEntrypointType => UnityInfo.Version.IsLess(5) ? "MonoBehaviour" : "Application"; + private static string DefaultEntrypointType => UnityInfo.Version.LessThan(5) ? "MonoBehaviour" : "Application"; private bool HasLoaded { get; set; } From 36d130f00db2022b52177a0ffeb25b79cd3d6c7a Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Thu, 16 May 2024 16:42:10 -0400 Subject: [PATCH 02/65] Upgrade doorstop to version 4.2.0 (#865) --- README.md | 2 +- Runtimes/Unity/Doorstop/doorstop_config_mono.ini | 3 ++- Runtimes/Unity/Doorstop/run_bepinex_mono.sh | 3 ++- build/Program.cs | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 89637a7..eff40f7 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ A more comprehensive comparison list of features and compatibility is available ## Used libraries -- [NeighTools/UnityDoorstop](https://github.com/NeighTools/UnityDoorstop) - v4.0.0 +- [NeighTools/UnityDoorstop](https://github.com/NeighTools/UnityDoorstop) - v4.2.0 - [BepInEx/HarmonyX](https://github.com/BepInEx/HarmonyX) - v2.10.1 - [0x0ade/MonoMod](https://github.com/0x0ade/MonoMod) - v22.5.1.1 - [jbevain/cecil](https://github.com/jbevain/cecil) - v0.10.4 diff --git a/Runtimes/Unity/Doorstop/doorstop_config_mono.ini b/Runtimes/Unity/Doorstop/doorstop_config_mono.ini index 743f430..7926ef0 100644 --- a/Runtimes/Unity/Doorstop/doorstop_config_mono.ini +++ b/Runtimes/Unity/Doorstop/doorstop_config_mono.ini @@ -23,7 +23,8 @@ ignore_disable_switch = false # (e.g. mscorlib is stripped in original game) # This option causes Mono to seek mscorlib and core libraries from a different folder before Managed # Original Managed folder is added as a secondary folder in the search path -dll_search_path_override = +# To specify multiple paths, separate them with semicolons (;) +dll_search_path_override = "BepInEx\core" # If true, Mono debugger server will be enabled debug_enabled = false diff --git a/Runtimes/Unity/Doorstop/run_bepinex_mono.sh b/Runtimes/Unity/Doorstop/run_bepinex_mono.sh index 0163209..ed5496a 100644 --- a/Runtimes/Unity/Doorstop/run_bepinex_mono.sh +++ b/Runtimes/Unity/Doorstop/run_bepinex_mono.sh @@ -36,7 +36,8 @@ ignore_disable_switch="0" # (e.g. mscorlib is stripped in original game) # This option causes Mono to seek mscorlib and core libraries from a different folder before Managed # Original Managed folder is added as a secondary folder in the search path -dll_search_path_override="" +# To specify multiple paths, separate them with colons (:) +dll_search_path_override="BepInEx/core" # If 1, Mono debugger server will be enabled debug_enable="0" diff --git a/build/Program.cs b/build/Program.cs index 1189597..e3012a8 100644 --- a/build/Program.cs +++ b/build/Program.cs @@ -32,7 +32,7 @@ public class BuildContext : FrostingContext BleedingEdge } - public const string DoorstopVersion = "4.0.0"; + public const string DoorstopVersion = "4.2.0"; public const string DotnetRuntimeVersion = "6.0.7"; public const string DobbyVersion = "1.0.5"; From 3ba398f269961fe6721f918423df399d0b481d7e Mon Sep 17 00:00:00 2001 From: deca <62450933+decaprime@users.noreply.github.com> Date: Tue, 21 May 2024 05:29:34 -0700 Subject: [PATCH 03/65] Upgrade AssetRipper.Primitives to avoid implying Json 8.0.1 (#873) --- Runtimes/Unity/BepInEx.Unity.Common/BepInEx.Unity.Common.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Runtimes/Unity/BepInEx.Unity.Common/BepInEx.Unity.Common.csproj b/Runtimes/Unity/BepInEx.Unity.Common/BepInEx.Unity.Common.csproj index 77791ba..2ff6c0c 100644 --- a/Runtimes/Unity/BepInEx.Unity.Common/BepInEx.Unity.Common.csproj +++ b/Runtimes/Unity/BepInEx.Unity.Common/BepInEx.Unity.Common.csproj @@ -6,7 +6,7 @@ - + From 851521cc126e4f9d841d2d9bfe857558f0395939 Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Tue, 11 Jun 2024 12:00:31 -0400 Subject: [PATCH 04/65] Upgrade Doorstop to 4.3.0 (#901) --- README.md | 2 +- Runtimes/Unity/Doorstop/doorstop_config_il2cpp.ini | 3 +++ Runtimes/Unity/Doorstop/doorstop_config_mono.ini | 3 +++ Runtimes/Unity/Doorstop/run_bepinex_il2cpp.sh | 7 +++++++ Runtimes/Unity/Doorstop/run_bepinex_mono.sh | 7 +++++++ build/Program.cs | 2 +- 6 files changed, 22 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index eff40f7..8e6dbe9 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ A more comprehensive comparison list of features and compatibility is available ## Used libraries -- [NeighTools/UnityDoorstop](https://github.com/NeighTools/UnityDoorstop) - v4.2.0 +- [NeighTools/UnityDoorstop](https://github.com/NeighTools/UnityDoorstop) - v4.3.0 - [BepInEx/HarmonyX](https://github.com/BepInEx/HarmonyX) - v2.10.1 - [0x0ade/MonoMod](https://github.com/0x0ade/MonoMod) - v22.5.1.1 - [jbevain/cecil](https://github.com/jbevain/cecil) - v0.10.4 diff --git a/Runtimes/Unity/Doorstop/doorstop_config_il2cpp.ini b/Runtimes/Unity/Doorstop/doorstop_config_il2cpp.ini index ed1f29a..8af1026 100644 --- a/Runtimes/Unity/Doorstop/doorstop_config_il2cpp.ini +++ b/Runtimes/Unity/Doorstop/doorstop_config_il2cpp.ini @@ -11,6 +11,9 @@ target_assembly = BepInEx\core\BepInEx.Unity.IL2CPP.dll # If true, Unity's output log is redirected to \output_log.txt redirect_output_log = false +# Overrides the default boot.config file path +boot_config_override = + # If enabled, DOORSTOP_DISABLE env var value is ignored # USE THIS ONLY WHEN ASKED TO OR YOU KNOW WHAT THIS MEANS ignore_disable_switch = false diff --git a/Runtimes/Unity/Doorstop/doorstop_config_mono.ini b/Runtimes/Unity/Doorstop/doorstop_config_mono.ini index 7926ef0..342d479 100644 --- a/Runtimes/Unity/Doorstop/doorstop_config_mono.ini +++ b/Runtimes/Unity/Doorstop/doorstop_config_mono.ini @@ -11,6 +11,9 @@ target_assembly = BepInEx\core\BepInEx.Unity.Mono.Preloader.dll # If true, Unity's output log is redirected to \output_log.txt redirect_output_log = false +# Overrides the default boot.config file path +boot_config_override = + # If enabled, DOORSTOP_DISABLE env var value is ignored # USE THIS ONLY WHEN ASKED TO OR YOU KNOW WHAT THIS MEANS ignore_disable_switch = false diff --git a/Runtimes/Unity/Doorstop/run_bepinex_il2cpp.sh b/Runtimes/Unity/Doorstop/run_bepinex_il2cpp.sh index d751dcc..e947ee4 100644 --- a/Runtimes/Unity/Doorstop/run_bepinex_il2cpp.sh +++ b/Runtimes/Unity/Doorstop/run_bepinex_il2cpp.sh @@ -25,6 +25,9 @@ enabled="1" # NOTE: The entrypoint must be of format `static void Doorstop.Entrypoint.Start()` target_assembly="BepInEx/core/BepInEx.Unity.IL2CPP.dll" +# Overrides the default boot.config file path +boot_config_override= + # If enabled, DOORSTOP_DISABLE env var value is ignored # USE THIS ONLY WHEN ASKED TO OR YOU KNOW WHAT THIS MEANS ignore_disable_switch="0" @@ -218,6 +221,10 @@ while :; do target_assembly="$2" shift ;; + --doorstop-boot-config-override) + boot_config_override="$2" + shift + ;; --doorstop-mono-dll-search-path-override) dll_search_path_override="$2" shift diff --git a/Runtimes/Unity/Doorstop/run_bepinex_mono.sh b/Runtimes/Unity/Doorstop/run_bepinex_mono.sh index ed5496a..aaa7496 100644 --- a/Runtimes/Unity/Doorstop/run_bepinex_mono.sh +++ b/Runtimes/Unity/Doorstop/run_bepinex_mono.sh @@ -25,6 +25,9 @@ enabled="1" # NOTE: The entrypoint must be of format `static void Doorstop.Entrypoint.Start()` target_assembly="BepInEx/core/BepInEx.Unity.Mono.Preloader.dll" +# Overrides the default boot.config file path +boot_config_override= + # If enabled, DOORSTOP_DISABLE env var value is ignored # USE THIS ONLY WHEN ASKED TO OR YOU KNOW WHAT THIS MEANS ignore_disable_switch="0" @@ -219,6 +222,10 @@ while :; do target_assembly="$2" shift ;; + --doorstop-boot-config-override) + boot_config_override="$2" + shift + ;; --doorstop-mono-dll-search-path-override) dll_search_path_override="$2" shift diff --git a/build/Program.cs b/build/Program.cs index e3012a8..0ba5f0a 100644 --- a/build/Program.cs +++ b/build/Program.cs @@ -32,7 +32,7 @@ public class BuildContext : FrostingContext BleedingEdge } - public const string DoorstopVersion = "4.2.0"; + public const string DoorstopVersion = "4.3.0"; public const string DotnetRuntimeVersion = "6.0.7"; public const string DobbyVersion = "1.0.5"; From 53625800b86f6c68751445248260edf0b27a71c2 Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Mon, 12 Aug 2024 15:54:02 -0400 Subject: [PATCH 05/65] Upgrade Harmonyx, Iced and AssetRipper.Primitives packages (#953) * Upgrade Harmonyx to 2.10.2 * Upgrade Iced to 1.21.0 * Upgrade AssetRipper.Primitives to 3.1.3 --- BepInEx.Core/BepInEx.Core.csproj | 2 +- BepInEx.Preloader.Core/BepInEx.Preloader.Core.csproj | 2 +- README.md | 2 +- Runtimes/NET/BepInEx.NET.Common/BepInEx.NET.Common.csproj | 2 +- .../BepInEx.NET.Framework.Launcher.csproj | 2 +- .../Unity/BepInEx.Unity.Common/BepInEx.Unity.Common.csproj | 2 +- .../Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj | 4 ++-- .../BepInEx.Unity.Mono.Preloader.csproj | 2 +- Runtimes/Unity/BepInEx.Unity.Mono/BepInEx.Unity.Mono.csproj | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/BepInEx.Core/BepInEx.Core.csproj b/BepInEx.Core/BepInEx.Core.csproj index 4b208a9..30a9e20 100644 --- a/BepInEx.Core/BepInEx.Core.csproj +++ b/BepInEx.Core/BepInEx.Core.csproj @@ -8,7 +8,7 @@ - + diff --git a/BepInEx.Preloader.Core/BepInEx.Preloader.Core.csproj b/BepInEx.Preloader.Core/BepInEx.Preloader.Core.csproj index 06e3b9f..b75a844 100644 --- a/BepInEx.Preloader.Core/BepInEx.Preloader.Core.csproj +++ b/BepInEx.Preloader.Core/BepInEx.Preloader.Core.csproj @@ -8,7 +8,7 @@ - + diff --git a/README.md b/README.md index 8e6dbe9..95a4de4 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ A more comprehensive comparison list of features and compatibility is available ## Used libraries - [NeighTools/UnityDoorstop](https://github.com/NeighTools/UnityDoorstop) - v4.3.0 -- [BepInEx/HarmonyX](https://github.com/BepInEx/HarmonyX) - v2.10.1 +- [BepInEx/HarmonyX](https://github.com/BepInEx/HarmonyX) - v2.10.2 - [0x0ade/MonoMod](https://github.com/0x0ade/MonoMod) - v22.5.1.1 - [jbevain/cecil](https://github.com/jbevain/cecil) - v0.10.4 diff --git a/Runtimes/NET/BepInEx.NET.Common/BepInEx.NET.Common.csproj b/Runtimes/NET/BepInEx.NET.Common/BepInEx.NET.Common.csproj index a93ecf2..6884e40 100644 --- a/Runtimes/NET/BepInEx.NET.Common/BepInEx.NET.Common.csproj +++ b/Runtimes/NET/BepInEx.NET.Common/BepInEx.NET.Common.csproj @@ -6,7 +6,7 @@ - + diff --git a/Runtimes/NET/BepInEx.NET.Framework.Launcher/BepInEx.NET.Framework.Launcher.csproj b/Runtimes/NET/BepInEx.NET.Framework.Launcher/BepInEx.NET.Framework.Launcher.csproj index 274dc40..90a37b8 100644 --- a/Runtimes/NET/BepInEx.NET.Framework.Launcher/BepInEx.NET.Framework.Launcher.csproj +++ b/Runtimes/NET/BepInEx.NET.Framework.Launcher/BepInEx.NET.Framework.Launcher.csproj @@ -14,7 +14,7 @@ - + diff --git a/Runtimes/Unity/BepInEx.Unity.Common/BepInEx.Unity.Common.csproj b/Runtimes/Unity/BepInEx.Unity.Common/BepInEx.Unity.Common.csproj index 2ff6c0c..78ebd3c 100644 --- a/Runtimes/Unity/BepInEx.Unity.Common/BepInEx.Unity.Common.csproj +++ b/Runtimes/Unity/BepInEx.Unity.Common/BepInEx.Unity.Common.csproj @@ -6,7 +6,7 @@ - + diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj index d902167..32eb297 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj @@ -13,8 +13,8 @@ - - + + diff --git a/Runtimes/Unity/BepInEx.Unity.Mono.Preloader/BepInEx.Unity.Mono.Preloader.csproj b/Runtimes/Unity/BepInEx.Unity.Mono.Preloader/BepInEx.Unity.Mono.Preloader.csproj index 58887aa..7a33d1d 100644 --- a/Runtimes/Unity/BepInEx.Unity.Mono.Preloader/BepInEx.Unity.Mono.Preloader.csproj +++ b/Runtimes/Unity/BepInEx.Unity.Mono.Preloader/BepInEx.Unity.Mono.Preloader.csproj @@ -16,6 +16,6 @@ - + diff --git a/Runtimes/Unity/BepInEx.Unity.Mono/BepInEx.Unity.Mono.csproj b/Runtimes/Unity/BepInEx.Unity.Mono/BepInEx.Unity.Mono.csproj index 26086e1..bf4385f 100644 --- a/Runtimes/Unity/BepInEx.Unity.Mono/BepInEx.Unity.Mono.csproj +++ b/Runtimes/Unity/BepInEx.Unity.Mono/BepInEx.Unity.Mono.csproj @@ -16,7 +16,7 @@ - + From 952c74f6fef9ad5b5d7237c9e08d53c7d3148e6a Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Wed, 14 Aug 2024 19:31:22 -0400 Subject: [PATCH 06/65] Implement Patcher and plugin providers --- BepInEx.Core/Bootstrap/BaseChainloader.cs | 238 ++++++++++-------- BepInEx.Core/Bootstrap/TypeLoader.cs | 97 +++++-- BepInEx.Core/Contract/Attributes.cs | 52 +++- BepInEx.Core/Contract/BasePluginProvider.cs | 71 ++++++ BepInEx.Core/Contract/IPluginLoader.cs | 26 ++ BepInEx.Core/Contract/PluginInfo.cs | 30 +-- BepInEx.Core/Paths.cs | 12 + .../BepInEx.PatcherProvider.csproj | 16 ++ .../BepInExPatcherLoader.cs | 19 ++ .../BepInExPatcherProvider.cs | 48 ++++ .../BepInEx.PluginProvider.csproj | 16 ++ BepInEx.PluginProvider/BepInExPluginLoader.cs | 19 ++ .../BepInExPluginProvider.cs | 47 ++++ .../Patching/AssemblyPatcher.cs | 231 ++++++++++++----- BepInEx.Preloader.Core/Patching/Attributes.cs | 25 ++ .../Patching/BasePatcherProvider.cs | 55 ++++ .../Patching/PatcherPluginMetadata.cs | 5 + BepInEx.sln | 20 ++ Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs | 2 +- .../BepInEx.NET.Common.csproj | 12 + .../BepInEx.NET.CoreCLR/NetCorePreloader.cs | 2 +- .../NetPreloader.cs | 2 +- .../Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs | 2 +- .../BepInEx.Unity.IL2CPP.csproj | 12 + .../Unity/BepInEx.Unity.IL2CPP/Preloader.cs | 2 +- .../UnityPreloader.cs | 2 +- .../BepInEx.Unity.Mono/BaseUnityPlugin.cs | 3 +- .../BepInEx.Unity.Mono.csproj | 12 + build/Program.cs | 2 + 29 files changed, 864 insertions(+), 216 deletions(-) create mode 100644 BepInEx.Core/Contract/BasePluginProvider.cs create mode 100644 BepInEx.Core/Contract/IPluginLoader.cs create mode 100644 BepInEx.PatcherProvider/BepInEx.PatcherProvider.csproj create mode 100644 BepInEx.PatcherProvider/BepInExPatcherLoader.cs create mode 100644 BepInEx.PatcherProvider/BepInExPatcherProvider.cs create mode 100644 BepInEx.PluginProvider/BepInEx.PluginProvider.csproj create mode 100644 BepInEx.PluginProvider/BepInExPluginLoader.cs create mode 100644 BepInEx.PluginProvider/BepInExPluginProvider.cs create mode 100644 BepInEx.Preloader.Core/Patching/BasePatcherProvider.cs diff --git a/BepInEx.Core/Bootstrap/BaseChainloader.cs b/BepInEx.Core/Bootstrap/BaseChainloader.cs index c6fe182..9489e93 100644 --- a/BepInEx.Core/Bootstrap/BaseChainloader.cs +++ b/BepInEx.Core/Bootstrap/BaseChainloader.cs @@ -25,14 +25,14 @@ public abstract class BaseChainloader /// Type definition to analyze. /// The filepath of the assembly, to keep as metadata. /// If the type represent a valid plugin, returns a instance. Otherwise, return null. - public static PluginInfo ToPluginInfo(TypeDefinition type, string assemblyLocation) + public static PluginInfo ToPluginInfo(TypeDefinition type, IPluginLoader loader, string assemblyLocation) { if (type.IsInterface || type.IsAbstract) return null; try { - if (!type.IsSubtypeOf(typeof(TPlugin))) + if (!type.IsSubtypeOf(typeof(T))) return null; } catch (AssemblyResolutionException) @@ -41,8 +41,13 @@ public abstract class BaseChainloader return null; } - var metadata = BepInPlugin.FromCecilType(type); + if (type.IsSubtypeOf(typeof(BasePluginProvider))) + return GetPluginInfo(type, BepInPluginProvider.FromCecilType(type), loader, assemblyLocation); + return GetPluginInfo(type, BepInPlugin.FromCecilType(type), loader, assemblyLocation); + } + private static PluginInfo GetPluginInfo(TypeDefinition type, BepInPlugin metadata, IPluginLoader loader, string assemblyLocation) + { // Perform checks that will prevent the plugin from being loaded in ALL cases if (metadata == null) { @@ -85,15 +90,16 @@ public abstract class BaseChainloader Incompatibilities = incompatibilities, TypeName = type.FullName, TargettedBepInExVersion = bepinVersion, - Location = assemblyLocation + Location = assemblyLocation, + Loader = loader }; } - protected static bool HasBepinPlugins(AssemblyDefinition ass) + protected static bool HasBepinPluginType(AssemblyDefinition ass) { if (ass.MainModule.AssemblyReferences.All(r => r.Name != CurrentAssemblyName)) return false; - if (ass.MainModule.GetTypeReferences().All(r => r.FullName != typeof(BepInPlugin).FullName)) + if (ass.MainModule.GetTypeReferences().All(r => r.FullName != typeof(T).FullName)) return false; return true; @@ -116,10 +122,20 @@ public abstract class BaseChainloader private bool _initialized; /// - /// List of all instances loaded via the chainloader. + /// List of all instances of all plugins loaded via the chainloader. /// public Dictionary Plugins { get; } = new(); + /// + /// List of all instances of all providers loaded via the chainloader. + /// + public Dictionary Providers { get; } = new(); + + /// + /// List of all discovered from all the providers + /// + public List Loaders { get; } = new(); + /// /// Collection of error chainloader messages that occured during plugin loading. /// Contains information about what certain plugins were not loaded. @@ -194,38 +210,30 @@ public abstract class BaseChainloader /// Path from which to search the plugins. /// Cache name to use. If null, results are not cached. /// List of discovered plugins and their metadata. - protected IList DiscoverPluginsFrom(string path, string cacheName = "chainloader") + protected List DiscoverPluginProvidersFrom(string path, string cacheName = "chainloader_meta") { var pluginsToLoad = - TypeLoader.FindPluginTypes(path, ToPluginInfo, HasBepinPlugins, cacheName); + TypeLoader.FindPluginTypes(path, ToPluginInfo, HasBepinPluginType, cacheName); return pluginsToLoad.SelectMany(p => p.Value).ToList(); } - /// - /// Discovers plugins to load. - /// - /// List of plugins to be loaded. - protected virtual IList DiscoverPlugins() - { - return DiscoverPluginsFrom(Paths.PluginPath); - } - /// /// Preprocess the plugins and modify the load order. /// /// Some plugins may be skipped if they cannot be loaded (wrong metadata, etc). /// Plugins to process. /// List of plugins to load in the correct load order. - protected virtual IList ModifyLoadOrder(IList plugins) + protected virtual List ModifyLoadOrder(IList plugins, Dictionary existingPlugins) + where T : PluginInfo { // We use a sorted dictionary to ensure consistent load order var dependencyDict = new SortedDictionary>(StringComparer.InvariantCultureIgnoreCase); - var pluginsByGuid = new Dictionary(); + var pluginsByGuid = new Dictionary(); foreach (var pluginInfoGroup in plugins.GroupBy(info => info.Metadata.GUID)) { - if (Plugins.TryGetValue(pluginInfoGroup.Key, out var loadedPlugin)) + if (existingPlugins.TryGetValue(pluginInfoGroup.Key, out var loadedPlugin)) { Logger.Log(LogLevel.Warning, $"Skipping [{pluginInfoGroup.Key}] because a plugin with a similar GUID ([{loadedPlugin}]) has been already loaded."); @@ -266,7 +274,7 @@ public abstract class BaseChainloader foreach (var pluginInfo in pluginsByGuid.Values.ToList()) if (pluginInfo.Incompatibilities.Any(incompatibility => pluginsByGuid.ContainsKey(incompatibility.IncompatibilityGUID) - || Plugins.ContainsKey(incompatibility.IncompatibilityGUID)) + || existingPlugins.ContainsKey(incompatibility.IncompatibilityGUID)) ) { pluginsByGuid.Remove(pluginInfo.Metadata.GUID); @@ -275,7 +283,7 @@ public abstract class BaseChainloader var incompatiblePluginsNew = pluginInfo.Incompatibilities.Select(x => x.IncompatibilityGUID) .Where(x => pluginsByGuid.ContainsKey(x)); var incompatiblePluginsExisting = pluginInfo.Incompatibilities.Select(x => x.IncompatibilityGUID) - .Where(x => Plugins.ContainsKey(x)); + .Where(x => existingPlugins.ContainsKey(x)); var incompatiblePlugins = incompatiblePluginsNew.Concat(incompatiblePluginsExisting).ToArray(); var message = $@"Could not load [{pluginInfo}] because it is incompatible with: {string.Join(", ", incompatiblePlugins)}"; @@ -306,15 +314,23 @@ public abstract class BaseChainloader } /// - /// Run the chainloader and load all plugins from the plugins folder. + /// Run the chainloader and load all plugins from all providers. /// public virtual void Execute() { try { - var plugins = DiscoverPlugins(); + var builtinProviders = DiscoverPluginProvidersFrom(Paths.BepInExAssemblyDirectory); + var providers = DiscoverPluginProvidersFrom(Paths.PluginProviderPath); + Logger.Log(LogLevel.Info, $"{providers.Count + builtinProviders.Count} " + + $"meta plugin{(providers.Count + builtinProviders.Count == 1 ? "" : "s")} to load"); + + LoadPlugins(builtinProviders, provider: true); + LoadPlugins(providers, provider: true); + + var plugins = TypeLoader.GetPluginsFromLoaders(Loaders, ToPluginInfo, HasBepinPluginType, "chainloader"); Logger.Log(LogLevel.Info, $"{plugins.Count} plugin{(plugins.Count == 1 ? "" : "s")} to load"); - LoadPlugins(plugins); + LoadPlugins(plugins, provider: false); Finished?.Invoke(); } catch (Exception ex) @@ -331,71 +347,19 @@ public abstract class BaseChainloader Logger.Log(LogLevel.Message, "Chainloader startup complete"); } - private IList LoadPlugins(IList plugins) + private void LoadPlugins(List plugins, bool provider) { - var sortedPlugins = ModifyLoadOrder(plugins); - + var existingPlugins = provider ? Providers : Plugins; + var sortedPlugins = ModifyLoadOrder(plugins, existingPlugins); + var invalidPlugins = new HashSet(); var processedPlugins = new Dictionary(); var loadedAssemblies = new Dictionary(); - var loadedPlugins = new List(); foreach (var plugin in sortedPlugins) { - var dependsOnInvalidPlugin = false; - var missingDependencies = new List(); - foreach (var dependency in plugin.Dependencies) + if (!CheckDependencies(plugin, processedPlugins, invalidPlugins, existingPlugins)) { - static bool IsHardDependency(BepInDependency dep) => - (dep.Flags & BepInDependency.DependencyFlags.HardDependency) != 0; - - // If the dependency wasn't already processed, it's missing altogether - var dependencyExists = - processedPlugins.TryGetValue(dependency.DependencyGUID, out var pluginVersion); - // Alternatively, if the dependency hasn't been loaded before, it's missing too - if (!dependencyExists) - { - dependencyExists = Plugins.TryGetValue(dependency.DependencyGUID, out var pluginInfo); - pluginVersion = pluginInfo?.Metadata.Version; - } - - if (!dependencyExists || dependency.VersionRange != null && - !dependency.VersionRange.IsSatisfied(pluginVersion)) - { - // If the dependency is hard, collect it into a list to show - if (IsHardDependency(dependency)) - missingDependencies.Add(dependency); - continue; - } - - // If the dependency is a hard and is invalid (e.g. has missing dependencies), report that to the user - if (invalidPlugins.Contains(dependency.DependencyGUID) && IsHardDependency(dependency)) - { - dependsOnInvalidPlugin = true; - break; - } - } - - processedPlugins.Add(plugin.Metadata.GUID, plugin.Metadata.Version); - - if (dependsOnInvalidPlugin) - { - var message = - $"Skipping [{plugin}] because it has a dependency that was not loaded. See previous errors for details."; - DependencyErrors.Add(message); - Logger.Log(LogLevel.Warning, message); - continue; - } - - if (missingDependencies.Count != 0) - { - var message = $@"Could not load [{plugin}] because it has missing dependencies: { - string.Join(", ", missingDependencies.Select(s => s.VersionRange == null ? s.DependencyGUID : $"{s.DependencyGUID} ({s.VersionRange})").ToArray()) - }"; - DependencyErrors.Add(message); - Logger.Log(LogLevel.Error, message); - - invalidPlugins.Add(plugin.Metadata.GUID); continue; } @@ -403,45 +367,107 @@ public abstract class BaseChainloader { Logger.Log(LogLevel.Info, $"Loading [{plugin}]"); - if (!loadedAssemblies.TryGetValue(plugin.Location, out var ass)) - loadedAssemblies[plugin.Location] = ass = Assembly.LoadFrom(plugin.Location); + Assembly ass = null; + if (provider && !loadedAssemblies.TryGetValue(plugin.Location, out ass)) + { + ass = Assembly.LoadFrom(plugin.Location); + loadedAssemblies[plugin.Location] = ass; + } + else if (!provider && !loadedAssemblies.TryGetValue(plugin.Loader.AssemblyIdentifier, out ass)) + { + ass = Assembly.Load(plugin.Loader.GetAssemblyData()); + loadedAssemblies[plugin.Loader.AssemblyIdentifier] = ass; + } - Plugins[plugin.Metadata.GUID] = plugin; + existingPlugins[plugin.Metadata.GUID] = plugin; TryRunModuleCtor(plugin, ass); - plugin.Instance = LoadPlugin(plugin, ass); - loadedPlugins.Add(plugin); + if (provider) + { + var type = ass.GetType(plugin.TypeName); + plugin.Instance = Activator.CreateInstance(type); + AppDomain.CurrentDomain.AssemblyResolve += (_, args) => ((BasePluginProvider)plugin.Instance).ResolveAssembly(args.Name); + var requestedPluginsStream = ((BasePluginProvider)plugin.Instance).GetPlugins(); + Loaders.AddRange(requestedPluginsStream); + } + else + { + plugin.Instance = LoadPlugin(plugin, ass); + } PluginLoaded?.Invoke(plugin); } catch (Exception ex) { invalidPlugins.Add(plugin.Metadata.GUID); - Plugins.Remove(plugin.Metadata.GUID); + existingPlugins.Remove(plugin.Metadata.GUID); Logger.Log(LogLevel.Error, $"Error loading [{plugin}]: {(ex is ReflectionTypeLoadException re ? TypeLoader.TypeLoadExceptionToString(re) : ex.ToString())}"); } } - - return loadedPlugins; } - /// - /// Detects and loads all plugins in the specified directories. - /// - /// - /// It is better to collect all paths at once and use a single call to LoadPlugins than multiple calls. - /// This allows to run proper dependency resolving and to load all plugins in one go. - /// - /// Directories to search the plugins from. - /// List of loaded plugin infos. - public IList LoadPlugins(params string[] pluginsPaths) + private bool CheckDependencies(T plugin, Dictionary processedPlugins, HashSet invalidPlugins, Dictionary existingPlugins) + where T : PluginInfo { - // TODO: This is a temporary solution for 3rd party loaders. Instead, this should be done via metaplugins. - var plugins = new List(); - foreach (var pluginsPath in pluginsPaths) - plugins.AddRange(DiscoverPluginsFrom(pluginsPath)); - return LoadPlugins(plugins); + var dependsOnInvalidPlugin = false; + var missingDependencies = new List(); + foreach (var dependency in plugin.Dependencies) + { + static bool IsHardDependency(BepInDependency dep) => + (dep.Flags & BepInDependency.DependencyFlags.HardDependency) != 0; + + // If the dependency wasn't already processed, it's missing altogether + var dependencyExists = + processedPlugins.TryGetValue(dependency.DependencyGUID, out var pluginVersion); + // Alternatively, if the dependency hasn't been loaded before, it's missing too + if (!dependencyExists) + { + dependencyExists = existingPlugins.TryGetValue(dependency.DependencyGUID, out var pluginInfo); + pluginVersion = pluginInfo?.Metadata.Version; + } + + if (!dependencyExists || dependency.VersionRange != null && + !dependency.VersionRange.IsSatisfied(pluginVersion)) + { + // If the dependency is hard, collect it into a list to show + if (IsHardDependency(dependency)) + missingDependencies.Add(dependency); + continue; + } + + // If the dependency is a hard and is invalid (e.g. has missing dependencies), report that to the user + if (invalidPlugins.Contains(dependency.DependencyGUID) && IsHardDependency(dependency)) + { + dependsOnInvalidPlugin = true; + break; + } + } + + processedPlugins.Add(plugin.Metadata.GUID, plugin.Metadata.Version); + + if (dependsOnInvalidPlugin) + { + var message = + $"Skipping [{plugin}] because it has a dependency that was not loaded. See previous errors for details."; + DependencyErrors.Add(message); + Logger.Log(LogLevel.Warning, message); + return false; + } + + if (missingDependencies.Count != 0) + { + var message = $@"Could not load [{plugin}] because it has missing dependencies: { + string.Join(", ", missingDependencies.Select(s => s.VersionRange == null ? s.DependencyGUID : $"{s.DependencyGUID} ({s.VersionRange})").ToArray()) + }"; + DependencyErrors.Add(message); + Logger.Log(LogLevel.Error, message); + + invalidPlugins.Add(plugin.Metadata.GUID); + return false; + } + + return true; } private static void TryRunModuleCtor(PluginInfo plugin, Assembly assembly) diff --git a/BepInEx.Core/Bootstrap/TypeLoader.cs b/BepInEx.Core/Bootstrap/TypeLoader.cs index 69a7775..111ac38 100644 --- a/BepInEx.Core/Bootstrap/TypeLoader.cs +++ b/BepInEx.Core/Bootstrap/TypeLoader.cs @@ -89,7 +89,9 @@ public static class TypeLoader { Paths.BepInExAssemblyDirectory, Paths.PluginPath, + Paths.PluginProviderPath, Paths.PatcherPluginPath, + Paths.PatcherProviderPath, Paths.ManagedPath }.Concat(SearchDirectories); @@ -126,9 +128,8 @@ public static class TypeLoader /// selector. /// public static Dictionary> FindPluginTypes(string directory, - Func typeSelector, - Func assemblyFilter = - null, + Func typeSelector, + Func assemblyFilter = null, string cacheName = null) where T : ICacheable, new() { @@ -153,19 +154,7 @@ public static class TypeLoader continue; } - using var ass = AssemblyDefinition.ReadAssembly(dllMs, ReaderParameters); - Logger.Log(LogLevel.Debug, $"Examining '{dll}'"); - - if (!assemblyFilter?.Invoke(ass) ?? false) - { - result[dll] = new List(); - continue; - } - - var matches = ass.MainModule.Types - .Select(t => typeSelector(t, dll)) - .Where(t => t != null).ToList(); - result[dll] = matches; + result[dll] = ExamineStream(typeSelector, assemblyFilter, dllMs, null, dll); } catch (BadImageFormatException e) { @@ -183,6 +172,82 @@ public static class TypeLoader return result; } + /// + /// Looks up assemblies using the given loaders and locates all types that can be loaded and collects their metadata. + /// + /// The specific base type to search for. + /// The loaders to obtain the assemblies from. + /// 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 dictionary of all assemblies in the directory and the list of type metadatas of types that match the + /// selector. + /// + public static List GetPluginsFromLoaders(IList loaders, + Func typeSelector, + Func assemblyFilter = null, + string cacheName = null) + where T : ICacheable, new() + { + var result = new Dictionary>(); + var hashes = new Dictionary(); + Dictionary> cache = null; + + if (cacheName != null) + cache = LoadAssemblyCache(cacheName); + + foreach (IPluginLoader pluginLoader in loaders) + { + IList plugins; + if (cache != null && pluginLoader.AssemblyHash != null && + cache.TryGetValue(pluginLoader.AssemblyIdentifier, out var cacheEntry) && + pluginLoader.AssemblyHash == cacheEntry.Hash) + { + plugins = cacheEntry.CacheItems; + } + else + { + var assemblyData = pluginLoader.GetAssemblyData(); + using var memory = new MemoryStream(assemblyData); + plugins = ExamineStream(typeSelector, assemblyFilter, memory, pluginLoader, null); + } + + foreach (T pluginInfo in plugins) + { + if (!result.ContainsKey(pluginLoader.AssemblyIdentifier)) + result[pluginLoader.AssemblyIdentifier] = new(); + result[pluginLoader.AssemblyIdentifier].Add(pluginInfo); + } + } + + if (cache != null) + SaveAssemblyCache(cacheName, result, hashes); + + return result.SelectMany(x => x.Value).ToList(); + } + + private static List ExamineStream(Func typeSelector, + Func assemblyFilter, + MemoryStream dllMs, + IPluginLoader loader, + string location) + where T : ICacheable, new() + { + using var ass = AssemblyDefinition.ReadAssembly(dllMs, ReaderParameters); + Logger.Log(LogLevel.Debug, $"Examining '{ass.Name}'"); + + if (!assemblyFilter?.Invoke(ass) ?? false) + { + return new List(); + } + + var matches = ass.MainModule.Types + .Select(t => typeSelector(t, loader, location)) + .Where(t => t != null).ToList(); + return matches; + } + /// /// Loads an index of type metadatas from a cache. /// diff --git a/BepInEx.Core/Contract/Attributes.cs b/BepInEx.Core/Contract/Attributes.cs index 0de9e93..978c170 100644 --- a/BepInEx.Core/Contract/Attributes.cs +++ b/BepInEx.Core/Contract/Attributes.cs @@ -76,6 +76,32 @@ public class BepInPlugin : Attribute } } +/// +/// This attribute denotes that a class is a plugin provider, and specifies the required metadata. +/// +[AttributeUsage(AttributeTargets.Class)] +public class BepInPluginProvider : BepInPlugin +{ + /// The unique identifier of the plugin. Should not change between plugin versions. + /// The user friendly name of the plugin. Is able to be changed between versions. + /// The specific version of the plugin. + public BepInPluginProvider(string GUID, string Name, string Version) : base(GUID, Name, Version) + { + } + + internal static BepInPluginProvider FromCecilType(TypeDefinition td) + { + var attr = MetadataHelper.GetCustomAttributes(td, false).FirstOrDefault(); + + if (attr == null) + return null; + + return new BepInPluginProvider((string) attr.ConstructorArguments[0].Value, + (string) attr.ConstructorArguments[1].Value, + (string) attr.ConstructorArguments[2].Value); + } +} + /// /// This attribute specifies any dependencies that this plugin has on other plugins. /// @@ -264,7 +290,7 @@ public static class MetadataHelper /// /// The plugin type. /// The BepInPlugin metadata of the plugin type. - public static BepInPlugin GetMetadata(Type pluginType) + public static BepInPlugin GetPluginMetadata(Type pluginType) { var attributes = pluginType.GetCustomAttributes(typeof(BepInPlugin), false); @@ -279,7 +305,29 @@ public static class MetadataHelper /// /// The plugin instance. /// The BepInPlugin metadata of the plugin instance. - public static BepInPlugin GetMetadata(object plugin) => GetMetadata(plugin.GetType()); + public static BepInPlugin GetPluginMetadata(object plugin) => GetPluginMetadata(plugin.GetType()); + + /// + /// Retrieves the BepInPluginProvider metadata from a plugin type. + /// + /// The plugin type. + /// The BepInPluginProvider metadata of the plugin type. + public static BepInPlugin GetPluginProviderMetadata(Type pluginType) + { + var attributes = pluginType.GetCustomAttributes(typeof(BepInPluginProvider), false); + + if (attributes.Length == 0) + return null; + + return (BepInPlugin) attributes[0]; + } + + /// + /// Retrieves the BepInPluginProvider metadata from a plugin instance. + /// + /// The plugin instance. + /// The BepInPluginProvider metadata of the plugin instance. + public static BepInPlugin GetPluginProviderMetadata(object plugin) => GetPluginProviderMetadata(plugin.GetType()); /// /// Gets the specified attributes of a type, if they exist. diff --git a/BepInEx.Core/Contract/BasePluginProvider.cs b/BepInEx.Core/Contract/BasePluginProvider.cs new file mode 100644 index 0000000..202bd3a --- /dev/null +++ b/BepInEx.Core/Contract/BasePluginProvider.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using BepInEx.Configuration; +using BepInEx.Logging; + +namespace BepInEx; + +/// +/// The base plugin provider type that is used by the BepInEx plugin loader. +/// +public abstract class BasePluginProvider +{ + /// + /// Create a new instance of a plugin provider and all of its tied in objects. + /// + /// BepInPluginProvider attribute is missing. + protected BasePluginProvider() + { + var metadata = MetadataHelper.GetPluginProviderMetadata(this); + if (metadata == null) + throw new InvalidOperationException("Can't create an instance of " + GetType().FullName + + " because it inherits from BasePluginProvider and the BepInPluginProvider attribute is missing."); + + Info = new PluginInfo + { + Metadata = metadata, + Instance = this, + Dependencies = MetadataHelper.GetDependencies(GetType()), + Processes = MetadataHelper.GetAttributes(GetType()), + }; + + Logger = BepInEx.Logging.Logger.CreateLogSource(metadata.Name); + + Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, metadata.GUID + ".cfg"), false, metadata); + } + + /// + /// Information about this plugin provider as it was loaded. + /// + public PluginInfo Info { get; } + + /// + /// Logger instance tied to this plugin provider. + /// + protected ManualLogSource Logger { get; } + + /// + /// Default config file tied to this plugin provider. The config file will not be created until + /// any settings are added and changed, or is called. + /// + public ConfigFile Config { get; } + + /// + /// Obtains a list of assemblies containing plugins to load + /// + /// A list of loaders, one per assembly + public abstract IList GetPlugins(); + + /// + /// A custom assembly resolver that can be used by this provider to resolve assemblies + /// that have failed to resolve + /// + /// The assembly's name + /// The resolved assembly or null if the assembly couldn't be resolved + public virtual Assembly ResolveAssembly(string name) + { + return null; + } +} diff --git a/BepInEx.Core/Contract/IPluginLoader.cs b/BepInEx.Core/Contract/IPluginLoader.cs new file mode 100644 index 0000000..f98c937 --- /dev/null +++ b/BepInEx.Core/Contract/IPluginLoader.cs @@ -0,0 +1,26 @@ +namespace BepInEx; + +/// +/// An interface that allows to dynamically load and track an assembly +/// which can be obtained from a provided +/// +public interface IPluginLoader +{ + /// + /// An identifier that uniquely identifies an assembly from a provider no matter its revision + /// + public string AssemblyIdentifier { get; } + + /// + /// An optional hash that changes each time the assembly changes which can be tracked for cache + /// invalidation purposes. If this is null, no caching occurs for this assembly load + /// + public string AssemblyHash { get; } + + /// + /// Obtains the assembly's raw data without loading it into the appdomain. + /// This may be called multiple times by the chainloader + /// + /// The assembly's raw data in bytes + public byte[] GetAssemblyData(); +} diff --git a/BepInEx.Core/Contract/PluginInfo.cs b/BepInEx.Core/Contract/PluginInfo.cs index 0aa7585..f4d33ca 100644 --- a/BepInEx.Core/Contract/PluginInfo.cs +++ b/BepInEx.Core/Contract/PluginInfo.cs @@ -16,6 +16,11 @@ public class PluginInfo : ICacheable /// General metadata about a plugin. /// public BepInPlugin Metadata { get; internal set; } + + /// + /// The loader used to load this plugin, null if it is a provider + /// + public IPluginLoader Loader { get; internal set; } /// /// Collection of attributes that describe what processes the plugin can run on. @@ -33,24 +38,23 @@ public class PluginInfo : ICacheable /// public IEnumerable Incompatibilities { get; internal set; } - /// - /// File path to the plugin DLL - /// - public string Location { get; internal set; } - + public string TypeName { get; internal set; } + /// /// Instance of the plugin that represents this info. NULL if no plugin is instantiated from info (yet) /// public object Instance { get; internal set; } - - public string TypeName { get; internal set; } - + internal Version TargettedBepInExVersion { get; set; } + + internal string Location { get; set; } + + /// + public override string ToString() => $"{Metadata?.Name} {Metadata?.Version}"; - void ICacheable.Save(BinaryWriter bw) + public virtual void Save(BinaryWriter bw) { bw.Write(TypeName); - bw.Write(Location); bw.Write(Metadata.GUID); bw.Write(Metadata.Name); @@ -74,10 +78,9 @@ public class PluginInfo : ICacheable bw.Write(TargettedBepInExVersion.ToString(4)); } - void ICacheable.Load(BinaryReader br) + public virtual void Load(BinaryReader br) { TypeName = br.ReadString(); - Location = br.ReadString(); Metadata = new BepInPlugin(br.ReadString(), br.ReadString(), br.ReadString()); @@ -111,7 +114,4 @@ public class PluginInfo : ICacheable TargettedBepInExVersion = new Version(br.ReadString()); } - - /// - public override string ToString() => $"{Metadata?.Name} {Metadata?.Version}"; } diff --git a/BepInEx.Core/Paths.cs b/BepInEx.Core/Paths.cs index 34d3e98..ca5a446 100644 --- a/BepInEx.Core/Paths.cs +++ b/BepInEx.Core/Paths.cs @@ -75,6 +75,16 @@ public static class Paths /// public static string PatcherPluginPath { get; private set; } + /// + /// The path to the plugin provider folder which resides in the BepInEx folder. + /// + public static string PluginProviderPath { get; private set; } + + /// + /// The path to the patcher plugin provider folder which resides in the BepInEx folder. + /// + public static string PatcherProviderPath { get; private set; } + /// /// The path to the plugin folder which resides in the BepInEx folder. /// @@ -113,7 +123,9 @@ public static class Paths BepInExRootPath = bepinRootPath ?? Path.Combine(GameRootPath, "BepInEx"); ConfigPath = Path.Combine(BepInExRootPath, "config"); BepInExConfigPath = Path.Combine(ConfigPath, "BepInEx.cfg"); + PluginProviderPath = Path.Combine(BepInExRootPath, "plugins-providers"); PluginPath = Path.Combine(BepInExRootPath, "plugins"); + PatcherProviderPath = Path.Combine(BepInExRootPath, "patchers-providers"); PatcherPluginPath = Path.Combine(BepInExRootPath, "patchers"); BepInExAssemblyDirectory = Path.Combine(BepInExRootPath, "core"); BepInExAssemblyPath = Path.Combine(BepInExAssemblyDirectory, diff --git a/BepInEx.PatcherProvider/BepInEx.PatcherProvider.csproj b/BepInEx.PatcherProvider/BepInEx.PatcherProvider.csproj new file mode 100644 index 0000000..a56204f --- /dev/null +++ b/BepInEx.PatcherProvider/BepInEx.PatcherProvider.csproj @@ -0,0 +1,16 @@ + + + + net35;netstandard2.0 + true + BepInEx patcher provider module + + + + + + + + + + diff --git a/BepInEx.PatcherProvider/BepInExPatcherLoader.cs b/BepInEx.PatcherProvider/BepInExPatcherLoader.cs new file mode 100644 index 0000000..0221434 --- /dev/null +++ b/BepInEx.PatcherProvider/BepInExPatcherLoader.cs @@ -0,0 +1,19 @@ +using System.IO; + +namespace BepInEx.PatcherProvider; + +public class BepInExPatcherLoader : IPluginLoader +{ + public string AssemblyIdentifier { get; internal set; } + public string AssemblyHash { get; internal set; } + private byte[] assemblyData; + public byte[] GetAssemblyData() + { + if (assemblyData == null) + { + assemblyData = File.ReadAllBytes(AssemblyIdentifier); + } + + return assemblyData; + } +} diff --git a/BepInEx.PatcherProvider/BepInExPatcherProvider.cs b/BepInEx.PatcherProvider/BepInExPatcherProvider.cs new file mode 100644 index 0000000..24ef3cc --- /dev/null +++ b/BepInEx.PatcherProvider/BepInExPatcherProvider.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using BepInEx.Logging; +using BepInEx.Preloader.Core.Patching; + +namespace BepInEx.PatcherProvider; + +[PatcherProviderPluginInfo("io.bepinex.bepinexpatcherprovider", "BepInExPatcherProvider", "1.0")] +public class BepInExPatcherProvider : BasePatcherProvider +{ + private static readonly Dictionary AssemblyLocationsByFilename = new(); + + public override IList GetPatchers() + { + var loaders = new List(); + foreach (var dll in Directory.GetFiles(Path.GetFullPath(Paths.PatcherPluginPath), "*.dll", SearchOption.AllDirectories)) + { + try + { + AssemblyLocationsByFilename.Add(Path.GetFileNameWithoutExtension(dll), Path.GetDirectoryName(dll)); + loaders.Add(new BepInExPatcherLoader + { + AssemblyHash = File.GetLastWriteTimeUtc(dll).ToString("O"), + AssemblyIdentifier = dll + }); + } + catch (Exception e) + { + Log.Log(LogLevel.Error, e); + } + } + + return loaders; + } + + public override Assembly ResolveAssembly(string name) + { + if (!AssemblyLocationsByFilename.TryGetValue(name, out var location)) + return null; + + if (!Utility.TryResolveDllAssembly(new(name), location, out var ass)) + return null; + + return ass; + } +} diff --git a/BepInEx.PluginProvider/BepInEx.PluginProvider.csproj b/BepInEx.PluginProvider/BepInEx.PluginProvider.csproj new file mode 100644 index 0000000..ee9c085 --- /dev/null +++ b/BepInEx.PluginProvider/BepInEx.PluginProvider.csproj @@ -0,0 +1,16 @@ + + + + net35;netstandard2.0 + true + BepInEx plugin provider module + + + + + + + + + + diff --git a/BepInEx.PluginProvider/BepInExPluginLoader.cs b/BepInEx.PluginProvider/BepInExPluginLoader.cs new file mode 100644 index 0000000..a71f3d7 --- /dev/null +++ b/BepInEx.PluginProvider/BepInExPluginLoader.cs @@ -0,0 +1,19 @@ +using System.IO; + +namespace BepInEx.PluginProvider; + +public class BepInExPluginLoader : IPluginLoader +{ + public string AssemblyIdentifier { get; internal set; } + public string AssemblyHash { get; internal set; } + private byte[] assemblyData; + public byte[] GetAssemblyData() + { + if (assemblyData == null) + { + assemblyData = File.ReadAllBytes(AssemblyIdentifier); + } + + return assemblyData; + } +} diff --git a/BepInEx.PluginProvider/BepInExPluginProvider.cs b/BepInEx.PluginProvider/BepInExPluginProvider.cs new file mode 100644 index 0000000..569b304 --- /dev/null +++ b/BepInEx.PluginProvider/BepInExPluginProvider.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using BepInEx.Logging; + +namespace BepInEx.PluginProvider; + +[BepInPluginProvider("io.bepinex.bepinexpluginprovider", "BepInExPluginProvider", "1.0")] +public class BepInExPluginProvider : BasePluginProvider +{ + private static readonly Dictionary AssemblyLocationsByFilename = new(); + + public override IList GetPlugins() + { + var loaders = new List(); + foreach (var dll in Directory.GetFiles(Path.GetFullPath(Paths.PluginPath), "*.dll", SearchOption.AllDirectories)) + { + try + { + AssemblyLocationsByFilename.Add(Path.GetFileNameWithoutExtension(dll), Path.GetDirectoryName(dll)); + loaders.Add(new BepInExPluginLoader + { + AssemblyHash = File.GetLastWriteTimeUtc(dll).ToString("O"), + AssemblyIdentifier = dll + }); + } + catch (Exception e) + { + Logger.Log(LogLevel.Error, e); + } + } + + return loaders; + } + + public override Assembly ResolveAssembly(string name) + { + if (!AssemblyLocationsByFilename.TryGetValue(name, out var location)) + return null; + + if (!Utility.TryResolveDllAssembly(new(name), location, out var ass)) + return null; + + return ass; + } +} diff --git a/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs b/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs index ad25d31..d24755b 100644 --- a/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs +++ b/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs @@ -11,6 +11,7 @@ using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Mono.Cecil; +using MonoMod.Utils; namespace BepInEx.Preloader.Core.Patching; @@ -24,6 +25,9 @@ public class AssemblyPatcher : IDisposable private Func assemblyLoader; + private readonly List loaders = new(); + private readonly List providers = new(); + public AssemblyPatcher(Func assemblyLoader) { this.assemblyLoader = assemblyLoader; @@ -61,16 +65,18 @@ public class AssemblyPatcher : IDisposable // Clear to allow GC collection. PatcherContext.PatcherPlugins.Clear(); + + AppDomain.CurrentDomain.AssemblyResolve -= PatcherProvidersAssemblyResolver; } - private PatcherPluginMetadata ToPatcherPlugin(TypeDefinition type, string assemblyPath) + private PatcherPluginMetadata ToPatcherPlugin(TypeDefinition type, IPluginLoader loader, string assemblyPath) { if (type.IsInterface || type.IsAbstract && !type.IsSealed) return null; try { - if (!type.IsSubtypeOf(typeof(BasePatcher))) + if (!type.IsSubtypeOf(typeof(T))) return null; } catch (AssemblyResolutionException) @@ -79,8 +85,13 @@ public class AssemblyPatcher : IDisposable return null; } - var metadata = PatcherPluginInfoAttribute.FromCecilType(type); + if (type.IsSubtypeOf(typeof(BasePatcherProvider))) + return GetPatcherInfo(type, loader, PatcherProviderPluginInfoAttribute.FromCecilType(type)); + return GetPatcherInfo(type, loader, PatcherPluginInfoAttribute.FromCecilType(type)); + } + private PatcherPluginMetadata GetPatcherInfo(TypeDefinition type, IPluginLoader loader, PatcherPluginInfoAttribute metadata) + { // Perform checks that will prevent the plugin from being loaded in ALL cases if (metadata == null) { @@ -109,20 +120,160 @@ public class AssemblyPatcher : IDisposable return new PatcherPluginMetadata { - TypeName = type.FullName + TypeName = type.FullName, + Loader = loader }; } - private bool HasPatcherPlugins(AssemblyDefinition ass) + private bool HasPatcherType(AssemblyDefinition ass) { if (ass.MainModule.AssemblyReferences.All(r => r.Name != CurrentAssemblyName) && ass.Name.Name != CurrentAssemblyName) return false; - if (ass.MainModule.GetTypeReferences().All(r => r.FullName != typeof(BasePatcher).FullName)) + if (ass.MainModule.GetTypeReferences().All(r => r.FullName != typeof(T).FullName)) return false; return true; } + + /// + /// Adds all patchers from all managed assemblies obtained from all patcher providers. + /// + public void AddPatchersFromProviders() + { + var allProviders = TypeLoader.FindPluginTypes(Paths.BepInExAssemblyDirectory, ToPatcherPlugin, HasPatcherType); + var foundProviders = TypeLoader.FindPluginTypes(Paths.PatcherProviderPath, ToPatcherPlugin, HasPatcherType); + allProviders.AddRange(foundProviders); + AppDomain.CurrentDomain.AssemblyResolve += PatcherProvidersAssemblyResolver; + + foreach (var keyValuePair in allProviders) + { + var assemblyPath = keyValuePair.Key; + var patcherCollection = keyValuePair.Value; + + if (patcherCollection.Count == 0) + continue; + + var ass = Assembly.LoadFrom(assemblyPath); + + foreach (var patcherPlugin in patcherCollection) + { + try + { + var type = ass.GetType(patcherPlugin.TypeName); + + var instance = (BasePatcherProvider) Activator.CreateInstance(type); + providers.Add(instance); + loaders.AddRange(instance.GetPatchers()); + } + catch (Exception e) + { + Logger.Log(LogLevel.Error, + $"Failed to load patchers from type [{patcherPlugin.TypeName}]: {(e is ReflectionTypeLoadException re ? TypeLoader.TypeLoadExceptionToString(re) : e.ToString())}"); + } + } + } + + var patchers = TypeLoader.GetPluginsFromLoaders(loaders, ToPatcherPlugin, HasPatcherType); + + var sortedPatchers = new List(); + + // TODO: Add dependency ordering and process attribute filtering + + foreach (var patcherPlugin in patchers) + { + var ass = Assembly.Load(patcherPlugin.Loader.GetAssemblyData()); + var patchDefinitions = LoadPatcherPlugin(ass, patcherPlugin); + sortedPatchers.AddRange(patchDefinitions); + + var assName = ass.GetName(); + Logger.Log(LogLevel.Debug, $"Loaded patcher {patcherPlugin.TypeName} from [{assName.Name} {assName.Version}]"); + } + + PatcherContext.PatchDefinitions.AddRange(sortedPatchers); + Logger.Log(LogLevel.Info, $"Loaded {sortedPatchers.Count} patcher{(sortedPatchers.Count > 0 ? "s" : "")}"); + } + + private Assembly PatcherProvidersAssemblyResolver(object sender, ResolveEventArgs args) + { + foreach (BasePatcherProvider provider in providers) + { + var ass = provider.ResolveAssembly(args.Name); + if (ass != null) + return ass; + } + + return null; + } + + private List LoadPatcherPlugin(Assembly ass, PatcherPluginMetadata patcher) + { + var patchDefinitions = new List(); + try + { + var type = ass.GetType(patcher.TypeName); + + var instance = (BasePatcher) Activator.CreateInstance(type); + instance.Context = PatcherContext; + + PatcherContext.PatcherPlugins.Add(instance); + + var methods = + type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + foreach (var method in methods) + { + var targetAssemblies = MetadataHelper.GetAttributes(method); + var targetTypes = MetadataHelper.GetAttributes(method); + + if (targetAssemblies.Length == 0 && targetTypes.Length == 0) + continue; + + var parameters = method.GetParameters(); + + if (parameters.Length < 1 || parameters.Length > 2 + // Next few lines ensure that the first parameter is AssemblyDefinition and does not have any + // target type attributes, and vice versa + || !( + parameters[0].ParameterType == typeof(AssemblyDefinition) + || parameters[0].ParameterType == + typeof(AssemblyDefinition).MakeByRefType() + && targetTypes.Length == 0 + || parameters[0].ParameterType == typeof(TypeDefinition) + && targetAssemblies.Length == 0 + ) + || parameters.Length == 2 && + parameters[1].ParameterType != typeof(string) + || method.ReturnType != typeof(void) && + method.ReturnType != typeof(bool) + ) + { + Logger + .Log(LogLevel.Warning, + $"Skipping method [{method.FullDescription()}] as it is not a valid patcher method"); + continue; + } + + void AddDefinition(PatchDefinition definition) + { + Logger.Log(LogLevel.Debug, $"Discovered patch [{definition.FullName}]"); + patchDefinitions.Add(definition); + } + + foreach (var targetAssembly in targetAssemblies) + AddDefinition(new PatchDefinition(targetAssembly, instance, method)); + foreach (var targetType in targetTypes) + AddDefinition(new PatchDefinition(targetType, instance, method)); + } + } + catch (Exception e) + { + Logger.Log(LogLevel.Error, + $"Failed to load patchers from type [{patcher.TypeName}]: {(e is ReflectionTypeLoadException re ? TypeLoader.TypeLoadExceptionToString(re) : e.ToString())}"); + } + + return patchDefinitions; + } /// /// Adds all patchers from all managed assemblies specified in a directory. @@ -135,7 +286,7 @@ public class AssemblyPatcher : IDisposable var sortedPatchers = new List(); - var patchers = TypeLoader.FindPluginTypes(directory, ToPatcherPlugin, HasPatcherPlugins); + var patchers = TypeLoader.FindPluginTypes(directory, ToPatcherPlugin, HasPatcherType); // TODO: Add dependency ordering and process attribute filtering @@ -150,68 +301,10 @@ public class AssemblyPatcher : IDisposable var ass = Assembly.LoadFrom(assemblyPath); foreach (var patcherPlugin in patcherCollection) - try - { - var type = ass.GetType(patcherPlugin.TypeName); - - var instance = (BasePatcher) Activator.CreateInstance(type); - instance.Context = PatcherContext; - - PatcherContext.PatcherPlugins.Add(instance); - - var methods = - type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - - foreach (var method in methods) - { - var targetAssemblies = MetadataHelper.GetAttributes(method); - var targetTypes = MetadataHelper.GetAttributes(method); - - if (targetAssemblies.Length == 0 && targetTypes.Length == 0) - continue; - - var parameters = method.GetParameters(); - - if (parameters.Length < 1 || parameters.Length > 2 - // Next few lines ensure that the first parameter is AssemblyDefinition and does not have any - // target type attributes, and vice versa - || !( - parameters[0].ParameterType == typeof(AssemblyDefinition) - || parameters[0].ParameterType == - typeof(AssemblyDefinition).MakeByRefType() - && targetTypes.Length == 0 - || parameters[0].ParameterType == typeof(TypeDefinition) - && targetAssemblies.Length == 0 - ) - || parameters.Length == 2 && - parameters[1].ParameterType != typeof(string) - || method.ReturnType != typeof(void) && - method.ReturnType != typeof(bool) - ) - { - Logger - .Log(LogLevel.Warning, - $"Skipping method [{method.FullDescription()}] as it is not a valid patcher method"); - continue; - } - - void AddDefinition(PatchDefinition definition) - { - Logger.Log(LogLevel.Debug, $"Discovered patch [{definition.FullName}]"); - sortedPatchers.Add(definition); - } - - foreach (var targetAssembly in targetAssemblies) - AddDefinition(new PatchDefinition(targetAssembly, instance, method)); - foreach (var targetType in targetTypes) - AddDefinition(new PatchDefinition(targetType, instance, method)); - } - } - catch (Exception e) - { - Logger.Log(LogLevel.Error, - $"Failed to load patchers from type [{patcherPlugin.TypeName}]: {(e is ReflectionTypeLoadException re ? TypeLoader.TypeLoadExceptionToString(re) : e.ToString())}"); - } + { + var patchDefinitions = LoadPatcherPlugin(ass, patcherPlugin); + sortedPatchers.AddRange(patchDefinitions); + } var assName = ass.GetName(); Logger.Log(patcherCollection.Any() ? LogLevel.Info : LogLevel.Debug, diff --git a/BepInEx.Preloader.Core/Patching/Attributes.cs b/BepInEx.Preloader.Core/Patching/Attributes.cs index 95b21c4..a6e5675 100644 --- a/BepInEx.Preloader.Core/Patching/Attributes.cs +++ b/BepInEx.Preloader.Core/Patching/Attributes.cs @@ -79,6 +79,31 @@ public class PatcherPluginInfoAttribute : Attribute } } +/// +/// This attribute denotes that a class is a patcher plugin provider, and specifies the required metadata. +/// +[AttributeUsage(AttributeTargets.Class)] +public class PatcherProviderPluginInfoAttribute : PatcherPluginInfoAttribute { + /// The unique identifier of the plugin. Should not change between plugin versions. + /// The user friendly name of the plugin. Is able to be changed between versions. + /// The specific version of the plugin. + public PatcherProviderPluginInfoAttribute(string GUID, string Name, string Version) : base(GUID, Name, Version) + { + } + + internal static PatcherProviderPluginInfoAttribute FromCecilType(TypeDefinition td) + { + var attr = MetadataHelper.GetCustomAttributes(td, false).FirstOrDefault(); + + if (attr == null) + return null; + + return new PatcherProviderPluginInfoAttribute((string) attr.ConstructorArguments[0].Value, + (string) attr.ConstructorArguments[1].Value, + (string) attr.ConstructorArguments[2].Value); + } +} + /// /// Defines an assembly that a patch method will target. /// diff --git a/BepInEx.Preloader.Core/Patching/BasePatcherProvider.cs b/BepInEx.Preloader.Core/Patching/BasePatcherProvider.cs new file mode 100644 index 0000000..c1ef817 --- /dev/null +++ b/BepInEx.Preloader.Core/Patching/BasePatcherProvider.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Reflection; +using BepInEx.Configuration; +using BepInEx.Logging; + +namespace BepInEx.Preloader.Core.Patching; + +/// +/// A patcher provider that can discover and load assemblies containing patcher plugins +/// +public abstract class BasePatcherProvider +{ + protected BasePatcherProvider() + { + Info = PatcherPluginInfoAttribute.FromType(GetType()); + + Log = Logger.CreateLogSource(Info.Name); + + Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, Info.GUID + ".cfg"), false, + new BepInPlugin(Info.GUID, Info.Name, Info.Version.ToString())); + } + + /// + /// A instance created for use by this patcher plugin provider. + /// + public ManualLogSource Log { get; } + + /// + /// A configuration file binding created with the of this provider as the + /// filename. + /// + public ConfigFile Config { get; } + + /// + /// Metadata associated with this patcher plugin provider. + /// + public PatcherPluginInfoAttribute Info { get; } + + /// + /// Obtains a list of assemblies containing patchers to load + /// + /// A list of loaders, one per assembly + public abstract IList GetPatchers(); + + /// + /// A custom assembly resolver that can be used by this provider to resolve assemblies + /// that have failed to resolve + /// + /// The assembly's name + /// The resolved assembly or null if the assembly couldn't be resolved + public virtual Assembly ResolveAssembly(string name) + { + return null; + } +} diff --git a/BepInEx.Preloader.Core/Patching/PatcherPluginMetadata.cs b/BepInEx.Preloader.Core/Patching/PatcherPluginMetadata.cs index 6994dea..9d8b963 100644 --- a/BepInEx.Preloader.Core/Patching/PatcherPluginMetadata.cs +++ b/BepInEx.Preloader.Core/Patching/PatcherPluginMetadata.cs @@ -13,6 +13,11 @@ internal class PatcherPluginMetadata : ICacheable /// public string TypeName { get; set; } = string.Empty; + /// + /// The loader used to load this patcher, null if it is a provider + /// + public IPluginLoader Loader { get; set; } + /// public void Save(BinaryWriter bw) => bw.Write(TypeName); diff --git a/BepInEx.sln b/BepInEx.sln index 356b05d..e723eb1 100644 --- a/BepInEx.sln +++ b/BepInEx.sln @@ -51,6 +51,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Doorstop", "Doorstop", "{CE EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Build", "build\Build.csproj", "{0C38DDF2-EAAA-43FA-BA0A-985636AC6040}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BepInEx.PluginProvider", "BepInEx.PluginProvider\BepInEx.PluginProvider.csproj", "{CBE495BC-0258-4F36-8B3D-1CA64D97E90A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BepInEx.PatcherProvider", "BepInEx.PatcherProvider\BepInEx.PatcherProvider.csproj", "{F88995B3-A5A2-4FC1-A11B-E2AA17483B20}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution BepInEx.NetLauncher.Shared\BepInEx.NetLauncher.Shared.projitems*{067bcc8c-fc25-4929-b4f4-c6a5a7496994}*SharedItemsImports = 13 @@ -147,6 +151,22 @@ Global {0C38DDF2-EAAA-43FA-BA0A-985636AC6040}.Release_Unity|Any CPU.ActiveCfg = Debug|Any CPU {0C38DDF2-EAAA-43FA-BA0A-985636AC6040}.Release_Unity|Any CPU.Build.0 = Debug|Any CPU {0C38DDF2-EAAA-43FA-BA0A-985636AC6040}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CBE495BC-0258-4F36-8B3D-1CA64D97E90A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CBE495BC-0258-4F36-8B3D-1CA64D97E90A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CBE495BC-0258-4F36-8B3D-1CA64D97E90A}.Release_NetFramework|Any CPU.ActiveCfg = Debug|Any CPU + {CBE495BC-0258-4F36-8B3D-1CA64D97E90A}.Release_NetFramework|Any CPU.Build.0 = Debug|Any CPU + {CBE495BC-0258-4F36-8B3D-1CA64D97E90A}.Release_Unity|Any CPU.ActiveCfg = Debug|Any CPU + {CBE495BC-0258-4F36-8B3D-1CA64D97E90A}.Release_Unity|Any CPU.Build.0 = Debug|Any CPU + {CBE495BC-0258-4F36-8B3D-1CA64D97E90A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CBE495BC-0258-4F36-8B3D-1CA64D97E90A}.Release|Any CPU.Build.0 = Release|Any CPU + {F88995B3-A5A2-4FC1-A11B-E2AA17483B20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F88995B3-A5A2-4FC1-A11B-E2AA17483B20}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F88995B3-A5A2-4FC1-A11B-E2AA17483B20}.Release_NetFramework|Any CPU.ActiveCfg = Debug|Any CPU + {F88995B3-A5A2-4FC1-A11B-E2AA17483B20}.Release_NetFramework|Any CPU.Build.0 = Debug|Any CPU + {F88995B3-A5A2-4FC1-A11B-E2AA17483B20}.Release_Unity|Any CPU.ActiveCfg = Debug|Any CPU + {F88995B3-A5A2-4FC1-A11B-E2AA17483B20}.Release_Unity|Any CPU.Build.0 = Debug|Any CPU + {F88995B3-A5A2-4FC1-A11B-E2AA17483B20}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F88995B3-A5A2-4FC1-A11B-E2AA17483B20}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs b/Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs index 5d40b10..dfc863b 100644 --- a/Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs +++ b/Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs @@ -8,7 +8,7 @@ namespace BepInEx.NET.Common { protected BasePlugin() { - var metadata = MetadataHelper.GetMetadata(this); + var metadata = MetadataHelper.GetPluginMetadata(this); HarmonyInstance = new Harmony("BepInEx.Plugin." + metadata.GUID); diff --git a/Runtimes/NET/BepInEx.NET.Common/BepInEx.NET.Common.csproj b/Runtimes/NET/BepInEx.NET.Common/BepInEx.NET.Common.csproj index 6884e40..168fc2b 100644 --- a/Runtimes/NET/BepInEx.NET.Common/BepInEx.NET.Common.csproj +++ b/Runtimes/NET/BepInEx.NET.Common/BepInEx.NET.Common.csproj @@ -12,6 +12,18 @@ + + false + Content + PreserveNewest + Build;DocumentationProjectOutputGroup + + + false + Content + PreserveNewest + Build;DocumentationProjectOutputGroup + diff --git a/Runtimes/NET/BepInEx.NET.CoreCLR/NetCorePreloader.cs b/Runtimes/NET/BepInEx.NET.CoreCLR/NetCorePreloader.cs index 90e7aa7..3448cdb 100644 --- a/Runtimes/NET/BepInEx.NET.CoreCLR/NetCorePreloader.cs +++ b/Runtimes/NET/BepInEx.NET.CoreCLR/NetCorePreloader.cs @@ -54,7 +54,7 @@ namespace BepInEx.NET.CoreCLR using (var assemblyPatcher = new AssemblyPatcher((data, _) => Assembly.Load(data))) { - assemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath); + assemblyPatcher.AddPatchersFromProviders(); Log.LogInfo($"{assemblyPatcher.PatcherContext.PatchDefinitions.Count} patcher definition(s) loaded"); diff --git a/Runtimes/NET/BepInEx.NET.Framework.Launcher/NetPreloader.cs b/Runtimes/NET/BepInEx.NET.Framework.Launcher/NetPreloader.cs index f7396bb..51fbe08 100644 --- a/Runtimes/NET/BepInEx.NET.Framework.Launcher/NetPreloader.cs +++ b/Runtimes/NET/BepInEx.NET.Framework.Launcher/NetPreloader.cs @@ -110,7 +110,7 @@ public static class NetPreloader using (var assemblyPatcher = new AssemblyPatcher((data, _) => Assembly.Load(data))) { - assemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath); + assemblyPatcher.AddPatchersFromProviders(); Log.Log(LogLevel.Info, $"{assemblyPatcher.PatcherContext.PatchDefinitions.Count} patcher definition(s) loaded"); diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs index 3b51e33..3476bde 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs @@ -8,7 +8,7 @@ public abstract class BasePlugin { protected BasePlugin() { - var metadata = MetadataHelper.GetMetadata(this); + var metadata = MetadataHelper.GetPluginMetadata(this); Log = Logger.CreateLogSource(metadata.Name); diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj index 32eb297..2698124 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj @@ -9,6 +9,18 @@ + + false + Content + PreserveNewest + Build;DocumentationProjectOutputGroup + + + false + Content + PreserveNewest + Build;DocumentationProjectOutputGroup + diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/Preloader.cs b/Runtimes/Unity/BepInEx.Unity.IL2CPP/Preloader.cs index 690d4ef..0f4f52e 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/Preloader.cs +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/Preloader.cs @@ -67,7 +67,7 @@ public static class Preloader using (var assemblyPatcher = new AssemblyPatcher((data, _) => Assembly.Load(data))) { - assemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath); + assemblyPatcher.AddPatchersFromProviders(); Log.LogInfo($"{assemblyPatcher.PatcherContext.PatcherPlugins.Count} patcher plugin{(assemblyPatcher.PatcherContext.PatcherPlugins.Count == 1 ? "" : "s")} loaded"); diff --git a/Runtimes/Unity/BepInEx.Unity.Mono.Preloader/UnityPreloader.cs b/Runtimes/Unity/BepInEx.Unity.Mono.Preloader/UnityPreloader.cs index 6cc3b33..b6b2a42 100644 --- a/Runtimes/Unity/BepInEx.Unity.Mono.Preloader/UnityPreloader.cs +++ b/Runtimes/Unity/BepInEx.Unity.Mono.Preloader/UnityPreloader.cs @@ -77,7 +77,7 @@ internal static class UnityPreloader using (var assemblyPatcher = new AssemblyPatcher(MonoAssemblyHelper.LoadFromMemory)) { assemblyPatcher.AddPatchersFromDirectory(Paths.BepInExAssemblyDirectory); - assemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath); + assemblyPatcher.AddPatchersFromProviders(); Log.Log(LogLevel.Info, $"{assemblyPatcher.PatcherContext.PatcherPlugins.Count} patcher plugin{(assemblyPatcher.PatcherContext.PatcherPlugins.Count == 1 ? "" : "s")} loaded"); diff --git a/Runtimes/Unity/BepInEx.Unity.Mono/BaseUnityPlugin.cs b/Runtimes/Unity/BepInEx.Unity.Mono/BaseUnityPlugin.cs index 4043771..09e9c07 100644 --- a/Runtimes/Unity/BepInEx.Unity.Mono/BaseUnityPlugin.cs +++ b/Runtimes/Unity/BepInEx.Unity.Mono/BaseUnityPlugin.cs @@ -16,7 +16,7 @@ public abstract class BaseUnityPlugin : MonoBehaviour /// BepInPlugin attribute is missing. protected BaseUnityPlugin() { - var metadata = MetadataHelper.GetMetadata(this); + var metadata = MetadataHelper.GetPluginMetadata(this); if (metadata == null) throw new InvalidOperationException("Can't create an instance of " + GetType().FullName + " because it inherits from BaseUnityPlugin and the BepInPlugin attribute is missing."); @@ -27,7 +27,6 @@ public abstract class BaseUnityPlugin : MonoBehaviour Instance = this, Dependencies = MetadataHelper.GetDependencies(GetType()), Processes = MetadataHelper.GetAttributes(GetType()), - Location = GetType().Assembly.Location }; Logger = BepInEx.Logging.Logger.CreateLogSource(metadata.Name); diff --git a/Runtimes/Unity/BepInEx.Unity.Mono/BepInEx.Unity.Mono.csproj b/Runtimes/Unity/BepInEx.Unity.Mono/BepInEx.Unity.Mono.csproj index bf4385f..71af84f 100644 --- a/Runtimes/Unity/BepInEx.Unity.Mono/BepInEx.Unity.Mono.csproj +++ b/Runtimes/Unity/BepInEx.Unity.Mono/BepInEx.Unity.Mono.csproj @@ -11,6 +11,18 @@ + + false + Content + PreserveNewest + Build;DocumentationProjectOutputGroup + + + false + Content + PreserveNewest + Build;DocumentationProjectOutputGroup + diff --git a/build/Program.cs b/build/Program.cs index 0ba5f0a..ab63c15 100644 --- a/build/Program.cs +++ b/build/Program.cs @@ -239,8 +239,10 @@ public sealed class MakeDistTask : FrostingTask var bepInExCoreDir = bepInExDir.Combine("core"); ctx.CreateDirectory(bepInExDir); ctx.CreateDirectory(bepInExCoreDir); + ctx.CreateDirectory(bepInExDir.Combine("plugins-providers")); ctx.CreateDirectory(bepInExDir.Combine("plugins")); ctx.CreateDirectory(bepInExDir.Combine("patchers")); + ctx.CreateDirectory(bepInExDir.Combine("patchers-providers")); File.WriteAllText(targetDir.CombineWithFilePath("changelog.txt").FullPath, changelog); From 4d47c3cd39d55adb8edf57410394b8e6ba475afa Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Tue, 20 Aug 2024 15:13:10 -0400 Subject: [PATCH 07/65] Address code review --- BepInEx.Core/Bootstrap/BaseChainloader.cs | 45 +++++++++++++------ BepInEx.Core/Bootstrap/TypeLoader.cs | 36 +++++++-------- BepInEx.Core/Contract/Attributes.cs | 2 +- BepInEx.Core/Contract/BasePluginProvider.cs | 4 +- ...IPluginLoader.cs => IPluginLoadContext.cs} | 4 +- BepInEx.Core/Contract/PluginInfo.cs | 8 ++-- ...Loader.cs => BepInExPatcherLoadContext.cs} | 2 +- .../BepInExPatcherProvider.cs | 8 ++-- ...nLoader.cs => BepInExPluginLoadContext.cs} | 2 +- .../BepInExPluginProvider.cs | 8 ++-- .../Patching/AssemblyPatcher.cs | 37 +++++++++------ BepInEx.Preloader.Core/Patching/Attributes.cs | 2 +- .../Patching/BasePatcherProvider.cs | 4 +- .../Patching/PatcherPluginMetadata.cs | 4 +- 14 files changed, 97 insertions(+), 69 deletions(-) rename BepInEx.Core/Contract/{IPluginLoader.cs => IPluginLoadContext.cs} (91%) rename BepInEx.PatcherProvider/{BepInExPatcherLoader.cs => BepInExPatcherLoadContext.cs} (87%) rename BepInEx.PluginProvider/{BepInExPluginLoader.cs => BepInExPluginLoadContext.cs} (87%) diff --git a/BepInEx.Core/Bootstrap/BaseChainloader.cs b/BepInEx.Core/Bootstrap/BaseChainloader.cs index 9489e93..a025b21 100644 --- a/BepInEx.Core/Bootstrap/BaseChainloader.cs +++ b/BepInEx.Core/Bootstrap/BaseChainloader.cs @@ -23,9 +23,10 @@ public abstract class BaseChainloader /// Analyzes the given type definition and attempts to convert it to a valid /// /// Type definition to analyze. + /// The load context of the plugin /// The filepath of the assembly, to keep as metadata. /// If the type represent a valid plugin, returns a instance. Otherwise, return null. - public static PluginInfo ToPluginInfo(TypeDefinition type, IPluginLoader loader, string assemblyLocation) + public static PluginInfo ToPluginInfo(TypeDefinition type, IPluginLoadContext loadContext, string assemblyLocation) { if (type.IsInterface || type.IsAbstract) return null; @@ -42,11 +43,11 @@ public abstract class BaseChainloader } if (type.IsSubtypeOf(typeof(BasePluginProvider))) - return GetPluginInfo(type, BepInPluginProvider.FromCecilType(type), loader, assemblyLocation); - return GetPluginInfo(type, BepInPlugin.FromCecilType(type), loader, assemblyLocation); + return GetPluginInfo(type, BepInPluginProvider.FromCecilType(type), loadContext, assemblyLocation); + return GetPluginInfo(type, BepInPlugin.FromCecilType(type), loadContext, assemblyLocation); } - private static PluginInfo GetPluginInfo(TypeDefinition type, BepInPlugin metadata, IPluginLoader loader, string assemblyLocation) + private static PluginInfo GetPluginInfo(TypeDefinition type, BepInPlugin metadata, IPluginLoadContext loadContext, string assemblyLocation) { // Perform checks that will prevent the plugin from being loaded in ALL cases if (metadata == null) @@ -91,7 +92,7 @@ public abstract class BaseChainloader TypeName = type.FullName, TargettedBepInExVersion = bepinVersion, Location = assemblyLocation, - Loader = loader + LoadContext = loadContext }; } @@ -123,18 +124,22 @@ public abstract class BaseChainloader /// /// List of all instances of all plugins loaded via the chainloader. + /// If a plugin fails to load, it is removed. This list only contains the plugins that were actually + /// loaded and have their dependencies satisfied. /// public Dictionary Plugins { get; } = new(); /// /// List of all instances of all providers loaded via the chainloader. + /// If a provider fails to load, it is removed. This list only contains the providers that were actually + /// loaded and have their dependencies satisfied. /// public Dictionary Providers { get; } = new(); /// - /// List of all discovered from all the providers + /// List of all discovered from all the providers /// - public List Loaders { get; } = new(); + public List LoadContexts { get; } = new(); /// /// Collection of error chainloader messages that occured during plugin loading. @@ -142,6 +147,16 @@ public abstract class BaseChainloader /// public List DependencyErrors { get; } = new(); + /// + /// Occurs after a plugin provider is loaded. + /// + public event Action ProviderLoaded; + + /// + /// Occurs after all plugins providers are loaded. + /// + public event Action FinishedProviders; + /// /// Occurs after a plugin is loaded. /// @@ -323,12 +338,13 @@ public abstract class BaseChainloader var builtinProviders = DiscoverPluginProvidersFrom(Paths.BepInExAssemblyDirectory); var providers = DiscoverPluginProvidersFrom(Paths.PluginProviderPath); Logger.Log(LogLevel.Info, $"{providers.Count + builtinProviders.Count} " + - $"meta plugin{(providers.Count + builtinProviders.Count == 1 ? "" : "s")} to load"); + $"plugins provider{(providers.Count + builtinProviders.Count == 1 ? "" : "s")} to load"); LoadPlugins(builtinProviders, provider: true); LoadPlugins(providers, provider: true); + FinishedProviders?.Invoke(); - var plugins = TypeLoader.GetPluginsFromLoaders(Loaders, ToPluginInfo, HasBepinPluginType, "chainloader"); + var plugins = TypeLoader.GetPluginsFromLoadContexts(LoadContexts, ToPluginInfo, HasBepinPluginType, "chainloader"); Logger.Log(LogLevel.Info, $"{plugins.Count} plugin{(plugins.Count == 1 ? "" : "s")} to load"); LoadPlugins(plugins, provider: false); Finished?.Invoke(); @@ -373,10 +389,10 @@ public abstract class BaseChainloader ass = Assembly.LoadFrom(plugin.Location); loadedAssemblies[plugin.Location] = ass; } - else if (!provider && !loadedAssemblies.TryGetValue(plugin.Loader.AssemblyIdentifier, out ass)) + else if (!provider && !loadedAssemblies.TryGetValue(plugin.LoadContext.AssemblyIdentifier, out ass)) { - ass = Assembly.Load(plugin.Loader.GetAssemblyData()); - loadedAssemblies[plugin.Loader.AssemblyIdentifier] = ass; + ass = Assembly.Load(plugin.LoadContext.GetAssemblyData()); + loadedAssemblies[plugin.LoadContext.AssemblyIdentifier] = ass; } existingPlugins[plugin.Metadata.GUID] = plugin; @@ -388,13 +404,14 @@ public abstract class BaseChainloader plugin.Instance = Activator.CreateInstance(type); AppDomain.CurrentDomain.AssemblyResolve += (_, args) => ((BasePluginProvider)plugin.Instance).ResolveAssembly(args.Name); var requestedPluginsStream = ((BasePluginProvider)plugin.Instance).GetPlugins(); - Loaders.AddRange(requestedPluginsStream); + LoadContexts.AddRange(requestedPluginsStream); + ProviderLoaded?.Invoke(plugin); } else { plugin.Instance = LoadPlugin(plugin, ass); + PluginLoaded?.Invoke(plugin); } - PluginLoaded?.Invoke(plugin); } catch (Exception ex) { diff --git a/BepInEx.Core/Bootstrap/TypeLoader.cs b/BepInEx.Core/Bootstrap/TypeLoader.cs index 111ac38..4cecee8 100644 --- a/BepInEx.Core/Bootstrap/TypeLoader.cs +++ b/BepInEx.Core/Bootstrap/TypeLoader.cs @@ -128,7 +128,7 @@ public static class TypeLoader /// selector. /// public static Dictionary> FindPluginTypes(string directory, - Func typeSelector, + Func typeSelector, Func assemblyFilter = null, string cacheName = null) where T : ICacheable, new() @@ -176,7 +176,7 @@ public static class TypeLoader /// Looks up assemblies using the given loaders and locates all types that can be loaded and collects their metadata. /// /// The specific base type to search for. - /// The loaders to obtain the assemblies from. + /// The load contexts to obtain the assemblies from. /// 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. @@ -184,10 +184,10 @@ public static class TypeLoader /// A dictionary of all assemblies in the directory and the list of type metadatas of types that match the /// selector. /// - public static List GetPluginsFromLoaders(IList loaders, - Func typeSelector, - Func assemblyFilter = null, - string cacheName = null) + public static List GetPluginsFromLoadContexts(IList loadContexts, + Func typeSelector, + Func assemblyFilter = null, + string cacheName = null) where T : ICacheable, new() { var result = new Dictionary>(); @@ -197,27 +197,27 @@ public static class TypeLoader if (cacheName != null) cache = LoadAssemblyCache(cacheName); - foreach (IPluginLoader pluginLoader in loaders) + foreach (IPluginLoadContext loadContext in loadContexts) { IList plugins; - if (cache != null && pluginLoader.AssemblyHash != null && - cache.TryGetValue(pluginLoader.AssemblyIdentifier, out var cacheEntry) && - pluginLoader.AssemblyHash == cacheEntry.Hash) + if (cache != null && loadContext.AssemblyHash != null && + cache.TryGetValue(loadContext.AssemblyIdentifier, out var cacheEntry) && + loadContext.AssemblyHash == cacheEntry.Hash) { plugins = cacheEntry.CacheItems; } else { - var assemblyData = pluginLoader.GetAssemblyData(); + var assemblyData = loadContext.GetAssemblyData(); using var memory = new MemoryStream(assemblyData); - plugins = ExamineStream(typeSelector, assemblyFilter, memory, pluginLoader, null); + plugins = ExamineStream(typeSelector, assemblyFilter, memory, loadContext, null); } foreach (T pluginInfo in plugins) { - if (!result.ContainsKey(pluginLoader.AssemblyIdentifier)) - result[pluginLoader.AssemblyIdentifier] = new(); - result[pluginLoader.AssemblyIdentifier].Add(pluginInfo); + if (!result.ContainsKey(loadContext.AssemblyIdentifier)) + result[loadContext.AssemblyIdentifier] = new(); + result[loadContext.AssemblyIdentifier].Add(pluginInfo); } } @@ -227,10 +227,10 @@ public static class TypeLoader return result.SelectMany(x => x.Value).ToList(); } - private static List ExamineStream(Func typeSelector, + private static List ExamineStream(Func typeSelector, Func assemblyFilter, MemoryStream dllMs, - IPluginLoader loader, + IPluginLoadContext loadContext, string location) where T : ICacheable, new() { @@ -243,7 +243,7 @@ public static class TypeLoader } var matches = ass.MainModule.Types - .Select(t => typeSelector(t, loader, location)) + .Select(t => typeSelector(t, loadContext, location)) .Where(t => t != null).ToList(); return matches; } diff --git a/BepInEx.Core/Contract/Attributes.cs b/BepInEx.Core/Contract/Attributes.cs index 978c170..f9d495c 100644 --- a/BepInEx.Core/Contract/Attributes.cs +++ b/BepInEx.Core/Contract/Attributes.cs @@ -89,7 +89,7 @@ public class BepInPluginProvider : BepInPlugin { } - internal static BepInPluginProvider FromCecilType(TypeDefinition td) + internal new static BepInPluginProvider FromCecilType(TypeDefinition td) { var attr = MetadataHelper.GetCustomAttributes(td, false).FirstOrDefault(); diff --git a/BepInEx.Core/Contract/BasePluginProvider.cs b/BepInEx.Core/Contract/BasePluginProvider.cs index 202bd3a..594044a 100644 --- a/BepInEx.Core/Contract/BasePluginProvider.cs +++ b/BepInEx.Core/Contract/BasePluginProvider.cs @@ -55,8 +55,8 @@ public abstract class BasePluginProvider /// /// Obtains a list of assemblies containing plugins to load /// - /// A list of loaders, one per assembly - public abstract IList GetPlugins(); + /// A list of load context, one per assembly + public abstract IList GetPlugins(); /// /// A custom assembly resolver that can be used by this provider to resolve assemblies diff --git a/BepInEx.Core/Contract/IPluginLoader.cs b/BepInEx.Core/Contract/IPluginLoadContext.cs similarity index 91% rename from BepInEx.Core/Contract/IPluginLoader.cs rename to BepInEx.Core/Contract/IPluginLoadContext.cs index f98c937..c17fc41 100644 --- a/BepInEx.Core/Contract/IPluginLoader.cs +++ b/BepInEx.Core/Contract/IPluginLoadContext.cs @@ -2,9 +2,9 @@ /// /// An interface that allows to dynamically load and track an assembly -/// which can be obtained from a provided +/// which can be obtained from a provider /// -public interface IPluginLoader +public interface IPluginLoadContext { /// /// An identifier that uniquely identifies an assembly from a provider no matter its revision diff --git a/BepInEx.Core/Contract/PluginInfo.cs b/BepInEx.Core/Contract/PluginInfo.cs index f4d33ca..15e2729 100644 --- a/BepInEx.Core/Contract/PluginInfo.cs +++ b/BepInEx.Core/Contract/PluginInfo.cs @@ -18,9 +18,9 @@ public class PluginInfo : ICacheable public BepInPlugin Metadata { get; internal set; } /// - /// The loader used to load this plugin, null if it is a provider + /// The load context used to load this plugin, null if it is a provider /// - public IPluginLoader Loader { get; internal set; } + public IPluginLoadContext LoadContext { get; internal set; } /// /// Collection of attributes that describe what processes the plugin can run on. @@ -52,7 +52,7 @@ public class PluginInfo : ICacheable /// public override string ToString() => $"{Metadata?.Name} {Metadata?.Version}"; - public virtual void Save(BinaryWriter bw) + void ICacheable.Save(BinaryWriter bw) { bw.Write(TypeName); @@ -78,7 +78,7 @@ public class PluginInfo : ICacheable bw.Write(TargettedBepInExVersion.ToString(4)); } - public virtual void Load(BinaryReader br) + void ICacheable.Load(BinaryReader br) { TypeName = br.ReadString(); diff --git a/BepInEx.PatcherProvider/BepInExPatcherLoader.cs b/BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs similarity index 87% rename from BepInEx.PatcherProvider/BepInExPatcherLoader.cs rename to BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs index 0221434..590678c 100644 --- a/BepInEx.PatcherProvider/BepInExPatcherLoader.cs +++ b/BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs @@ -2,7 +2,7 @@ namespace BepInEx.PatcherProvider; -public class BepInExPatcherLoader : IPluginLoader +public class BepInExPatcherLoadContext : IPluginLoadContext { public string AssemblyIdentifier { get; internal set; } public string AssemblyHash { get; internal set; } diff --git a/BepInEx.PatcherProvider/BepInExPatcherProvider.cs b/BepInEx.PatcherProvider/BepInExPatcherProvider.cs index 24ef3cc..fb2ff8c 100644 --- a/BepInEx.PatcherProvider/BepInExPatcherProvider.cs +++ b/BepInEx.PatcherProvider/BepInExPatcherProvider.cs @@ -12,15 +12,15 @@ public class BepInExPatcherProvider : BasePatcherProvider { private static readonly Dictionary AssemblyLocationsByFilename = new(); - public override IList GetPatchers() + public override IList GetPatchers() { - var loaders = new List(); + var loadContexts = new List(); foreach (var dll in Directory.GetFiles(Path.GetFullPath(Paths.PatcherPluginPath), "*.dll", SearchOption.AllDirectories)) { try { AssemblyLocationsByFilename.Add(Path.GetFileNameWithoutExtension(dll), Path.GetDirectoryName(dll)); - loaders.Add(new BepInExPatcherLoader + loadContexts.Add(new BepInExPatcherLoadContext { AssemblyHash = File.GetLastWriteTimeUtc(dll).ToString("O"), AssemblyIdentifier = dll @@ -32,7 +32,7 @@ public class BepInExPatcherProvider : BasePatcherProvider } } - return loaders; + return loadContexts; } public override Assembly ResolveAssembly(string name) diff --git a/BepInEx.PluginProvider/BepInExPluginLoader.cs b/BepInEx.PluginProvider/BepInExPluginLoadContext.cs similarity index 87% rename from BepInEx.PluginProvider/BepInExPluginLoader.cs rename to BepInEx.PluginProvider/BepInExPluginLoadContext.cs index a71f3d7..a39f649 100644 --- a/BepInEx.PluginProvider/BepInExPluginLoader.cs +++ b/BepInEx.PluginProvider/BepInExPluginLoadContext.cs @@ -2,7 +2,7 @@ namespace BepInEx.PluginProvider; -public class BepInExPluginLoader : IPluginLoader +public class BepInExPluginLoadContext : IPluginLoadContext { public string AssemblyIdentifier { get; internal set; } public string AssemblyHash { get; internal set; } diff --git a/BepInEx.PluginProvider/BepInExPluginProvider.cs b/BepInEx.PluginProvider/BepInExPluginProvider.cs index 569b304..b2f151c 100644 --- a/BepInEx.PluginProvider/BepInExPluginProvider.cs +++ b/BepInEx.PluginProvider/BepInExPluginProvider.cs @@ -11,15 +11,15 @@ public class BepInExPluginProvider : BasePluginProvider { private static readonly Dictionary AssemblyLocationsByFilename = new(); - public override IList GetPlugins() + public override IList GetPlugins() { - var loaders = new List(); + var loadContexts = new List(); foreach (var dll in Directory.GetFiles(Path.GetFullPath(Paths.PluginPath), "*.dll", SearchOption.AllDirectories)) { try { AssemblyLocationsByFilename.Add(Path.GetFileNameWithoutExtension(dll), Path.GetDirectoryName(dll)); - loaders.Add(new BepInExPluginLoader + loadContexts.Add(new BepInExPluginLoadContext { AssemblyHash = File.GetLastWriteTimeUtc(dll).ToString("O"), AssemblyIdentifier = dll @@ -31,7 +31,7 @@ public class BepInExPluginProvider : BasePluginProvider } } - return loaders; + return loadContexts; } public override Assembly ResolveAssembly(string name) diff --git a/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs b/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs index d24755b..50cff04 100644 --- a/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs +++ b/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs @@ -25,7 +25,7 @@ public class AssemblyPatcher : IDisposable private Func assemblyLoader; - private readonly List loaders = new(); + private readonly List loadContexts = new(); private readonly List providers = new(); public AssemblyPatcher(Func assemblyLoader) @@ -69,7 +69,7 @@ public class AssemblyPatcher : IDisposable AppDomain.CurrentDomain.AssemblyResolve -= PatcherProvidersAssemblyResolver; } - private PatcherPluginMetadata ToPatcherPlugin(TypeDefinition type, IPluginLoader loader, string assemblyPath) + private PatcherPluginMetadata ToPatcherPlugin(TypeDefinition type, IPluginLoadContext loadContext, string assemblyPath) { if (type.IsInterface || type.IsAbstract && !type.IsSealed) return null; @@ -86,11 +86,11 @@ public class AssemblyPatcher : IDisposable } if (type.IsSubtypeOf(typeof(BasePatcherProvider))) - return GetPatcherInfo(type, loader, PatcherProviderPluginInfoAttribute.FromCecilType(type)); - return GetPatcherInfo(type, loader, PatcherPluginInfoAttribute.FromCecilType(type)); + return GetPatcherInfo(type, loadContext, PatcherProviderPluginInfoAttribute.FromCecilType(type)); + return GetPatcherInfo(type, loadContext, PatcherPluginInfoAttribute.FromCecilType(type)); } - private PatcherPluginMetadata GetPatcherInfo(TypeDefinition type, IPluginLoader loader, PatcherPluginInfoAttribute metadata) + private PatcherPluginMetadata GetPatcherInfo(TypeDefinition type, IPluginLoadContext loadContext, PatcherPluginInfoAttribute metadata) { // Perform checks that will prevent the plugin from being loaded in ALL cases if (metadata == null) @@ -121,7 +121,7 @@ public class AssemblyPatcher : IDisposable return new PatcherPluginMetadata { TypeName = type.FullName, - Loader = loader + LoadContext = loadContext }; } @@ -164,7 +164,7 @@ public class AssemblyPatcher : IDisposable var instance = (BasePatcherProvider) Activator.CreateInstance(type); providers.Add(instance); - loaders.AddRange(instance.GetPatchers()); + loadContexts.AddRange(instance.GetPatchers()); } catch (Exception e) { @@ -174,7 +174,7 @@ public class AssemblyPatcher : IDisposable } } - var patchers = TypeLoader.GetPluginsFromLoaders(loaders, ToPatcherPlugin, HasPatcherType); + var patchers = TypeLoader.GetPluginsFromLoadContexts(loadContexts, ToPatcherPlugin, HasPatcherType); var sortedPatchers = new List(); @@ -182,7 +182,7 @@ public class AssemblyPatcher : IDisposable foreach (var patcherPlugin in patchers) { - var ass = Assembly.Load(patcherPlugin.Loader.GetAssemblyData()); + var ass = Assembly.Load(patcherPlugin.LoadContext.GetAssemblyData()); var patchDefinitions = LoadPatcherPlugin(ass, patcherPlugin); sortedPatchers.AddRange(patchDefinitions); @@ -196,11 +196,22 @@ public class AssemblyPatcher : IDisposable private Assembly PatcherProvidersAssemblyResolver(object sender, ResolveEventArgs args) { - foreach (BasePatcherProvider provider in providers) + BasePatcherProvider currentProvider = null; + try { - var ass = provider.ResolveAssembly(args.Name); - if (ass != null) - return ass; + foreach (BasePatcherProvider provider in providers) + { + currentProvider = provider; + var ass = provider.ResolveAssembly(args.Name); + if (ass != null) + return ass; + } + } + catch (Exception e) + { + Logger.Log(LogLevel.Error, $"The assembly resolver of the patcher provider [{currentProvider?.Info.GUID}] " + + $"threw an exception while resolving {args.Name}: {e}"); + return null; } return null; diff --git a/BepInEx.Preloader.Core/Patching/Attributes.cs b/BepInEx.Preloader.Core/Patching/Attributes.cs index a6e5675..1b2ccab 100644 --- a/BepInEx.Preloader.Core/Patching/Attributes.cs +++ b/BepInEx.Preloader.Core/Patching/Attributes.cs @@ -91,7 +91,7 @@ public class PatcherProviderPluginInfoAttribute : PatcherPluginInfoAttribute { { } - internal static PatcherProviderPluginInfoAttribute FromCecilType(TypeDefinition td) + internal new static PatcherProviderPluginInfoAttribute FromCecilType(TypeDefinition td) { var attr = MetadataHelper.GetCustomAttributes(td, false).FirstOrDefault(); diff --git a/BepInEx.Preloader.Core/Patching/BasePatcherProvider.cs b/BepInEx.Preloader.Core/Patching/BasePatcherProvider.cs index c1ef817..08c3503 100644 --- a/BepInEx.Preloader.Core/Patching/BasePatcherProvider.cs +++ b/BepInEx.Preloader.Core/Patching/BasePatcherProvider.cs @@ -39,8 +39,8 @@ public abstract class BasePatcherProvider /// /// Obtains a list of assemblies containing patchers to load /// - /// A list of loaders, one per assembly - public abstract IList GetPatchers(); + /// A list of load context, one per assembly + public abstract IList GetPatchers(); /// /// A custom assembly resolver that can be used by this provider to resolve assemblies diff --git a/BepInEx.Preloader.Core/Patching/PatcherPluginMetadata.cs b/BepInEx.Preloader.Core/Patching/PatcherPluginMetadata.cs index 9d8b963..ceb88b7 100644 --- a/BepInEx.Preloader.Core/Patching/PatcherPluginMetadata.cs +++ b/BepInEx.Preloader.Core/Patching/PatcherPluginMetadata.cs @@ -14,9 +14,9 @@ internal class PatcherPluginMetadata : ICacheable public string TypeName { get; set; } = string.Empty; /// - /// The loader used to load this patcher, null if it is a provider + /// The load context used to load this patcher, null if it is a provider /// - public IPluginLoader Loader { get; set; } + public IPluginLoadContext LoadContext { get; set; } /// public void Save(BinaryWriter bw) => bw.Write(TypeName); From d8dd404e52d4669f54b665cb525f9b7119713a9e Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Fri, 23 Aug 2024 15:40:22 -0400 Subject: [PATCH 08/65] Address code review --- BepInEx.Core/Bootstrap/BaseChainloader.cs | 6 ++++-- BepInEx.Core/Contract/IPluginLoadContext.cs | 13 +++++++++++-- .../BepInExPatcherLoadContext.cs | 18 +++++++++++++++++- .../BepInExPluginLoadContext.cs | 18 +++++++++++++++++- .../Patching/AssemblyPatcher.cs | 1 + 5 files changed, 50 insertions(+), 6 deletions(-) diff --git a/BepInEx.Core/Bootstrap/BaseChainloader.cs b/BepInEx.Core/Bootstrap/BaseChainloader.cs index a025b21..75328c7 100644 --- a/BepInEx.Core/Bootstrap/BaseChainloader.cs +++ b/BepInEx.Core/Bootstrap/BaseChainloader.cs @@ -403,20 +403,22 @@ public abstract class BaseChainloader var type = ass.GetType(plugin.TypeName); plugin.Instance = Activator.CreateInstance(type); AppDomain.CurrentDomain.AssemblyResolve += (_, args) => ((BasePluginProvider)plugin.Instance).ResolveAssembly(args.Name); - var requestedPluginsStream = ((BasePluginProvider)plugin.Instance).GetPlugins(); - LoadContexts.AddRange(requestedPluginsStream); + var pluginLoadContexts = ((BasePluginProvider)plugin.Instance).GetPlugins(); + LoadContexts.AddRange(pluginLoadContexts); ProviderLoaded?.Invoke(plugin); } else { plugin.Instance = LoadPlugin(plugin, ass); PluginLoaded?.Invoke(plugin); + plugin.LoadContext.Dispose(); } } catch (Exception ex) { invalidPlugins.Add(plugin.Metadata.GUID); existingPlugins.Remove(plugin.Metadata.GUID); + plugin.LoadContext.Dispose(); Logger.Log(LogLevel.Error, $"Error loading [{plugin}]: {(ex is ReflectionTypeLoadException re ? TypeLoader.TypeLoadExceptionToString(re) : ex.ToString())}"); diff --git a/BepInEx.Core/Contract/IPluginLoadContext.cs b/BepInEx.Core/Contract/IPluginLoadContext.cs index c17fc41..715f09c 100644 --- a/BepInEx.Core/Contract/IPluginLoadContext.cs +++ b/BepInEx.Core/Contract/IPluginLoadContext.cs @@ -1,10 +1,12 @@ -namespace BepInEx; +using System; + +namespace BepInEx; /// /// An interface that allows to dynamically load and track an assembly /// which can be obtained from a provider /// -public interface IPluginLoadContext +public interface IPluginLoadContext : IDisposable { /// /// An identifier that uniquely identifies an assembly from a provider no matter its revision @@ -23,4 +25,11 @@ public interface IPluginLoadContext /// /// The assembly's raw data in bytes public byte[] GetAssemblyData(); + + /// + /// Obtains a file's raw data using a relative path from the plugin assembly + /// + /// The relative path from the plugin assembly used to locate the file + /// The file's raw data if resolved successfully, null if the file couldn't be loaded + public byte[] GetFile(string relativePath); } diff --git a/BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs b/BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs index 590678c..9be40a5 100644 --- a/BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs +++ b/BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; namespace BepInEx.PatcherProvider; @@ -16,4 +17,19 @@ public class BepInExPatcherLoadContext : IPluginLoadContext return assemblyData; } + + public byte[] GetFile(string relativePath) + { + if (relativePath == null) + throw new ArgumentNullException(nameof(relativePath)); + + string assemblyFolder = Path.GetDirectoryName(AssemblyIdentifier); + string filePath = Path.Combine(assemblyFolder, relativePath); + return File.ReadAllBytes(filePath); + } + + public void Dispose() + { + assemblyData = null; + } } diff --git a/BepInEx.PluginProvider/BepInExPluginLoadContext.cs b/BepInEx.PluginProvider/BepInExPluginLoadContext.cs index a39f649..9a7cb38 100644 --- a/BepInEx.PluginProvider/BepInExPluginLoadContext.cs +++ b/BepInEx.PluginProvider/BepInExPluginLoadContext.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; namespace BepInEx.PluginProvider; @@ -16,4 +17,19 @@ public class BepInExPluginLoadContext : IPluginLoadContext return assemblyData; } + + public byte[] GetFile(string relativePath) + { + if (relativePath == null) + throw new ArgumentNullException(nameof(relativePath)); + + string assemblyFolder = Path.GetDirectoryName(AssemblyIdentifier); + string filePath = Path.Combine(assemblyFolder, relativePath); + return File.ReadAllBytes(filePath); + } + + public void Dispose() + { + assemblyData = null; + } } diff --git a/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs b/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs index 50cff04..a9822d9 100644 --- a/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs +++ b/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs @@ -188,6 +188,7 @@ public class AssemblyPatcher : IDisposable var assName = ass.GetName(); Logger.Log(LogLevel.Debug, $"Loaded patcher {patcherPlugin.TypeName} from [{assName.Name} {assName.Version}]"); + patcherPlugin.LoadContext.Dispose(); } PatcherContext.PatchDefinitions.AddRange(sortedPatchers); From b688bfa97898187939dfefc7eb071dfae5188e47 Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Fri, 23 Aug 2024 23:06:54 -0400 Subject: [PATCH 09/65] Add a Source property to PluginInfo Also abstract an interface among the base plugins classes to make this easier --- BepInEx.Core/BepInEx.Core.csproj | 3 -- BepInEx.Core/Bootstrap/BaseChainloader.cs | 23 +++++++++-- BepInEx.Core/Bootstrap/TypeLoader.cs | 2 +- BepInEx.Core/Contract/IPlugin.cs | 39 ++++++++----------- BepInEx.Core/Contract/PluginInfo.cs | 6 +++ BepInEx.Core/Properties/AssemblyInfo.cs | 1 + Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs | 18 +++++++-- .../Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs | 15 +++++-- .../BepInEx.Unity.Mono/BaseUnityPlugin.cs | 15 +++---- 9 files changed, 75 insertions(+), 47 deletions(-) diff --git a/BepInEx.Core/BepInEx.Core.csproj b/BepInEx.Core/BepInEx.Core.csproj index 30a9e20..c4f5cd0 100644 --- a/BepInEx.Core/BepInEx.Core.csproj +++ b/BepInEx.Core/BepInEx.Core.csproj @@ -12,9 +12,6 @@ - - - diff --git a/BepInEx.Core/Bootstrap/BaseChainloader.cs b/BepInEx.Core/Bootstrap/BaseChainloader.cs index 75328c7..dab5eb0 100644 --- a/BepInEx.Core/Bootstrap/BaseChainloader.cs +++ b/BepInEx.Core/Bootstrap/BaseChainloader.cs @@ -116,6 +116,16 @@ public abstract class BaseChainloader return pluginTarget.Build > CurrentAssemblyVersion.Build; } + /// + /// Obtains the corresponding to its GUID + /// + /// The GUID of the plugin to get the info from + /// The info of the plugin, null if the plugin wasn't loaded by the chainloader + public static PluginInfo GetPluginInfoFromGuid(string pluginGuid) + { + return Plugins.TryGetValue(pluginGuid, out var pluginInfo) ? pluginInfo : null; + } + #region Contract protected virtual string ConsoleTitle => $"BepInEx {Paths.BepInExVersion} - {Paths.ProcessName}"; @@ -127,7 +137,7 @@ public abstract class BaseChainloader /// If a plugin fails to load, it is removed. This list only contains the plugins that were actually /// loaded and have their dependencies satisfied. /// - public Dictionary Plugins { get; } = new(); + public static Dictionary Plugins { get; } = new(); /// /// List of all instances of all providers loaded via the chainloader. @@ -138,8 +148,9 @@ public abstract class BaseChainloader /// /// List of all discovered from all the providers + /// with each context associated with its provider info /// - public List LoadContexts { get; } = new(); + public Dictionary LoadContexts { get; } = new(); /// /// Collection of error chainloader messages that occured during plugin loading. @@ -344,7 +355,7 @@ public abstract class BaseChainloader LoadPlugins(providers, provider: true); FinishedProviders?.Invoke(); - var plugins = TypeLoader.GetPluginsFromLoadContexts(LoadContexts, ToPluginInfo, HasBepinPluginType, "chainloader"); + var plugins = TypeLoader.GetPluginsFromLoadContexts(LoadContexts.Keys, ToPluginInfo, HasBepinPluginType, "chainloader"); Logger.Log(LogLevel.Info, $"{plugins.Count} plugin{(plugins.Count == 1 ? "" : "s")} to load"); LoadPlugins(plugins, provider: false); Finished?.Invoke(); @@ -404,11 +415,15 @@ public abstract class BaseChainloader plugin.Instance = Activator.CreateInstance(type); AppDomain.CurrentDomain.AssemblyResolve += (_, args) => ((BasePluginProvider)plugin.Instance).ResolveAssembly(args.Name); var pluginLoadContexts = ((BasePluginProvider)plugin.Instance).GetPlugins(); - LoadContexts.AddRange(pluginLoadContexts); + foreach (IPluginLoadContext context in pluginLoadContexts) + { + LoadContexts[context] = plugin; + } ProviderLoaded?.Invoke(plugin); } else { + plugin.Source = LoadContexts[plugin.LoadContext]; plugin.Instance = LoadPlugin(plugin, ass); PluginLoaded?.Invoke(plugin); plugin.LoadContext.Dispose(); diff --git a/BepInEx.Core/Bootstrap/TypeLoader.cs b/BepInEx.Core/Bootstrap/TypeLoader.cs index 4cecee8..b847c4e 100644 --- a/BepInEx.Core/Bootstrap/TypeLoader.cs +++ b/BepInEx.Core/Bootstrap/TypeLoader.cs @@ -184,7 +184,7 @@ public static class TypeLoader /// A dictionary of all assemblies in the directory and the list of type metadatas of types that match the /// selector. /// - public static List GetPluginsFromLoadContexts(IList loadContexts, + public static List GetPluginsFromLoadContexts(IEnumerable loadContexts, Func typeSelector, Func assemblyFilter = null, string cacheName = null) diff --git a/BepInEx.Core/Contract/IPlugin.cs b/BepInEx.Core/Contract/IPlugin.cs index 6043fb0..84cc6b5 100644 --- a/BepInEx.Core/Contract/IPlugin.cs +++ b/BepInEx.Core/Contract/IPlugin.cs @@ -1,28 +1,23 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using BepInEx.Configuration; +using BepInEx.Configuration; using BepInEx.Logging; -namespace BepInEx.Contract +namespace BepInEx.Contract; + +internal interface IPlugin { - public interface IPlugin - { - /// - /// Information about this plugin as it was loaded. - /// - PluginInfo Info { get; } + /// + /// Information about this plugin as it was loaded. + /// + PluginInfo Info { get; } - /// - /// Logger instance tied to this plugin. - /// - ManualLogSource Logger { get; } + /// + /// Logger instance tied to this plugin. + /// + ManualLogSource Logger { get; } - /// - /// Default config file tied to this plugin. The config file will not be created until - /// any settings are added and changed, or is called. - /// - ConfigFile Config { get; } - } + /// + /// Default config file tied to this plugin. The config file will not be created until + /// any settings are added and changed, or is called. + /// + ConfigFile Config { get; } } diff --git a/BepInEx.Core/Contract/PluginInfo.cs b/BepInEx.Core/Contract/PluginInfo.cs index 15e2729..26e5cae 100644 --- a/BepInEx.Core/Contract/PluginInfo.cs +++ b/BepInEx.Core/Contract/PluginInfo.cs @@ -17,6 +17,12 @@ public class PluginInfo : ICacheable /// public BepInPlugin Metadata { get; internal set; } + /// + /// The of the plugin provider that loaded this plugin. + /// This is null if this plugin is a provider + /// + public PluginInfo Source { get; internal set; } + /// /// The load context used to load this plugin, null if it is a provider /// diff --git a/BepInEx.Core/Properties/AssemblyInfo.cs b/BepInEx.Core/Properties/AssemblyInfo.cs index 749dcc2..96d56c1 100644 --- a/BepInEx.Core/Properties/AssemblyInfo.cs +++ b/BepInEx.Core/Properties/AssemblyInfo.cs @@ -2,6 +2,7 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("BepInEx.Preloader.Core")] [assembly: InternalsVisibleTo("BepInEx.Unity.Mono")] +[assembly: InternalsVisibleTo("BepInEx.NET.Common")] [assembly: InternalsVisibleTo("BepInEx.NET.Framework.Launcher")] [assembly: InternalsVisibleTo("BepInEx.NET.CoreCLR")] [assembly: InternalsVisibleTo("BepInEx.Unity.IL2CPP")] diff --git a/Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs b/Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs index dfc863b..f417838 100644 --- a/Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs +++ b/Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs @@ -1,10 +1,12 @@ +using BepInEx.Bootstrap; using BepInEx.Configuration; +using BepInEx.Contract; using BepInEx.Logging; using HarmonyLib; namespace BepInEx.NET.Common { - public abstract class BasePlugin + public abstract class BasePlugin : IPlugin { protected BasePlugin() { @@ -12,13 +14,21 @@ namespace BepInEx.NET.Common HarmonyInstance = new Harmony("BepInEx.Plugin." + metadata.GUID); - Log = Logger.CreateLogSource(metadata.Name); + Info = BaseChainloader.GetPluginInfoFromGuid(metadata.GUID); + Info.Instance = this; + + Logger = BepInEx.Logging.Logger.CreateLogSource(metadata.Name); Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, metadata.GUID + ".cfg"), false, metadata); } + + /// + public PluginInfo Info { get; } + + /// + public ManualLogSource Logger { get; } - public ManualLogSource Log { get; } - + /// public ConfigFile Config { get; } public Harmony HarmonyInstance { get; set; } diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs index 3476bde..581a6b4 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs @@ -1,21 +1,28 @@ -using BepInEx.Configuration; +using BepInEx.Bootstrap; +using BepInEx.Configuration; +using BepInEx.Contract; using BepInEx.Logging; using Il2CppInterop.Runtime.InteropTypes; namespace BepInEx.Unity.IL2CPP; -public abstract class BasePlugin +public abstract class BasePlugin : IPlugin { protected BasePlugin() { var metadata = MetadataHelper.GetPluginMetadata(this); - Log = Logger.CreateLogSource(metadata.Name); + Info = BaseChainloader.GetPluginInfoFromGuid(metadata.GUID); + Info.Instance = this; + + Logger = BepInEx.Logging.Logger.CreateLogSource(metadata.Name); Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, metadata.GUID + ".cfg"), false, metadata); } - public ManualLogSource Log { get; } + public PluginInfo Info { get; } + + public ManualLogSource Logger { get; } public ConfigFile Config { get; } diff --git a/Runtimes/Unity/BepInEx.Unity.Mono/BaseUnityPlugin.cs b/Runtimes/Unity/BepInEx.Unity.Mono/BaseUnityPlugin.cs index 09e9c07..cdf73c2 100644 --- a/Runtimes/Unity/BepInEx.Unity.Mono/BaseUnityPlugin.cs +++ b/Runtimes/Unity/BepInEx.Unity.Mono/BaseUnityPlugin.cs @@ -1,5 +1,7 @@ using System; +using BepInEx.Bootstrap; using BepInEx.Configuration; +using BepInEx.Contract; using BepInEx.Logging; using UnityEngine; @@ -8,7 +10,7 @@ namespace BepInEx.Unity.Mono; /// /// The base plugin type that is used by the BepInEx plugin loader. /// -public abstract class BaseUnityPlugin : MonoBehaviour +public abstract class BaseUnityPlugin : MonoBehaviour, IPlugin { /// /// Create a new instance of a plugin and all of its tied in objects. @@ -21,13 +23,8 @@ public abstract class BaseUnityPlugin : MonoBehaviour throw new InvalidOperationException("Can't create an instance of " + GetType().FullName + " because it inherits from BaseUnityPlugin and the BepInPlugin attribute is missing."); - Info = new PluginInfo - { - Metadata = metadata, - Instance = this, - Dependencies = MetadataHelper.GetDependencies(GetType()), - Processes = MetadataHelper.GetAttributes(GetType()), - }; + Info = BaseChainloader.GetPluginInfoFromGuid(metadata.GUID); + Info.Instance = this; Logger = BepInEx.Logging.Logger.CreateLogSource(metadata.Name); @@ -42,7 +39,7 @@ public abstract class BaseUnityPlugin : MonoBehaviour /// /// Logger instance tied to this plugin. /// - protected ManualLogSource Logger { get; } + public ManualLogSource Logger { get; } /// /// Default config file tied to this plugin. The config file will not be created until From bfc539bd74699d21eaea84140671ef86a92a0618 Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Sun, 25 Aug 2024 12:03:33 -0400 Subject: [PATCH 10/65] Address code review round 2 --- BepInEx.Core/Bootstrap/BaseChainloader.cs | 76 ++++++++++--------- Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs | 12 ++- .../NET/BepInEx.NET.Common/NetChainloader.cs | 3 - .../Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs | 14 +++- .../BepInEx.Unity.IL2CPP/IL2CPPChainloader.cs | 2 - .../BepInEx.Unity.Mono/BaseUnityPlugin.cs | 11 ++- .../Bootstrap/UnityChainloader.cs | 9 +-- 7 files changed, 75 insertions(+), 52 deletions(-) diff --git a/BepInEx.Core/Bootstrap/BaseChainloader.cs b/BepInEx.Core/Bootstrap/BaseChainloader.cs index dab5eb0..15509b7 100644 --- a/BepInEx.Core/Bootstrap/BaseChainloader.cs +++ b/BepInEx.Core/Bootstrap/BaseChainloader.cs @@ -120,10 +120,12 @@ public abstract class BaseChainloader /// Obtains the corresponding to its GUID /// /// The GUID of the plugin to get the info from - /// The info of the plugin, null if the plugin wasn't loaded by the chainloader - public static PluginInfo GetPluginInfoFromGuid(string pluginGuid) + /// When this method returns, contains the + /// associated with the specified GUID, if the plugin was loaded; otherwise, null. This parameter is passed uninitialized. + /// true if the plugin with the GUID was loaded by the chainloader; otherwise, false. + public bool TryGetPluginInfoFromGuid(string pluginGuid, out PluginInfo foundPluginInfo) { - return Plugins.TryGetValue(pluginGuid, out var pluginInfo) ? pluginInfo : null; + return Plugins.TryGetValue(pluginGuid, out foundPluginInfo); } #region Contract @@ -132,12 +134,17 @@ public abstract class BaseChainloader private bool _initialized; + /// + /// The current chainloader instance + /// + public static BaseChainloader Instance { get; protected set; } + /// /// List of all instances of all plugins loaded via the chainloader. /// If a plugin fails to load, it is removed. This list only contains the plugins that were actually /// loaded and have their dependencies satisfied. /// - public static Dictionary Plugins { get; } = new(); + public Dictionary Plugins { get; } = new(); /// /// List of all instances of all providers loaded via the chainloader. @@ -147,8 +154,9 @@ public abstract class BaseChainloader public Dictionary Providers { get; } = new(); /// - /// List of all discovered from all the providers - /// with each context associated with its provider info + /// List of all objects received from plugin providers. + /// Value holds info of the originating provider. These contexts are used by chainloader to + /// load plugins. Plugins that are loaded successfully are added to . /// public Dictionary LoadContexts { get; } = new(); @@ -349,7 +357,7 @@ public abstract class BaseChainloader var builtinProviders = DiscoverPluginProvidersFrom(Paths.BepInExAssemblyDirectory); var providers = DiscoverPluginProvidersFrom(Paths.PluginProviderPath); Logger.Log(LogLevel.Info, $"{providers.Count + builtinProviders.Count} " + - $"plugins provider{(providers.Count + builtinProviders.Count == 1 ? "" : "s")} to load"); + $"plugin provider{(providers.Count + builtinProviders.Count == 1 ? "" : "s")} to load"); LoadPlugins(builtinProviders, provider: true); LoadPlugins(providers, provider: true); @@ -376,67 +384,67 @@ public abstract class BaseChainloader private void LoadPlugins(List plugins, bool provider) { - var existingPlugins = provider ? Providers : Plugins; - var sortedPlugins = ModifyLoadOrder(plugins, existingPlugins); + var existingPluginInfos = provider ? Providers : Plugins; + var sortedPluginInfos = ModifyLoadOrder(plugins, existingPluginInfos); var invalidPlugins = new HashSet(); var processedPlugins = new Dictionary(); var loadedAssemblies = new Dictionary(); - foreach (var plugin in sortedPlugins) + foreach (var pluginInfo in sortedPluginInfos) { - if (!CheckDependencies(plugin, processedPlugins, invalidPlugins, existingPlugins)) + if (!CheckDependencies(pluginInfo, processedPlugins, invalidPlugins, existingPluginInfos)) { continue; } try { - Logger.Log(LogLevel.Info, $"Loading [{plugin}]"); + Logger.Log(LogLevel.Info, $"Loading [{pluginInfo}]"); Assembly ass = null; - if (provider && !loadedAssemblies.TryGetValue(plugin.Location, out ass)) + if (provider && !loadedAssemblies.TryGetValue(pluginInfo.Location, out ass)) { - ass = Assembly.LoadFrom(plugin.Location); - loadedAssemblies[plugin.Location] = ass; + ass = Assembly.LoadFrom(pluginInfo.Location); + loadedAssemblies[pluginInfo.Location] = ass; } - else if (!provider && !loadedAssemblies.TryGetValue(plugin.LoadContext.AssemblyIdentifier, out ass)) + else if (!provider && !loadedAssemblies.TryGetValue(pluginInfo.LoadContext.AssemblyIdentifier, out ass)) { - ass = Assembly.Load(plugin.LoadContext.GetAssemblyData()); - loadedAssemblies[plugin.LoadContext.AssemblyIdentifier] = ass; + ass = Assembly.Load(pluginInfo.LoadContext.GetAssemblyData()); + loadedAssemblies[pluginInfo.LoadContext.AssemblyIdentifier] = ass; } - existingPlugins[plugin.Metadata.GUID] = plugin; - TryRunModuleCtor(plugin, ass); + existingPluginInfos[pluginInfo.Metadata.GUID] = pluginInfo; + TryRunModuleCtor(pluginInfo, ass); if (provider) { - var type = ass.GetType(plugin.TypeName); - plugin.Instance = Activator.CreateInstance(type); - AppDomain.CurrentDomain.AssemblyResolve += (_, args) => ((BasePluginProvider)plugin.Instance).ResolveAssembly(args.Name); - var pluginLoadContexts = ((BasePluginProvider)plugin.Instance).GetPlugins(); + var type = ass.GetType(pluginInfo.TypeName); + pluginInfo.Instance = Activator.CreateInstance(type); + AppDomain.CurrentDomain.AssemblyResolve += (_, args) => ((BasePluginProvider)pluginInfo.Instance).ResolveAssembly(args.Name); + var pluginLoadContexts = ((BasePluginProvider)pluginInfo.Instance).GetPlugins(); foreach (IPluginLoadContext context in pluginLoadContexts) { - LoadContexts[context] = plugin; + LoadContexts[context] = pluginInfo; } - ProviderLoaded?.Invoke(plugin); + ProviderLoaded?.Invoke(pluginInfo); } else { - plugin.Source = LoadContexts[plugin.LoadContext]; - plugin.Instance = LoadPlugin(plugin, ass); - PluginLoaded?.Invoke(plugin); - plugin.LoadContext.Dispose(); + pluginInfo.Source = LoadContexts[pluginInfo.LoadContext]; + pluginInfo.Instance = LoadPlugin(pluginInfo, ass); + PluginLoaded?.Invoke(pluginInfo); + pluginInfo.LoadContext.Dispose(); } } catch (Exception ex) { - invalidPlugins.Add(plugin.Metadata.GUID); - existingPlugins.Remove(plugin.Metadata.GUID); - plugin.LoadContext.Dispose(); + invalidPlugins.Add(pluginInfo.Metadata.GUID); + existingPluginInfos.Remove(pluginInfo.Metadata.GUID); + pluginInfo.LoadContext.Dispose(); Logger.Log(LogLevel.Error, - $"Error loading [{plugin}]: {(ex is ReflectionTypeLoadException re ? TypeLoader.TypeLoadExceptionToString(re) : ex.ToString())}"); + $"Error loading [{pluginInfo}]: {(ex is ReflectionTypeLoadException re ? TypeLoader.TypeLoadExceptionToString(re) : ex.ToString())}"); } } } diff --git a/Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs b/Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs index f417838..810dd19 100644 --- a/Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs +++ b/Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs @@ -1,3 +1,4 @@ +using System; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Contract; @@ -14,8 +15,15 @@ namespace BepInEx.NET.Common HarmonyInstance = new Harmony("BepInEx.Plugin." + metadata.GUID); - Info = BaseChainloader.GetPluginInfoFromGuid(metadata.GUID); - Info.Instance = this; + if (BaseChainloader.Instance.TryGetPluginInfoFromGuid(metadata.GUID, out var pluginInfo)) + { + Info = pluginInfo; + Info.Instance = this; + } + else + { + throw new InvalidOperationException($"The plugin information for {metadata.GUID} couldn't be found on the chainloader"); + } Logger = BepInEx.Logging.Logger.CreateLogSource(metadata.Name); diff --git a/Runtimes/NET/BepInEx.NET.Common/NetChainloader.cs b/Runtimes/NET/BepInEx.NET.Common/NetChainloader.cs index 5e4af66..9f9717c 100644 --- a/Runtimes/NET/BepInEx.NET.Common/NetChainloader.cs +++ b/Runtimes/NET/BepInEx.NET.Common/NetChainloader.cs @@ -7,9 +7,6 @@ namespace BepInEx.NET.Common { public class NetChainloader : BaseChainloader { - // TODO: Remove once proper instance handling exists - public static NetChainloader Instance { get; set; } - public override void Initialize(string gameExePath = null) { Instance = this; diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs index 581a6b4..3ffa16d 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs @@ -1,4 +1,5 @@ -using BepInEx.Bootstrap; +using System; +using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Contract; using BepInEx.Logging; @@ -12,8 +13,15 @@ public abstract class BasePlugin : IPlugin { var metadata = MetadataHelper.GetPluginMetadata(this); - Info = BaseChainloader.GetPluginInfoFromGuid(metadata.GUID); - Info.Instance = this; + if (BaseChainloader.Instance.TryGetPluginInfoFromGuid(metadata.GUID, out var pluginInfo)) + { + Info = pluginInfo; + Info.Instance = this; + } + else + { + throw new InvalidOperationException($"The plugin information for {metadata.GUID} couldn't be found on the chainloader"); + } Logger = BepInEx.Logging.Logger.CreateLogSource(metadata.Name); diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/IL2CPPChainloader.cs b/Runtimes/Unity/BepInEx.Unity.IL2CPP/IL2CPPChainloader.cs index 5f857cd..b493789 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/IL2CPPChainloader.cs +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/IL2CPPChainloader.cs @@ -32,8 +32,6 @@ public class IL2CPPChainloader : BaseChainloader private static INativeDetour RuntimeInvokeDetour { get; set; } - public static IL2CPPChainloader Instance { get; set; } - /// /// Register and add a Unity Component (for example MonoBehaviour) into BepInEx global manager. /// Automatically registers the type with Il2Cpp type system if it isn't initialised already. diff --git a/Runtimes/Unity/BepInEx.Unity.Mono/BaseUnityPlugin.cs b/Runtimes/Unity/BepInEx.Unity.Mono/BaseUnityPlugin.cs index cdf73c2..4c0552b 100644 --- a/Runtimes/Unity/BepInEx.Unity.Mono/BaseUnityPlugin.cs +++ b/Runtimes/Unity/BepInEx.Unity.Mono/BaseUnityPlugin.cs @@ -23,8 +23,15 @@ public abstract class BaseUnityPlugin : MonoBehaviour, IPlugin throw new InvalidOperationException("Can't create an instance of " + GetType().FullName + " because it inherits from BaseUnityPlugin and the BepInPlugin attribute is missing."); - Info = BaseChainloader.GetPluginInfoFromGuid(metadata.GUID); - Info.Instance = this; + if (BaseChainloader.Instance.TryGetPluginInfoFromGuid(metadata.GUID, out var pluginInfo)) + { + Info = pluginInfo; + Info.Instance = this; + } + else + { + throw new InvalidOperationException($"The plugin information for {metadata.GUID} couldn't be found on the chainloader"); + } Logger = BepInEx.Logging.Logger.CreateLogSource(metadata.Name); diff --git a/Runtimes/Unity/BepInEx.Unity.Mono/Bootstrap/UnityChainloader.cs b/Runtimes/Unity/BepInEx.Unity.Mono/Bootstrap/UnityChainloader.cs index c08a8de..1c163b9 100644 --- a/Runtimes/Unity/BepInEx.Unity.Mono/Bootstrap/UnityChainloader.cs +++ b/Runtimes/Unity/BepInEx.Unity.Mono/Bootstrap/UnityChainloader.cs @@ -37,9 +37,6 @@ public class UnityChainloader : BaseChainloader private string _consoleTitle; - // TODO: Remove once proper instance handling exists - public static UnityChainloader Instance { get; set; } - /// /// The GameObject that all plugins are attached to as components. /// @@ -70,9 +67,9 @@ public class UnityChainloader : BaseChainloader Logger.Log(LogLevel.Debug, "Entering chainloader StaticStart"); - var instance = new UnityChainloader(); - instance.Initialize(gameExePath); - instance.Execute(); + Instance = new UnityChainloader(); + Instance.Initialize(gameExePath); + Instance.Execute(); Logger.Log(LogLevel.Debug, "Exiting chainloader StaticStart"); } From b729aee545aee871f17029f2c0e2c56d4c567dd7 Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Wed, 28 Aug 2024 01:14:00 -0400 Subject: [PATCH 11/65] Address code review round 3 (first part) --- BepInEx.Core/Bootstrap/BaseChainloader.cs | 7 +++---- BepInEx.PatcherProvider/BepInExPatcherProvider.cs | 2 +- BepInEx.PluginProvider/BepInExPluginProvider.cs | 2 +- Runtimes/NET/BepInEx.NET.Common/BepInEx.NET.Common.csproj | 4 ++-- .../Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj | 4 ++-- .../Unity/BepInEx.Unity.Mono/BepInEx.Unity.Mono.csproj | 4 ++-- 6 files changed, 11 insertions(+), 12 deletions(-) diff --git a/BepInEx.Core/Bootstrap/BaseChainloader.cs b/BepInEx.Core/Bootstrap/BaseChainloader.cs index 15509b7..665535f 100644 --- a/BepInEx.Core/Bootstrap/BaseChainloader.cs +++ b/BepInEx.Core/Bootstrap/BaseChainloader.cs @@ -257,13 +257,12 @@ public abstract class BaseChainloader /// Some plugins may be skipped if they cannot be loaded (wrong metadata, etc). /// Plugins to process. /// List of plugins to load in the correct load order. - protected virtual List ModifyLoadOrder(IList plugins, Dictionary existingPlugins) - where T : PluginInfo + protected virtual List ModifyLoadOrder(IList plugins, Dictionary existingPlugins) { // We use a sorted dictionary to ensure consistent load order var dependencyDict = new SortedDictionary>(StringComparer.InvariantCultureIgnoreCase); - var pluginsByGuid = new Dictionary(); + var pluginsByGuid = new Dictionary(); foreach (var pluginInfoGroup in plugins.GroupBy(info => info.Metadata.GUID)) { @@ -441,7 +440,7 @@ public abstract class BaseChainloader { invalidPlugins.Add(pluginInfo.Metadata.GUID); existingPluginInfos.Remove(pluginInfo.Metadata.GUID); - pluginInfo.LoadContext.Dispose(); + pluginInfo.LoadContext?.Dispose(); Logger.Log(LogLevel.Error, $"Error loading [{pluginInfo}]: {(ex is ReflectionTypeLoadException re ? TypeLoader.TypeLoadExceptionToString(re) : ex.ToString())}"); diff --git a/BepInEx.PatcherProvider/BepInExPatcherProvider.cs b/BepInEx.PatcherProvider/BepInExPatcherProvider.cs index fb2ff8c..9221ddb 100644 --- a/BepInEx.PatcherProvider/BepInExPatcherProvider.cs +++ b/BepInEx.PatcherProvider/BepInExPatcherProvider.cs @@ -7,7 +7,7 @@ using BepInEx.Preloader.Core.Patching; namespace BepInEx.PatcherProvider; -[PatcherProviderPluginInfo("io.bepinex.bepinexpatcherprovider", "BepInExPatcherProvider", "1.0")] +[PatcherProviderPluginInfo("BepInExPatcherProvider", "BepInExPatcherProvider", "1.0")] public class BepInExPatcherProvider : BasePatcherProvider { private static readonly Dictionary AssemblyLocationsByFilename = new(); diff --git a/BepInEx.PluginProvider/BepInExPluginProvider.cs b/BepInEx.PluginProvider/BepInExPluginProvider.cs index b2f151c..783342a 100644 --- a/BepInEx.PluginProvider/BepInExPluginProvider.cs +++ b/BepInEx.PluginProvider/BepInExPluginProvider.cs @@ -6,7 +6,7 @@ using BepInEx.Logging; namespace BepInEx.PluginProvider; -[BepInPluginProvider("io.bepinex.bepinexpluginprovider", "BepInExPluginProvider", "1.0")] +[BepInPluginProvider("BepInExPluginProvider", "BepInExPluginProvider", "1.0")] public class BepInExPluginProvider : BasePluginProvider { private static readonly Dictionary AssemblyLocationsByFilename = new(); diff --git a/Runtimes/NET/BepInEx.NET.Common/BepInEx.NET.Common.csproj b/Runtimes/NET/BepInEx.NET.Common/BepInEx.NET.Common.csproj index 168fc2b..f715bf1 100644 --- a/Runtimes/NET/BepInEx.NET.Common/BepInEx.NET.Common.csproj +++ b/Runtimes/NET/BepInEx.NET.Common/BepInEx.NET.Common.csproj @@ -14,13 +14,13 @@ false - Content + None PreserveNewest Build;DocumentationProjectOutputGroup false - Content + None PreserveNewest Build;DocumentationProjectOutputGroup diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj index 2698124..86a2e3e 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj @@ -11,13 +11,13 @@ false - Content + None PreserveNewest Build;DocumentationProjectOutputGroup false - Content + None PreserveNewest Build;DocumentationProjectOutputGroup diff --git a/Runtimes/Unity/BepInEx.Unity.Mono/BepInEx.Unity.Mono.csproj b/Runtimes/Unity/BepInEx.Unity.Mono/BepInEx.Unity.Mono.csproj index 71af84f..7f49885 100644 --- a/Runtimes/Unity/BepInEx.Unity.Mono/BepInEx.Unity.Mono.csproj +++ b/Runtimes/Unity/BepInEx.Unity.Mono/BepInEx.Unity.Mono.csproj @@ -13,13 +13,13 @@ false - Content + None PreserveNewest Build;DocumentationProjectOutputGroup false - Content + None PreserveNewest Build;DocumentationProjectOutputGroup From e2396d202b82cea5c162fdf09a046b3001d3b0e0 Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Thu, 29 Aug 2024 10:48:50 -0400 Subject: [PATCH 12/65] address code review round 3 --- BepInEx.Core/Bootstrap/BaseChainloader.cs | 16 +---- BepInEx.Core/Contract/IPluginLoadContext.cs | 6 ++ BepInEx.Core/Utility.cs | 59 +++++++++++++++++++ .../BepInExPatcherLoadContext.cs | 12 ++++ .../BepInExPatcherProvider.cs | 2 +- .../BepInExPluginLoadContext.cs | 12 ++++ .../BepInExPluginProvider.cs | 2 +- Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs | 2 +- .../Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs | 2 +- .../BepInEx.Unity.Mono/BaseUnityPlugin.cs | 2 +- 10 files changed, 97 insertions(+), 18 deletions(-) diff --git a/BepInEx.Core/Bootstrap/BaseChainloader.cs b/BepInEx.Core/Bootstrap/BaseChainloader.cs index 665535f..c2e7c7e 100644 --- a/BepInEx.Core/Bootstrap/BaseChainloader.cs +++ b/BepInEx.Core/Bootstrap/BaseChainloader.cs @@ -116,18 +116,6 @@ public abstract class BaseChainloader return pluginTarget.Build > CurrentAssemblyVersion.Build; } - /// - /// Obtains the corresponding to its GUID - /// - /// The GUID of the plugin to get the info from - /// When this method returns, contains the - /// associated with the specified GUID, if the plugin was loaded; otherwise, null. This parameter is passed uninitialized. - /// true if the plugin with the GUID was loaded by the chainloader; otherwise, false. - public bool TryGetPluginInfoFromGuid(string pluginGuid, out PluginInfo foundPluginInfo) - { - return Plugins.TryGetValue(pluginGuid, out foundPluginInfo); - } - #region Contract protected virtual string ConsoleTitle => $"BepInEx {Paths.BepInExVersion} - {Paths.ProcessName}"; @@ -409,7 +397,9 @@ public abstract class BaseChainloader } else if (!provider && !loadedAssemblies.TryGetValue(pluginInfo.LoadContext.AssemblyIdentifier, out ass)) { - ass = Assembly.Load(pluginInfo.LoadContext.GetAssemblyData()); + var symbols = pluginInfo.LoadContext.GetAssemblySymbolsData(); + ass = symbols != null ? Assembly.Load(pluginInfo.LoadContext.GetAssemblyData(), symbols) + : Assembly.Load(pluginInfo.LoadContext.GetAssemblyData()); loadedAssemblies[pluginInfo.LoadContext.AssemblyIdentifier] = ass; } diff --git a/BepInEx.Core/Contract/IPluginLoadContext.cs b/BepInEx.Core/Contract/IPluginLoadContext.cs index 715f09c..013b973 100644 --- a/BepInEx.Core/Contract/IPluginLoadContext.cs +++ b/BepInEx.Core/Contract/IPluginLoadContext.cs @@ -26,6 +26,12 @@ public interface IPluginLoadContext : IDisposable /// The assembly's raw data in bytes public byte[] GetAssemblyData(); + /// + /// Obtains the assembly's symbols data which will be loaded by the chainloader alongside the assembly + /// + /// The assembly's symbols data in bytes (either in portable pdb or mdb format). Null if no symbols exists for this assembly + public byte[] GetAssemblySymbolsData(); + /// /// Obtains a file's raw data using a relative path from the plugin assembly /// diff --git a/BepInEx.Core/Utility.cs b/BepInEx.Core/Utility.cs index a6b93bd..b8cd30c 100644 --- a/BepInEx.Core/Utility.cs +++ b/BepInEx.Core/Utility.cs @@ -221,6 +221,55 @@ public static class Utility return false; } + /// + /// Try to resolve the symbols data from an assembly DLL. + /// + /// File path of the DLL assembly. + /// The symbols of the DLL assembly. + /// True, if the symbols were found. Otherwise, false. + public static bool TryResolveDllSymbols(string dllPath, out byte[] symbolsData) + { + symbolsData = null; + + var subDirectory = Path.GetDirectoryName(dllPath); + var filename = Path.GetFileNameWithoutExtension(dllPath); + + var potentialSymbolsFileNames = new[] + { + $"{filename}.mdb", + $"{filename}.pdb" + }; + + foreach (var symbolFileName in potentialSymbolsFileNames) + { + var symbolPath = Path.Combine(subDirectory, symbolFileName); + if (!File.Exists(symbolPath)) + continue; + + try + { + symbolsData = File.ReadAllBytes(symbolPath); + } + catch (Exception) + { + continue; + } + + return true; + } + + return false; + } + + private static Assembly LoadAssemblyWithSymbols(string assemblyFilePath) + { + var assemblyData = File.ReadAllBytes(assemblyFilePath); + + return TryResolveDllSymbols(assemblyFilePath, out byte[] symbolsData) + ? Assembly.Load(assemblyData, symbolsData) + : Assembly.Load(assemblyData); + } + /// /// Checks whether a given cecil type definition is a subtype of a provided type. /// @@ -259,6 +308,16 @@ public static class Utility TryResolveDllAssembly(assemblyName, directory, s => AssemblyDefinition.ReadAssembly(s, readerParameters), out assembly); + /// + /// Try to resolve and load the given assembly DLL including their symbols file. + /// + /// 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 TryResolveDllAssemblyWithSymbols(AssemblyName assemblyName, string directory, out Assembly assembly) => + TryResolveDllAssembly(assemblyName, directory, LoadAssemblyWithSymbols, out assembly); + /// /// Tries to create a file with the given name /// diff --git a/BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs b/BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs index 9be40a5..63fc627 100644 --- a/BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs +++ b/BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs @@ -8,6 +8,7 @@ public class BepInExPatcherLoadContext : IPluginLoadContext public string AssemblyIdentifier { get; internal set; } public string AssemblyHash { get; internal set; } private byte[] assemblyData; + private byte[] assemblySymbolsData; public byte[] GetAssemblyData() { if (assemblyData == null) @@ -17,6 +18,17 @@ public class BepInExPatcherLoadContext : IPluginLoadContext return assemblyData; } + + public byte[] GetAssemblySymbolsData() + { + if (assemblySymbolsData == null) + { + if (!Utility.TryResolveDllSymbols(AssemblyIdentifier, out assemblySymbolsData)) + assemblySymbolsData = null; + } + + return assemblySymbolsData; + } public byte[] GetFile(string relativePath) { diff --git a/BepInEx.PatcherProvider/BepInExPatcherProvider.cs b/BepInEx.PatcherProvider/BepInExPatcherProvider.cs index 9221ddb..78fa699 100644 --- a/BepInEx.PatcherProvider/BepInExPatcherProvider.cs +++ b/BepInEx.PatcherProvider/BepInExPatcherProvider.cs @@ -40,7 +40,7 @@ public class BepInExPatcherProvider : BasePatcherProvider if (!AssemblyLocationsByFilename.TryGetValue(name, out var location)) return null; - if (!Utility.TryResolveDllAssembly(new(name), location, out var ass)) + if (!Utility.TryResolveDllAssemblyWithSymbols(new(name), location, out var ass)) return null; return ass; diff --git a/BepInEx.PluginProvider/BepInExPluginLoadContext.cs b/BepInEx.PluginProvider/BepInExPluginLoadContext.cs index 9a7cb38..3271a24 100644 --- a/BepInEx.PluginProvider/BepInExPluginLoadContext.cs +++ b/BepInEx.PluginProvider/BepInExPluginLoadContext.cs @@ -8,6 +8,7 @@ public class BepInExPluginLoadContext : IPluginLoadContext public string AssemblyIdentifier { get; internal set; } public string AssemblyHash { get; internal set; } private byte[] assemblyData; + private byte[] assemblySymbolsData; public byte[] GetAssemblyData() { if (assemblyData == null) @@ -18,6 +19,17 @@ public class BepInExPluginLoadContext : IPluginLoadContext return assemblyData; } + public byte[] GetAssemblySymbolsData() + { + if (assemblySymbolsData == null) + { + if (!Utility.TryResolveDllSymbols(AssemblyIdentifier, out assemblySymbolsData)) + assemblySymbolsData = null; + } + + return assemblySymbolsData; + } + public byte[] GetFile(string relativePath) { if (relativePath == null) diff --git a/BepInEx.PluginProvider/BepInExPluginProvider.cs b/BepInEx.PluginProvider/BepInExPluginProvider.cs index 783342a..bae7689 100644 --- a/BepInEx.PluginProvider/BepInExPluginProvider.cs +++ b/BepInEx.PluginProvider/BepInExPluginProvider.cs @@ -39,7 +39,7 @@ public class BepInExPluginProvider : BasePluginProvider if (!AssemblyLocationsByFilename.TryGetValue(name, out var location)) return null; - if (!Utility.TryResolveDllAssembly(new(name), location, out var ass)) + if (!Utility.TryResolveDllAssemblyWithSymbols(new(name), location, out var ass)) return null; return ass; diff --git a/Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs b/Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs index 810dd19..bbd52eb 100644 --- a/Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs +++ b/Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs @@ -15,7 +15,7 @@ namespace BepInEx.NET.Common HarmonyInstance = new Harmony("BepInEx.Plugin." + metadata.GUID); - if (BaseChainloader.Instance.TryGetPluginInfoFromGuid(metadata.GUID, out var pluginInfo)) + if (BaseChainloader.Instance.Plugins.TryGetValue(metadata.GUID, out var pluginInfo)) { Info = pluginInfo; Info.Instance = this; diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs index 3ffa16d..e6889da 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs @@ -13,7 +13,7 @@ public abstract class BasePlugin : IPlugin { var metadata = MetadataHelper.GetPluginMetadata(this); - if (BaseChainloader.Instance.TryGetPluginInfoFromGuid(metadata.GUID, out var pluginInfo)) + if (BaseChainloader.Instance.Plugins.TryGetValue(metadata.GUID, out var pluginInfo)) { Info = pluginInfo; Info.Instance = this; diff --git a/Runtimes/Unity/BepInEx.Unity.Mono/BaseUnityPlugin.cs b/Runtimes/Unity/BepInEx.Unity.Mono/BaseUnityPlugin.cs index 4c0552b..f7ea012 100644 --- a/Runtimes/Unity/BepInEx.Unity.Mono/BaseUnityPlugin.cs +++ b/Runtimes/Unity/BepInEx.Unity.Mono/BaseUnityPlugin.cs @@ -23,7 +23,7 @@ public abstract class BaseUnityPlugin : MonoBehaviour, IPlugin throw new InvalidOperationException("Can't create an instance of " + GetType().FullName + " because it inherits from BaseUnityPlugin and the BepInPlugin attribute is missing."); - if (BaseChainloader.Instance.TryGetPluginInfoFromGuid(metadata.GUID, out var pluginInfo)) + if (BaseChainloader.Instance.Plugins.TryGetValue(metadata.GUID, out var pluginInfo)) { Info = pluginInfo; Info.Instance = this; From ee5918bd76ff547925d97e800a9f856b33fcbbed Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Thu, 29 Aug 2024 14:44:30 -0400 Subject: [PATCH 13/65] Address minor code review --- BepInEx.Core/Utility.cs | 2 +- BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs | 1 + BepInEx.PluginProvider/BepInExPluginLoadContext.cs | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/BepInEx.Core/Utility.cs b/BepInEx.Core/Utility.cs index b8cd30c..1a35841 100644 --- a/BepInEx.Core/Utility.cs +++ b/BepInEx.Core/Utility.cs @@ -236,7 +236,7 @@ public static class Utility var potentialSymbolsFileNames = new[] { - $"{filename}.mdb", + $"{filename}.dll.mdb", $"{filename}.pdb" }; diff --git a/BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs b/BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs index 63fc627..3aca66f 100644 --- a/BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs +++ b/BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs @@ -43,5 +43,6 @@ public class BepInExPatcherLoadContext : IPluginLoadContext public void Dispose() { assemblyData = null; + assemblySymbolsData = null; } } diff --git a/BepInEx.PluginProvider/BepInExPluginLoadContext.cs b/BepInEx.PluginProvider/BepInExPluginLoadContext.cs index 3271a24..389e181 100644 --- a/BepInEx.PluginProvider/BepInExPluginLoadContext.cs +++ b/BepInEx.PluginProvider/BepInExPluginLoadContext.cs @@ -43,5 +43,6 @@ public class BepInExPluginLoadContext : IPluginLoadContext public void Dispose() { assemblyData = null; + assemblySymbolsData = null; } } From 1f182c7770c91e716464785e2cf3ca4e50cccf4c Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Thu, 29 Aug 2024 21:11:36 -0400 Subject: [PATCH 14/65] Correct the symbols resolution and rename the method --- BepInEx.Core/Utility.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/BepInEx.Core/Utility.cs b/BepInEx.Core/Utility.cs index 1a35841..3ba6f1f 100644 --- a/BepInEx.Core/Utility.cs +++ b/BepInEx.Core/Utility.cs @@ -227,17 +227,16 @@ public static class Utility /// File path of the DLL assembly. /// The symbols of the DLL assembly. /// True, if the symbols were found. Otherwise, false. - public static bool TryResolveDllSymbols(string dllPath, out byte[] symbolsData) + public static bool TryResolveAssemblySymbols(string dllPath, out byte[] symbolsData) { symbolsData = null; var subDirectory = Path.GetDirectoryName(dllPath); - var filename = Path.GetFileNameWithoutExtension(dllPath); var potentialSymbolsFileNames = new[] { - $"{filename}.dll.mdb", - $"{filename}.pdb" + $"{dllPath}.mdb", + Path.ChangeExtension(dllPath, ".pdb") }; foreach (var symbolFileName in potentialSymbolsFileNames) @@ -265,7 +264,7 @@ public static class Utility { var assemblyData = File.ReadAllBytes(assemblyFilePath); - return TryResolveDllSymbols(assemblyFilePath, out byte[] symbolsData) + return TryResolveAssemblySymbols(assemblyFilePath, out byte[] symbolsData) ? Assembly.Load(assemblyData, symbolsData) : Assembly.Load(assemblyData); } From 40643ffdefe2cb5e1bd36ee4a7a1a92a396ca0e5 Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Thu, 29 Aug 2024 21:12:07 -0400 Subject: [PATCH 15/65] Offload the load context caching to bepinex --- BepInEx.Core/Bootstrap/BaseChainloader.cs | 13 +++--- BepInEx.Core/Bootstrap/TypeLoader.cs | 41 +++++++++++++++++++ BepInEx.Core/Contract/IPluginLoadContext.cs | 2 +- .../BepInExPatcherLoadContext.cs | 22 ++-------- .../BepInExPluginLoadContext.cs | 24 ++--------- .../Patching/AssemblyPatcher.cs | 12 ++++-- 6 files changed, 64 insertions(+), 50 deletions(-) diff --git a/BepInEx.Core/Bootstrap/BaseChainloader.cs b/BepInEx.Core/Bootstrap/BaseChainloader.cs index c2e7c7e..c233d7e 100644 --- a/BepInEx.Core/Bootstrap/BaseChainloader.cs +++ b/BepInEx.Core/Bootstrap/BaseChainloader.cs @@ -146,7 +146,7 @@ public abstract class BaseChainloader /// Value holds info of the originating provider. These contexts are used by chainloader to /// load plugins. Plugins that are loaded successfully are added to . /// - public Dictionary LoadContexts { get; } = new(); + internal Dictionary LoadContexts { get; } = new(); /// /// Collection of error chainloader messages that occured during plugin loading. @@ -350,7 +350,7 @@ public abstract class BaseChainloader LoadPlugins(providers, provider: true); FinishedProviders?.Invoke(); - var plugins = TypeLoader.GetPluginsFromLoadContexts(LoadContexts.Keys, ToPluginInfo, HasBepinPluginType, "chainloader"); + var plugins = TypeLoader.GetPluginsFromLoadContexts(LoadContexts.Keys.Cast(), ToPluginInfo, HasBepinPluginType, "chainloader"); Logger.Log(LogLevel.Info, $"{plugins.Count} plugin{(plugins.Count == 1 ? "" : "s")} to load"); LoadPlugins(plugins, provider: false); Finished?.Invoke(); @@ -414,23 +414,24 @@ public abstract class BaseChainloader var pluginLoadContexts = ((BasePluginProvider)pluginInfo.Instance).GetPlugins(); foreach (IPluginLoadContext context in pluginLoadContexts) { - LoadContexts[context] = pluginInfo; + var cachedContext = new CachedPluginLoadContext(context); + LoadContexts[cachedContext] = pluginInfo; } ProviderLoaded?.Invoke(pluginInfo); } else { - pluginInfo.Source = LoadContexts[pluginInfo.LoadContext]; + pluginInfo.Source = LoadContexts[(CachedPluginLoadContext)pluginInfo.LoadContext]; pluginInfo.Instance = LoadPlugin(pluginInfo, ass); PluginLoaded?.Invoke(pluginInfo); - pluginInfo.LoadContext.Dispose(); + foreach (CachedPluginLoadContext context in LoadContexts.Keys) + context.Dispose(); } } catch (Exception ex) { invalidPlugins.Add(pluginInfo.Metadata.GUID); existingPluginInfos.Remove(pluginInfo.Metadata.GUID); - pluginInfo.LoadContext?.Dispose(); Logger.Log(LogLevel.Error, $"Error loading [{pluginInfo}]: {(ex is ReflectionTypeLoadException re ? TypeLoader.TypeLoadExceptionToString(re) : ex.ToString())}"); diff --git a/BepInEx.Core/Bootstrap/TypeLoader.cs b/BepInEx.Core/Bootstrap/TypeLoader.cs index b847c4e..d83e6d8 100644 --- a/BepInEx.Core/Bootstrap/TypeLoader.cs +++ b/BepInEx.Core/Bootstrap/TypeLoader.cs @@ -10,6 +10,47 @@ using Mono.Cecil; namespace BepInEx.Bootstrap; +internal class CachedPluginLoadContext : IPluginLoadContext, IDisposable +{ + public IPluginLoadContext PluginLoadContext { get; } + private byte[] assemblyData; + private byte[] assemblySymbolsData; + private readonly Dictionary files = new(); + + public CachedPluginLoadContext(IPluginLoadContext pluginLoadContext) + { + PluginLoadContext = pluginLoadContext; + } + + public string AssemblyIdentifier => PluginLoadContext.AssemblyIdentifier; + public string AssemblyHash => PluginLoadContext.AssemblyHash; + public byte[] GetAssemblyData() + { + return assemblyData ??= PluginLoadContext.GetAssemblyData(); + } + + public byte[] GetAssemblySymbolsData() + { + return assemblySymbolsData ??= PluginLoadContext.GetAssemblySymbolsData(); + } + + public byte[] GetFile(string relativePath) + { + if (!files.ContainsKey(relativePath)) + { + files[relativePath] = PluginLoadContext.GetFile(relativePath); + } + return files[relativePath]; + } + + public void Dispose() + { + assemblyData = null; + assemblySymbolsData = null; + files.Clear(); + } +} + /// /// A cacheable metadata item. Can be used with and /// to cache plugin metadata. diff --git a/BepInEx.Core/Contract/IPluginLoadContext.cs b/BepInEx.Core/Contract/IPluginLoadContext.cs index 013b973..1812ea8 100644 --- a/BepInEx.Core/Contract/IPluginLoadContext.cs +++ b/BepInEx.Core/Contract/IPluginLoadContext.cs @@ -6,7 +6,7 @@ namespace BepInEx; /// An interface that allows to dynamically load and track an assembly /// which can be obtained from a provider /// -public interface IPluginLoadContext : IDisposable +public interface IPluginLoadContext { /// /// An identifier that uniquely identifies an assembly from a provider no matter its revision diff --git a/BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs b/BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs index 3aca66f..be1ca0a 100644 --- a/BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs +++ b/BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs @@ -7,25 +7,15 @@ public class BepInExPatcherLoadContext : IPluginLoadContext { public string AssemblyIdentifier { get; internal set; } public string AssemblyHash { get; internal set; } - private byte[] assemblyData; - private byte[] assemblySymbolsData; public byte[] GetAssemblyData() { - if (assemblyData == null) - { - assemblyData = File.ReadAllBytes(AssemblyIdentifier); - } - - return assemblyData; + return File.ReadAllBytes(AssemblyIdentifier); } public byte[] GetAssemblySymbolsData() { - if (assemblySymbolsData == null) - { - if (!Utility.TryResolveDllSymbols(AssemblyIdentifier, out assemblySymbolsData)) - assemblySymbolsData = null; - } + if (Utility.TryResolveAssemblySymbols(AssemblyIdentifier, out var assemblySymbolsData)) + return assemblySymbolsData; return assemblySymbolsData; } @@ -39,10 +29,4 @@ public class BepInExPatcherLoadContext : IPluginLoadContext string filePath = Path.Combine(assemblyFolder, relativePath); return File.ReadAllBytes(filePath); } - - public void Dispose() - { - assemblyData = null; - assemblySymbolsData = null; - } } diff --git a/BepInEx.PluginProvider/BepInExPluginLoadContext.cs b/BepInEx.PluginProvider/BepInExPluginLoadContext.cs index 389e181..d7b0136 100644 --- a/BepInEx.PluginProvider/BepInExPluginLoadContext.cs +++ b/BepInEx.PluginProvider/BepInExPluginLoadContext.cs @@ -7,25 +7,15 @@ public class BepInExPluginLoadContext : IPluginLoadContext { public string AssemblyIdentifier { get; internal set; } public string AssemblyHash { get; internal set; } - private byte[] assemblyData; - private byte[] assemblySymbolsData; public byte[] GetAssemblyData() { - if (assemblyData == null) - { - assemblyData = File.ReadAllBytes(AssemblyIdentifier); - } - - return assemblyData; + return File.ReadAllBytes(AssemblyIdentifier); } - + public byte[] GetAssemblySymbolsData() { - if (assemblySymbolsData == null) - { - if (!Utility.TryResolveDllSymbols(AssemblyIdentifier, out assemblySymbolsData)) - assemblySymbolsData = null; - } + if (Utility.TryResolveAssemblySymbols(AssemblyIdentifier, out var assemblySymbolsData)) + return assemblySymbolsData; return assemblySymbolsData; } @@ -39,10 +29,4 @@ public class BepInExPluginLoadContext : IPluginLoadContext string filePath = Path.Combine(assemblyFolder, relativePath); return File.ReadAllBytes(filePath); } - - public void Dispose() - { - assemblyData = null; - assemblySymbolsData = null; - } } diff --git a/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs b/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs index a9822d9..64e04d8 100644 --- a/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs +++ b/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs @@ -25,7 +25,7 @@ public class AssemblyPatcher : IDisposable private Func assemblyLoader; - private readonly List loadContexts = new(); + private readonly List loadContexts = new(); private readonly List providers = new(); public AssemblyPatcher(Func assemblyLoader) @@ -164,7 +164,7 @@ public class AssemblyPatcher : IDisposable var instance = (BasePatcherProvider) Activator.CreateInstance(type); providers.Add(instance); - loadContexts.AddRange(instance.GetPatchers()); + loadContexts.AddRange(instance.GetPatchers().Select(x => new CachedPluginLoadContext(x))); } catch (Exception e) { @@ -174,7 +174,7 @@ public class AssemblyPatcher : IDisposable } } - var patchers = TypeLoader.GetPluginsFromLoadContexts(loadContexts, ToPatcherPlugin, HasPatcherType); + var patchers = TypeLoader.GetPluginsFromLoadContexts(loadContexts.Cast(), ToPatcherPlugin, HasPatcherType); var sortedPatchers = new List(); @@ -188,11 +188,15 @@ public class AssemblyPatcher : IDisposable var assName = ass.GetName(); Logger.Log(LogLevel.Debug, $"Loaded patcher {patcherPlugin.TypeName} from [{assName.Name} {assName.Version}]"); - patcherPlugin.LoadContext.Dispose(); } PatcherContext.PatchDefinitions.AddRange(sortedPatchers); Logger.Log(LogLevel.Info, $"Loaded {sortedPatchers.Count} patcher{(sortedPatchers.Count > 0 ? "s" : "")}"); + + foreach (CachedPluginLoadContext loadContext in loadContexts) + { + loadContext.Dispose(); + } } private Assembly PatcherProvidersAssemblyResolver(object sender, ResolveEventArgs args) From ba3fac94dab3796f87649d3ae96712da16f01977 Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Sat, 31 Aug 2024 05:07:51 -0400 Subject: [PATCH 16/65] Upgrade MonoMod to 22.7.31.1 to fix old mono preloader crashes (#973) --- BepInEx.Core/BepInEx.Core.csproj | 2 +- BepInEx.Preloader.Core/BepInEx.Preloader.Core.csproj | 4 ++-- README.md | 2 +- .../Unity/BepInEx.Unity.Common/BepInEx.Unity.Common.csproj | 2 +- .../Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/BepInEx.Core/BepInEx.Core.csproj b/BepInEx.Core/BepInEx.Core.csproj index 30a9e20..c2873d3 100644 --- a/BepInEx.Core/BepInEx.Core.csproj +++ b/BepInEx.Core/BepInEx.Core.csproj @@ -10,7 +10,7 @@ - + diff --git a/BepInEx.Preloader.Core/BepInEx.Preloader.Core.csproj b/BepInEx.Preloader.Core/BepInEx.Preloader.Core.csproj index b75a844..6002a99 100644 --- a/BepInEx.Preloader.Core/BepInEx.Preloader.Core.csproj +++ b/BepInEx.Preloader.Core/BepInEx.Preloader.Core.csproj @@ -9,7 +9,7 @@ - - + + diff --git a/README.md b/README.md index 95a4de4..18a1145 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ A more comprehensive comparison list of features and compatibility is available - [NeighTools/UnityDoorstop](https://github.com/NeighTools/UnityDoorstop) - v4.3.0 - [BepInEx/HarmonyX](https://github.com/BepInEx/HarmonyX) - v2.10.2 -- [0x0ade/MonoMod](https://github.com/0x0ade/MonoMod) - v22.5.1.1 +- [0x0ade/MonoMod](https://github.com/0x0ade/MonoMod) - v22.7.31.1 - [jbevain/cecil](https://github.com/jbevain/cecil) - v0.10.4 #### IL2CPP libraries diff --git a/Runtimes/Unity/BepInEx.Unity.Common/BepInEx.Unity.Common.csproj b/Runtimes/Unity/BepInEx.Unity.Common/BepInEx.Unity.Common.csproj index 78ebd3c..240e981 100644 --- a/Runtimes/Unity/BepInEx.Unity.Common/BepInEx.Unity.Common.csproj +++ b/Runtimes/Unity/BepInEx.Unity.Common/BepInEx.Unity.Common.csproj @@ -8,7 +8,7 @@ - + diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj index 32eb297..a8ebac2 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj @@ -21,7 +21,7 @@ - + From 4597be006e683ac6f4dc9165dd5ebd04c98cb32a Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Sat, 31 Aug 2024 05:54:41 -0400 Subject: [PATCH 17/65] Remove files caching from CachedPluginLoadContext, make it forward the call instead --- BepInEx.Core/Bootstrap/TypeLoader.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/BepInEx.Core/Bootstrap/TypeLoader.cs b/BepInEx.Core/Bootstrap/TypeLoader.cs index d83e6d8..0c8e256 100644 --- a/BepInEx.Core/Bootstrap/TypeLoader.cs +++ b/BepInEx.Core/Bootstrap/TypeLoader.cs @@ -15,7 +15,6 @@ internal class CachedPluginLoadContext : IPluginLoadContext, IDisposable public IPluginLoadContext PluginLoadContext { get; } private byte[] assemblyData; private byte[] assemblySymbolsData; - private readonly Dictionary files = new(); public CachedPluginLoadContext(IPluginLoadContext pluginLoadContext) { @@ -36,18 +35,13 @@ internal class CachedPluginLoadContext : IPluginLoadContext, IDisposable public byte[] GetFile(string relativePath) { - if (!files.ContainsKey(relativePath)) - { - files[relativePath] = PluginLoadContext.GetFile(relativePath); - } - return files[relativePath]; + return PluginLoadContext.GetFile(relativePath); } public void Dispose() { assemblyData = null; assemblySymbolsData = null; - files.Clear(); } } From 24cf635e6555004b04ab93b18ebcccf4dec9a3ca Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Sun, 1 Sep 2024 09:29:35 -0400 Subject: [PATCH 18/65] Ensure providers and plugins / patchers folders are created if they don't exist --- BepInEx.Core/Bootstrap/BaseChainloader.cs | 6 +++--- BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/BepInEx.Core/Bootstrap/BaseChainloader.cs b/BepInEx.Core/Bootstrap/BaseChainloader.cs index c233d7e..2d57639 100644 --- a/BepInEx.Core/Bootstrap/BaseChainloader.cs +++ b/BepInEx.Core/Bootstrap/BaseChainloader.cs @@ -187,12 +187,12 @@ public abstract class BaseChainloader InitializeLoggers(); + if (!Directory.Exists(Paths.PluginProviderPath)) + Directory.CreateDirectory(Paths.PluginProviderPath); + if (!Directory.Exists(Paths.PluginPath)) Directory.CreateDirectory(Paths.PluginPath); - if (!Directory.Exists(Paths.PatcherPluginPath)) - Directory.CreateDirectory(Paths.PatcherPluginPath); - _initialized = true; Logger.Log(LogLevel.Message, "Chainloader initialized"); diff --git a/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs b/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs index 64e04d8..3022e6a 100644 --- a/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs +++ b/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs @@ -31,6 +31,12 @@ public class AssemblyPatcher : IDisposable public AssemblyPatcher(Func assemblyLoader) { this.assemblyLoader = assemblyLoader; + + if (!Directory.Exists(Paths.PatcherProviderPath)) + Directory.CreateDirectory(Paths.PatcherProviderPath); + + if (!Directory.Exists(Paths.PatcherPluginPath)) + Directory.CreateDirectory(Paths.PatcherPluginPath); } /// From 5a324246f16c47bdd38aeb3078c80d785c7b8e00 Mon Sep 17 00:00:00 2001 From: thegu5 <58223632+thegu5@users.noreply.github.com> Date: Sun, 1 Sep 2024 13:09:38 -0400 Subject: [PATCH 19/65] Bump Cpp2IL (#967) The currently used version doesn't have support for metadata version 29.2 (Unity 2023.2+) and 31 (2022.3.33+) --- Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj index a8ebac2..bfc326a 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj @@ -22,7 +22,7 @@ - + From 0a4f1d6d7320fa4af991fa062fc20df2985f3dbd Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Sun, 1 Sep 2024 17:14:59 -0400 Subject: [PATCH 20/65] Accomodate breaking changes in preparation for api refactors --- BepInEx.Core/Contract/Attributes.cs | 4 ++-- BepInEx.Core/Contract/IPlugin.cs | 2 +- Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs | 6 +++--- Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs | 6 +++--- Runtimes/Unity/BepInEx.Unity.Mono/BaseUnityPlugin.cs | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/BepInEx.Core/Contract/Attributes.cs b/BepInEx.Core/Contract/Attributes.cs index f9d495c..793f456 100644 --- a/BepInEx.Core/Contract/Attributes.cs +++ b/BepInEx.Core/Contract/Attributes.cs @@ -290,7 +290,7 @@ public static class MetadataHelper /// /// The plugin type. /// The BepInPlugin metadata of the plugin type. - public static BepInPlugin GetPluginMetadata(Type pluginType) + public static BepInPlugin GetMetadata(Type pluginType) { var attributes = pluginType.GetCustomAttributes(typeof(BepInPlugin), false); @@ -305,7 +305,7 @@ public static class MetadataHelper /// /// The plugin instance. /// The BepInPlugin metadata of the plugin instance. - public static BepInPlugin GetPluginMetadata(object plugin) => GetPluginMetadata(plugin.GetType()); + public static BepInPlugin GetMetadata(object plugin) => GetMetadata(plugin.GetType()); /// /// Retrieves the BepInPluginProvider metadata from a plugin type. diff --git a/BepInEx.Core/Contract/IPlugin.cs b/BepInEx.Core/Contract/IPlugin.cs index 84cc6b5..6963150 100644 --- a/BepInEx.Core/Contract/IPlugin.cs +++ b/BepInEx.Core/Contract/IPlugin.cs @@ -13,7 +13,7 @@ internal interface IPlugin /// /// Logger instance tied to this plugin. /// - ManualLogSource Logger { get; } + ManualLogSource Log { get; } /// /// Default config file tied to this plugin. The config file will not be created until diff --git a/Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs b/Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs index bbd52eb..4356862 100644 --- a/Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs +++ b/Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs @@ -11,7 +11,7 @@ namespace BepInEx.NET.Common { protected BasePlugin() { - var metadata = MetadataHelper.GetPluginMetadata(this); + var metadata = MetadataHelper.GetMetadata(this); HarmonyInstance = new Harmony("BepInEx.Plugin." + metadata.GUID); @@ -25,7 +25,7 @@ namespace BepInEx.NET.Common throw new InvalidOperationException($"The plugin information for {metadata.GUID} couldn't be found on the chainloader"); } - Logger = BepInEx.Logging.Logger.CreateLogSource(metadata.Name); + Log = BepInEx.Logging.Logger.CreateLogSource(metadata.Name); Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, metadata.GUID + ".cfg"), false, metadata); } @@ -34,7 +34,7 @@ namespace BepInEx.NET.Common public PluginInfo Info { get; } /// - public ManualLogSource Logger { get; } + public ManualLogSource Log { get; } /// public ConfigFile Config { get; } diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs index e6889da..b9d90c0 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs @@ -11,7 +11,7 @@ public abstract class BasePlugin : IPlugin { protected BasePlugin() { - var metadata = MetadataHelper.GetPluginMetadata(this); + var metadata = MetadataHelper.GetMetadata(this); if (BaseChainloader.Instance.Plugins.TryGetValue(metadata.GUID, out var pluginInfo)) { @@ -23,14 +23,14 @@ public abstract class BasePlugin : IPlugin throw new InvalidOperationException($"The plugin information for {metadata.GUID} couldn't be found on the chainloader"); } - Logger = BepInEx.Logging.Logger.CreateLogSource(metadata.Name); + Log = BepInEx.Logging.Logger.CreateLogSource(metadata.Name); Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, metadata.GUID + ".cfg"), false, metadata); } public PluginInfo Info { get; } - public ManualLogSource Logger { get; } + public ManualLogSource Log { get; } public ConfigFile Config { get; } diff --git a/Runtimes/Unity/BepInEx.Unity.Mono/BaseUnityPlugin.cs b/Runtimes/Unity/BepInEx.Unity.Mono/BaseUnityPlugin.cs index f7ea012..3076c2a 100644 --- a/Runtimes/Unity/BepInEx.Unity.Mono/BaseUnityPlugin.cs +++ b/Runtimes/Unity/BepInEx.Unity.Mono/BaseUnityPlugin.cs @@ -10,7 +10,7 @@ namespace BepInEx.Unity.Mono; /// /// The base plugin type that is used by the BepInEx plugin loader. /// -public abstract class BaseUnityPlugin : MonoBehaviour, IPlugin +public abstract class BaseUnityPlugin : MonoBehaviour { /// /// Create a new instance of a plugin and all of its tied in objects. @@ -18,7 +18,7 @@ public abstract class BaseUnityPlugin : MonoBehaviour, IPlugin /// BepInPlugin attribute is missing. protected BaseUnityPlugin() { - var metadata = MetadataHelper.GetPluginMetadata(this); + var metadata = MetadataHelper.GetMetadata(this); if (metadata == null) throw new InvalidOperationException("Can't create an instance of " + GetType().FullName + " because it inherits from BaseUnityPlugin and the BepInPlugin attribute is missing."); From e9337e9982df31d8ffea92466bfa5a37bc34c574 Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Sun, 1 Sep 2024 17:35:53 -0400 Subject: [PATCH 21/65] Revert the removal of the il2cpp chainloader instance --- Runtimes/Unity/BepInEx.Unity.IL2CPP/IL2CPPChainloader.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/IL2CPPChainloader.cs b/Runtimes/Unity/BepInEx.Unity.IL2CPP/IL2CPPChainloader.cs index b493789..e27c3cd 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/IL2CPPChainloader.cs +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/IL2CPPChainloader.cs @@ -32,6 +32,8 @@ public class IL2CPPChainloader : BaseChainloader private static INativeDetour RuntimeInvokeDetour { get; set; } + public static new IL2CPPChainloader Instance { get; set; } + /// /// Register and add a Unity Component (for example MonoBehaviour) into BepInEx global manager. /// Automatically registers the type with Il2Cpp type system if it isn't initialised already. From d44b93ba93ae44658a7992e6e44c878849c747f7 Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Sun, 1 Sep 2024 22:56:30 -0400 Subject: [PATCH 22/65] Use the IL2CPPChainLoader instance for il2cpp to preserve compatibility --- Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs | 2 +- Runtimes/Unity/BepInEx.Unity.IL2CPP/IL2CPPChainloader.cs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs index b9d90c0..825526b 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs @@ -13,7 +13,7 @@ public abstract class BasePlugin : IPlugin { var metadata = MetadataHelper.GetMetadata(this); - if (BaseChainloader.Instance.Plugins.TryGetValue(metadata.GUID, out var pluginInfo)) + if (IL2CPPChainloader.Instance.Plugins.TryGetValue(metadata.GUID, out var pluginInfo)) { Info = pluginInfo; Info.Instance = this; diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/IL2CPPChainloader.cs b/Runtimes/Unity/BepInEx.Unity.IL2CPP/IL2CPPChainloader.cs index e27c3cd..390e845 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/IL2CPPChainloader.cs +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/IL2CPPChainloader.cs @@ -32,7 +32,11 @@ public class IL2CPPChainloader : BaseChainloader private static INativeDetour RuntimeInvokeDetour { get; set; } - public static new IL2CPPChainloader Instance { get; set; } + public static new IL2CPPChainloader Instance + { + get => (IL2CPPChainloader)BaseChainloader.Instance; + set => BaseChainloader.Instance = value; + } /// /// Register and add a Unity Component (for example MonoBehaviour) into BepInEx global manager. From e0a81dfdbd18965e290ed69c4d1c48b5969bf98a Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Mon, 2 Sep 2024 07:16:13 -0400 Subject: [PATCH 23/65] Rename Location to prevent compilation with it --- BepInEx.Core/Bootstrap/BaseChainloader.cs | 8 ++++---- BepInEx.Core/Contract/PluginInfo.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/BepInEx.Core/Bootstrap/BaseChainloader.cs b/BepInEx.Core/Bootstrap/BaseChainloader.cs index 2d57639..bf8646b 100644 --- a/BepInEx.Core/Bootstrap/BaseChainloader.cs +++ b/BepInEx.Core/Bootstrap/BaseChainloader.cs @@ -91,7 +91,7 @@ public abstract class BaseChainloader Incompatibilities = incompatibilities, TypeName = type.FullName, TargettedBepInExVersion = bepinVersion, - Location = assemblyLocation, + _location = assemblyLocation, LoadContext = loadContext }; } @@ -390,10 +390,10 @@ public abstract class BaseChainloader Logger.Log(LogLevel.Info, $"Loading [{pluginInfo}]"); Assembly ass = null; - if (provider && !loadedAssemblies.TryGetValue(pluginInfo.Location, out ass)) + if (provider && !loadedAssemblies.TryGetValue(pluginInfo._location, out ass)) { - ass = Assembly.LoadFrom(pluginInfo.Location); - loadedAssemblies[pluginInfo.Location] = ass; + ass = Assembly.LoadFrom(pluginInfo._location); + loadedAssemblies[pluginInfo._location] = ass; } else if (!provider && !loadedAssemblies.TryGetValue(pluginInfo.LoadContext.AssemblyIdentifier, out ass)) { diff --git a/BepInEx.Core/Contract/PluginInfo.cs b/BepInEx.Core/Contract/PluginInfo.cs index 26e5cae..02a8f59 100644 --- a/BepInEx.Core/Contract/PluginInfo.cs +++ b/BepInEx.Core/Contract/PluginInfo.cs @@ -53,7 +53,7 @@ public class PluginInfo : ICacheable internal Version TargettedBepInExVersion { get; set; } - internal string Location { get; set; } + internal string _location { get; set; } /// public override string ToString() => $"{Metadata?.Name} {Metadata?.Version}"; From c02ad9cd68ff4a08b53e145c7db99748e98bd935 Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Mon, 2 Sep 2024 07:23:10 -0400 Subject: [PATCH 24/65] Add an obsolete property for compatibility --- BepInEx.Core/Contract/PluginInfo.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/BepInEx.Core/Contract/PluginInfo.cs b/BepInEx.Core/Contract/PluginInfo.cs index 02a8f59..3c5dfc8 100644 --- a/BepInEx.Core/Contract/PluginInfo.cs +++ b/BepInEx.Core/Contract/PluginInfo.cs @@ -53,6 +53,9 @@ public class PluginInfo : ICacheable internal Version TargettedBepInExVersion { get; set; } + [Obsolete("Use LoadContext.AssemblyIdentifier instead", true)] + public string Location => LoadContext?.AssemblyIdentifier; + internal string _location { get; set; } /// From 6b38cee1277f4e3da5f2f685aee8b503a14d4054 Mon Sep 17 00:00:00 2001 From: ManlyMarco <39247311+ManlyMarco@users.noreply.github.com> Date: Mon, 2 Sep 2024 13:26:49 +0200 Subject: [PATCH 25/65] Fix failing to initialize if game's Data folder is just 'Data' (#975) Any Unity game can have its _Data folder renamed to just Data and continue working just fine. --- BepInEx.Core/Paths.cs | 22 +++++++++++++++---- .../Il2CppInteropManager.cs | 3 +-- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/BepInEx.Core/Paths.cs b/BepInEx.Core/Paths.cs index 34d3e98..6a37342 100644 --- a/BepInEx.Core/Paths.cs +++ b/BepInEx.Core/Paths.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using System.Linq; using System.Reflection; using MonoMod.Utils; @@ -106,9 +106,23 @@ public static class Paths ? Utility.ParentDirectory(executablePath, 4) : Path.GetDirectoryName(executablePath); - GameDataPath = managedPath != null && gameDataRelativeToManaged - ? Path.GetDirectoryName(managedPath) - : Path.Combine(GameRootPath, $"{ProcessName}_Data"); + if (managedPath != null && gameDataRelativeToManaged) + { + GameDataPath = Path.GetDirectoryName(managedPath); + } + else + { + // According to some experiments, Unity checks whether globalgamemanagers/data.unity3d exists in the data folder before picking it. + // 'ProcessName_Data' folder is checked first, then if that fails 'Data' folder is checked. If neither is valid, the player crashes. + // A simple Directory.Exists check is accurate enough while being less likely to break in case these conditions change. + GameDataPath = Path.Combine(GameRootPath, $"{ProcessName}_Data"); + if (!Directory.Exists(GameDataPath)) + GameDataPath = Path.Combine(GameRootPath, "Data"); + } + + if (string.IsNullOrEmpty(GameDataPath) || !Directory.Exists(GameDataPath)) + throw new DirectoryNotFoundException("Failed to extract valid GameDataPath from executablePath: " + executablePath); + ManagedPath = managedPath ?? Path.Combine(GameDataPath, "Managed"); BepInExRootPath = bepinRootPath ?? Path.Combine(GameRootPath, "BepInEx"); ConfigPath = Path.Combine(BepInExRootPath, "config"); diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs b/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs index cc43fac..4e2761a 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs @@ -281,8 +281,7 @@ internal static partial class Il2CppInteropManager { Logger.LogMessage("Running Cpp2IL to generate dummy assemblies"); - var metadataPath = Path.Combine(Paths.GameRootPath, - $"{Paths.ProcessName}_Data", + var metadataPath = Path.Combine(Paths.GameDataPath, "il2cpp_data", "Metadata", "global-metadata.dat"); From 7315b7dd664f13e073f88c8cb41e3aab605c318b Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Mon, 2 Sep 2024 14:16:12 -0400 Subject: [PATCH 26/65] Make the default providers classes internal in their assemblies --- BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs | 2 +- BepInEx.PatcherProvider/BepInExPatcherProvider.cs | 2 +- BepInEx.PluginProvider/BepInExPluginLoadContext.cs | 2 +- BepInEx.PluginProvider/BepInExPluginProvider.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs b/BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs index be1ca0a..9cb4cbc 100644 --- a/BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs +++ b/BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs @@ -3,7 +3,7 @@ using System.IO; namespace BepInEx.PatcherProvider; -public class BepInExPatcherLoadContext : IPluginLoadContext +internal class BepInExPatcherLoadContext : IPluginLoadContext { public string AssemblyIdentifier { get; internal set; } public string AssemblyHash { get; internal set; } diff --git a/BepInEx.PatcherProvider/BepInExPatcherProvider.cs b/BepInEx.PatcherProvider/BepInExPatcherProvider.cs index 78fa699..5ae4555 100644 --- a/BepInEx.PatcherProvider/BepInExPatcherProvider.cs +++ b/BepInEx.PatcherProvider/BepInExPatcherProvider.cs @@ -8,7 +8,7 @@ using BepInEx.Preloader.Core.Patching; namespace BepInEx.PatcherProvider; [PatcherProviderPluginInfo("BepInExPatcherProvider", "BepInExPatcherProvider", "1.0")] -public class BepInExPatcherProvider : BasePatcherProvider +internal class BepInExPatcherProvider : BasePatcherProvider { private static readonly Dictionary AssemblyLocationsByFilename = new(); diff --git a/BepInEx.PluginProvider/BepInExPluginLoadContext.cs b/BepInEx.PluginProvider/BepInExPluginLoadContext.cs index d7b0136..354e972 100644 --- a/BepInEx.PluginProvider/BepInExPluginLoadContext.cs +++ b/BepInEx.PluginProvider/BepInExPluginLoadContext.cs @@ -3,7 +3,7 @@ using System.IO; namespace BepInEx.PluginProvider; -public class BepInExPluginLoadContext : IPluginLoadContext +internal class BepInExPluginLoadContext : IPluginLoadContext { public string AssemblyIdentifier { get; internal set; } public string AssemblyHash { get; internal set; } diff --git a/BepInEx.PluginProvider/BepInExPluginProvider.cs b/BepInEx.PluginProvider/BepInExPluginProvider.cs index bae7689..6520974 100644 --- a/BepInEx.PluginProvider/BepInExPluginProvider.cs +++ b/BepInEx.PluginProvider/BepInExPluginProvider.cs @@ -7,7 +7,7 @@ using BepInEx.Logging; namespace BepInEx.PluginProvider; [BepInPluginProvider("BepInExPluginProvider", "BepInExPluginProvider", "1.0")] -public class BepInExPluginProvider : BasePluginProvider +internal class BepInExPluginProvider : BasePluginProvider { private static readonly Dictionary AssemblyLocationsByFilename = new(); From be1beb5e135b5754ea89e0ec5b2e94a4d1438e44 Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Tue, 3 Sep 2024 20:34:17 -0400 Subject: [PATCH 27/65] Address code review round 5 --- BepInEx.Core/BepInEx.Core.csproj | 3 ++ BepInEx.Core/Bootstrap/BaseChainloader.cs | 8 ++++ BepInEx.Core/Contract/IPlugin.cs | 39 +++++++++++-------- BepInEx.Core/Utility.cs | 3 +- .../BepInEx.PatcherProvider.csproj | 1 + .../BepInEx.PluginProvider.csproj | 1 + Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs | 3 +- .../Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs | 4 +- .../BepInEx.Unity.Mono/BaseUnityPlugin.cs | 1 - .../Bootstrap/UnityChainloader.cs | 6 +++ 10 files changed, 44 insertions(+), 25 deletions(-) diff --git a/BepInEx.Core/BepInEx.Core.csproj b/BepInEx.Core/BepInEx.Core.csproj index c4f5cd0..30a9e20 100644 --- a/BepInEx.Core/BepInEx.Core.csproj +++ b/BepInEx.Core/BepInEx.Core.csproj @@ -12,6 +12,9 @@ + + + diff --git a/BepInEx.Core/Bootstrap/BaseChainloader.cs b/BepInEx.Core/Bootstrap/BaseChainloader.cs index bf8646b..6b62ffd 100644 --- a/BepInEx.Core/Bootstrap/BaseChainloader.cs +++ b/BepInEx.Core/Bootstrap/BaseChainloader.cs @@ -19,6 +19,8 @@ public abstract class BaseChainloader private static Regex allowedGuidRegex { get; } = new(@"^[a-zA-Z0-9\._\-]+$"); + internal BaseChainloader() { } + /// /// Analyzes the given type definition and attempts to convert it to a valid /// @@ -369,6 +371,12 @@ public abstract class BaseChainloader Logger.Log(LogLevel.Message, "Chainloader startup complete"); } + [Obsolete("Use a plugin provider instead", true)] + public IList LoadPlugins(params string[] pluginsPaths) + { + throw new NotSupportedException("Use a plugin provider instead"); + } + private void LoadPlugins(List plugins, bool provider) { var existingPluginInfos = provider ? Providers : Plugins; diff --git a/BepInEx.Core/Contract/IPlugin.cs b/BepInEx.Core/Contract/IPlugin.cs index 6963150..150d151 100644 --- a/BepInEx.Core/Contract/IPlugin.cs +++ b/BepInEx.Core/Contract/IPlugin.cs @@ -1,23 +1,28 @@ -using BepInEx.Configuration; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using BepInEx.Configuration; using BepInEx.Logging; -namespace BepInEx.Contract; - -internal interface IPlugin +namespace BepInEx.Contract { - /// - /// Information about this plugin as it was loaded. - /// - PluginInfo Info { get; } + public interface IPlugin + { + /// + /// Information about this plugin as it was loaded. + /// + PluginInfo Info { get; } - /// - /// Logger instance tied to this plugin. - /// - ManualLogSource Log { get; } + /// + /// Logger instance tied to this plugin. + /// + ManualLogSource Logger { get; } - /// - /// Default config file tied to this plugin. The config file will not be created until - /// any settings are added and changed, or is called. - /// - ConfigFile Config { get; } + /// + /// Default config file tied to this plugin. The config file will not be created until + /// any settings are added and changed, or is called. + /// + ConfigFile Config { get; } + } } diff --git a/BepInEx.Core/Utility.cs b/BepInEx.Core/Utility.cs index 3ba6f1f..08e6c35 100644 --- a/BepInEx.Core/Utility.cs +++ b/BepInEx.Core/Utility.cs @@ -229,8 +229,6 @@ public static class Utility /// True, if the symbols were found. Otherwise, false. public static bool TryResolveAssemblySymbols(string dllPath, out byte[] symbolsData) { - symbolsData = null; - var subDirectory = Path.GetDirectoryName(dllPath); var potentialSymbolsFileNames = new[] @@ -257,6 +255,7 @@ public static class Utility return true; } + symbolsData = null; return false; } diff --git a/BepInEx.PatcherProvider/BepInEx.PatcherProvider.csproj b/BepInEx.PatcherProvider/BepInEx.PatcherProvider.csproj index a56204f..c37d3e6 100644 --- a/BepInEx.PatcherProvider/BepInEx.PatcherProvider.csproj +++ b/BepInEx.PatcherProvider/BepInEx.PatcherProvider.csproj @@ -4,6 +4,7 @@ net35;netstandard2.0 true BepInEx patcher provider module + false diff --git a/BepInEx.PluginProvider/BepInEx.PluginProvider.csproj b/BepInEx.PluginProvider/BepInEx.PluginProvider.csproj index ee9c085..e22ed74 100644 --- a/BepInEx.PluginProvider/BepInEx.PluginProvider.csproj +++ b/BepInEx.PluginProvider/BepInEx.PluginProvider.csproj @@ -4,6 +4,7 @@ net35;netstandard2.0 true BepInEx plugin provider module + false diff --git a/Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs b/Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs index 4356862..d9b375a 100644 --- a/Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs +++ b/Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs @@ -1,13 +1,12 @@ using System; using BepInEx.Bootstrap; using BepInEx.Configuration; -using BepInEx.Contract; using BepInEx.Logging; using HarmonyLib; namespace BepInEx.NET.Common { - public abstract class BasePlugin : IPlugin + public abstract class BasePlugin { protected BasePlugin() { diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs index 825526b..1a0fda3 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs @@ -1,13 +1,11 @@ using System; -using BepInEx.Bootstrap; using BepInEx.Configuration; -using BepInEx.Contract; using BepInEx.Logging; using Il2CppInterop.Runtime.InteropTypes; namespace BepInEx.Unity.IL2CPP; -public abstract class BasePlugin : IPlugin +public abstract class BasePlugin { protected BasePlugin() { diff --git a/Runtimes/Unity/BepInEx.Unity.Mono/BaseUnityPlugin.cs b/Runtimes/Unity/BepInEx.Unity.Mono/BaseUnityPlugin.cs index 3076c2a..21fde4f 100644 --- a/Runtimes/Unity/BepInEx.Unity.Mono/BaseUnityPlugin.cs +++ b/Runtimes/Unity/BepInEx.Unity.Mono/BaseUnityPlugin.cs @@ -1,7 +1,6 @@ using System; using BepInEx.Bootstrap; using BepInEx.Configuration; -using BepInEx.Contract; using BepInEx.Logging; using UnityEngine; diff --git a/Runtimes/Unity/BepInEx.Unity.Mono/Bootstrap/UnityChainloader.cs b/Runtimes/Unity/BepInEx.Unity.Mono/Bootstrap/UnityChainloader.cs index 1c163b9..19769e8 100644 --- a/Runtimes/Unity/BepInEx.Unity.Mono/Bootstrap/UnityChainloader.cs +++ b/Runtimes/Unity/BepInEx.Unity.Mono/Bootstrap/UnityChainloader.cs @@ -42,6 +42,12 @@ public class UnityChainloader : BaseChainloader /// public static GameObject ManagerObject { get; private set; } + public static new UnityChainloader Instance + { + get => (UnityChainloader)BaseChainloader.Instance; + set => BaseChainloader.Instance = value; + } + protected override string ConsoleTitle => _consoleTitle; From d389ed8067e72a7bbc6374801a832c3e84d7a2d2 Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Tue, 3 Sep 2024 21:09:52 -0400 Subject: [PATCH 28/65] Add versioning to CachedAssembly --- BepInEx.Core/Bootstrap/TypeLoader.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/BepInEx.Core/Bootstrap/TypeLoader.cs b/BepInEx.Core/Bootstrap/TypeLoader.cs index 0c8e256..eab5723 100644 --- a/BepInEx.Core/Bootstrap/TypeLoader.cs +++ b/BepInEx.Core/Bootstrap/TypeLoader.cs @@ -70,6 +70,11 @@ public interface ICacheable /// public class CachedAssembly where T : ICacheable { + /// + /// The version of the cache which increments on each format changes + /// + public static int Version => 0; + /// /// List of cached items inside the assembly. /// @@ -305,7 +310,9 @@ public static class TypeLoader if (!File.Exists(path)) return null; - using (var br = new BinaryReader(File.OpenRead(path))) + using var br = new BinaryReader(File.OpenRead(path)); + var version = br.ReadInt32(); + if (version == CachedAssembly.Version) { var entriesCount = br.ReadInt32(); From ae4d7bb34cee7358da9128928d180e24528fa8e0 Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Wed, 4 Sep 2024 11:46:18 -0400 Subject: [PATCH 29/65] Actually write the cache version in SaveAssemblyCache --- BepInEx.Core/Bootstrap/TypeLoader.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/BepInEx.Core/Bootstrap/TypeLoader.cs b/BepInEx.Core/Bootstrap/TypeLoader.cs index eab5723..ed428b8 100644 --- a/BepInEx.Core/Bootstrap/TypeLoader.cs +++ b/BepInEx.Core/Bootstrap/TypeLoader.cs @@ -366,6 +366,7 @@ public static class TypeLoader var path = Path.Combine(Paths.CachePath, $"{cacheName}_typeloader.dat"); using var bw = new BinaryWriter(File.OpenWrite(path)); + bw.Write(CachedAssembly.Version); bw.Write(entries.Count); foreach (var kv in entries) From ede28de4a5a09451733f86475625ff20ceb37f2c Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Wed, 4 Sep 2024 11:54:53 -0400 Subject: [PATCH 30/65] Make the cache version a const --- BepInEx.Core/Bootstrap/TypeLoader.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/BepInEx.Core/Bootstrap/TypeLoader.cs b/BepInEx.Core/Bootstrap/TypeLoader.cs index ed428b8..7fb173f 100644 --- a/BepInEx.Core/Bootstrap/TypeLoader.cs +++ b/BepInEx.Core/Bootstrap/TypeLoader.cs @@ -73,7 +73,7 @@ public class CachedAssembly where T : ICacheable /// /// The version of the cache which increments on each format changes /// - public static int Version => 0; + public const int VERSION = 0; /// /// List of cached items inside the assembly. @@ -312,7 +312,7 @@ public static class TypeLoader using var br = new BinaryReader(File.OpenRead(path)); var version = br.ReadInt32(); - if (version == CachedAssembly.Version) + if (version == CachedAssembly.VERSION) { var entriesCount = br.ReadInt32(); @@ -366,7 +366,7 @@ public static class TypeLoader var path = Path.Combine(Paths.CachePath, $"{cacheName}_typeloader.dat"); using var bw = new BinaryWriter(File.OpenWrite(path)); - bw.Write(CachedAssembly.Version); + bw.Write(CachedAssembly.VERSION); bw.Write(entries.Count); foreach (var kv in entries) From 2474dce1a13f018174941bbd812c0ee606c63a5e Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Wed, 4 Sep 2024 11:58:52 -0400 Subject: [PATCH 31/65] Use PascalCase for the cache versioning const --- BepInEx.Core/Bootstrap/TypeLoader.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/BepInEx.Core/Bootstrap/TypeLoader.cs b/BepInEx.Core/Bootstrap/TypeLoader.cs index 7fb173f..63c1295 100644 --- a/BepInEx.Core/Bootstrap/TypeLoader.cs +++ b/BepInEx.Core/Bootstrap/TypeLoader.cs @@ -73,7 +73,7 @@ public class CachedAssembly where T : ICacheable /// /// The version of the cache which increments on each format changes /// - public const int VERSION = 0; + public const int Version = 0; /// /// List of cached items inside the assembly. @@ -312,7 +312,7 @@ public static class TypeLoader using var br = new BinaryReader(File.OpenRead(path)); var version = br.ReadInt32(); - if (version == CachedAssembly.VERSION) + if (version == CachedAssembly.Version) { var entriesCount = br.ReadInt32(); @@ -366,7 +366,7 @@ public static class TypeLoader var path = Path.Combine(Paths.CachePath, $"{cacheName}_typeloader.dat"); using var bw = new BinaryWriter(File.OpenWrite(path)); - bw.Write(CachedAssembly.VERSION); + bw.Write(CachedAssembly.Version); bw.Write(entries.Count); foreach (var kv in entries) From cbd70eb2f085090751af32626f6c0f12c06efa4f Mon Sep 17 00:00:00 2001 From: ManlyMarco <39247311+ManlyMarco@users.noreply.github.com> Date: Sun, 8 Sep 2024 15:45:19 +0200 Subject: [PATCH 32/65] Automatically load all interop assemblies before loading plugins (#777) Assemblies are often loaded on the il2cpp side but not on the mono side, causing confusion when using reflection. This will load all interop assemblies right before plugins are loaded, which should mostly avoid these issues at a low time cost of around 40ms. --- .../BepInEx.Unity.IL2CPP/IL2CPPChainloader.cs | 2 + .../Il2CppInteropManager.cs | 38 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/IL2CPPChainloader.cs b/Runtimes/Unity/BepInEx.Unity.IL2CPP/IL2CPPChainloader.cs index 390e845..bc130b6 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/IL2CPPChainloader.cs +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/IL2CPPChainloader.cs @@ -97,6 +97,8 @@ public class IL2CPPChainloader : BaseChainloader unhook = true; + Il2CppInteropManager.PreloadInteropAssemblies(); + Instance.Execute(); } catch (Exception ex) diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs b/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs index 4e2761a..61aa583 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs @@ -8,6 +8,8 @@ using System.Reflection; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; using BepInEx.Configuration; using BepInEx.Logging; using BepInEx.Unity.Common; @@ -88,6 +90,14 @@ internal static partial class Il2CppInteropManager .AppendLine("{BepInEx} - Path to the BepInEx folder.") .AppendLine("{ProcessName} - Name of the current process") .ToString()); + + private static readonly ConfigEntry PreloadIL2CPPInteropAssemblies = ConfigFile.CoreConfig.Bind( + "IL2CPP", "PreloadIL2CPPInteropAssemblies", + true, + new StringBuilder() + .AppendLine("Automatically load all interop assemblies right before loading plugins.") + .AppendLine("Some plugins may not work properly without this, but it may cause issues in some games.") + .ToString()); private static readonly ManualLogSource Logger = BepInEx.Logging.Logger.CreateLogSource("InteropManager"); @@ -357,4 +367,32 @@ internal static partial class Il2CppInteropManager sourceAssemblies.Do(x => x.Dispose()); } + + internal static void PreloadInteropAssemblies() + { + if (!PreloadIL2CPPInteropAssemblies.Value) + return; + + var sw = Stopwatch.StartNew(); + + var files = Directory.EnumerateFiles(IL2CPPInteropAssemblyPath); + var loaded = 0; + Parallel.ForEach(files, file => + { + if (!file.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) return; + if (file.Equals("netstandard.dll", StringComparison.OrdinalIgnoreCase)) return; + if (file.Equals("Il2Cppnetstandard.dll", StringComparison.OrdinalIgnoreCase)) return; + try + { + Assembly.LoadFrom(file); + Interlocked.Increment(ref loaded); + } + catch (Exception e) + { + Logger.LogWarning($"Failed to preload {file} - {e}"); + } + }); + + Logger.LogDebug($"Preloaded {loaded} interop assemblies in {sw.ElapsedMilliseconds}ms"); + } } From f800cbcc43f774d3adb0b1a9c264f0326a5d9f38 Mon Sep 17 00:00:00 2001 From: ManlyMarco <39247311+ManlyMarco@users.noreply.github.com> Date: Sun, 8 Sep 2024 16:09:44 +0200 Subject: [PATCH 33/65] Add GlobalMetadataPath setting (#978) --- .../Il2CppInteropManager.cs | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs b/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs index 61aa583..1ebf4c9 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs @@ -99,6 +99,17 @@ internal static partial class Il2CppInteropManager .AppendLine("Some plugins may not work properly without this, but it may cause issues in some games.") .ToString()); + private static readonly ConfigEntry GlobalMetadataPath = ConfigFile.CoreConfig.Bind( + "IL2CPP", "GlobalMetadataPath", + "{GameDataPath}/il2cpp_data/Metadata/global-metadata.dat", + new StringBuilder() + .AppendLine("The path to the IL2CPP metadata file.") + .AppendLine("Supports the following placeholders:") + .AppendLine("{BepInEx} - Path to the BepInEx folder.") + .AppendLine("{ProcessName} - Name of the current process") + .AppendLine("{GameDataPath} - Path to the game's Data folder.") + .ToString()); + private static readonly ManualLogSource Logger = BepInEx.Logging.Logger.CreateLogSource("InteropManager"); private static string il2cppInteropBasePath; @@ -289,12 +300,13 @@ internal static partial class Il2CppInteropManager private static List RunCpp2Il() { - Logger.LogMessage("Running Cpp2IL to generate dummy assemblies"); - - var metadataPath = Path.Combine(Paths.GameDataPath, - "il2cpp_data", - "Metadata", - "global-metadata.dat"); + var metadataPath = Path.Combine(Paths.GameRootPath, + GlobalMetadataPath.Value + .Replace("{BepInEx}", Paths.BepInExRootPath) + .Replace("{ProcessName}", Paths.ProcessName) + .Replace("{GameDataPath}", Paths.GameDataPath)); + + Logger.LogMessage("Running Cpp2IL to generate dummy assemblies from " + metadataPath); var stopwatch = new Stopwatch(); stopwatch.Start(); From cdeaad7868599c074894dfacfe50ec307b453244 Mon Sep 17 00:00:00 2001 From: ManlyMarco <39247311+ManlyMarco@users.noreply.github.com> Date: Tue, 10 Sep 2024 14:47:22 +0200 Subject: [PATCH 34/65] Use upload-artifact@v4 (#981) --- .github/workflows/build.yml | 2 +- .github/workflows/build_pr.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3c4d5c4..63d2f62 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,7 +72,7 @@ jobs: ] } - name: Upload Artifacts - uses: actions/upload-artifact@v2.2.4 + uses: actions/upload-artifact@v4 with: path: "./bin/dist/*.zip" name: "BepInEx_CI_${{ steps.info.outputs.build_type }}_${{ steps.info.outputs.sha_short }}_${{ steps.info.outputs.build_id || 0 }}" diff --git a/.github/workflows/build_pr.yml b/.github/workflows/build_pr.yml index e0ed1d6..25a749b 100644 --- a/.github/workflows/build_pr.yml +++ b/.github/workflows/build_pr.yml @@ -17,7 +17,7 @@ jobs: run: | ./build.sh --target Publish - name: Upload Artifacts - uses: actions/upload-artifact@v2.2.4 + uses: actions/upload-artifact@v4 with: path: "./bin/dist/*.zip" name: "BepInEx_CI" From 726b68e52b13981fd8a23011ec21df3ef9b8d530 Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Tue, 10 Sep 2024 08:48:48 -0400 Subject: [PATCH 35/65] Prioritize the shallowest path for the same assembly name in the default providers (#980) Change the behaviors of the default providers so if multiple assemblies with the same name exists, but in different directories, ignore any that are located deeper. --- .../BepInExPatcherProvider.cs | 24 ++++++++++++++++++- .../BepInExPluginProvider.cs | 24 ++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/BepInEx.PatcherProvider/BepInExPatcherProvider.cs b/BepInEx.PatcherProvider/BepInExPatcherProvider.cs index 5ae4555..7685874 100644 --- a/BepInEx.PatcherProvider/BepInExPatcherProvider.cs +++ b/BepInEx.PatcherProvider/BepInExPatcherProvider.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Reflection; using BepInEx.Logging; using BepInEx.Preloader.Core.Patching; @@ -19,7 +20,28 @@ internal class BepInExPatcherProvider : BasePatcherProvider { try { - AssemblyLocationsByFilename.Add(Path.GetFileNameWithoutExtension(dll), Path.GetDirectoryName(dll)); + var filename = Path.GetFileNameWithoutExtension(dll); + var foundDirectory = Path.GetDirectoryName(dll); + + // Prioritize the shallowest path of each assembly name + if (AssemblyLocationsByFilename.TryGetValue(filename, out var existingDirectory)) + { + int levelExistingDirectory = existingDirectory?.Count(x => x == Path.DirectorySeparatorChar) ?? 0; + int levelFoundDirectory = foundDirectory?.Count(x => x == Path.DirectorySeparatorChar) ?? 0; + + bool shallowerPathFound = levelExistingDirectory > levelFoundDirectory; + Log.LogWarning($"Found duplicate assemblies filenames: {filename} was found at {foundDirectory} " + + $"while it exists already at {AssemblyLocationsByFilename[filename]}. " + + $"Only the {(shallowerPathFound ? "first" : "second")} will be examined and resolved"); + + if (levelExistingDirectory > levelFoundDirectory) + AssemblyLocationsByFilename[filename] = foundDirectory; + } + else + { + AssemblyLocationsByFilename.Add(filename, foundDirectory); + } + loadContexts.Add(new BepInExPatcherLoadContext { AssemblyHash = File.GetLastWriteTimeUtc(dll).ToString("O"), diff --git a/BepInEx.PluginProvider/BepInExPluginProvider.cs b/BepInEx.PluginProvider/BepInExPluginProvider.cs index 6520974..6f134ab 100644 --- a/BepInEx.PluginProvider/BepInExPluginProvider.cs +++ b/BepInEx.PluginProvider/BepInExPluginProvider.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Reflection; using BepInEx.Logging; @@ -18,7 +19,28 @@ internal class BepInExPluginProvider : BasePluginProvider { try { - AssemblyLocationsByFilename.Add(Path.GetFileNameWithoutExtension(dll), Path.GetDirectoryName(dll)); + var filename = Path.GetFileNameWithoutExtension(dll); + var foundDirectory = Path.GetDirectoryName(dll); + + // Prioritize the shallowest path of each assembly name + if (AssemblyLocationsByFilename.TryGetValue(filename, out var existingDirectory)) + { + int levelExistingDirectory = existingDirectory?.Count(x => x == Path.DirectorySeparatorChar) ?? 0; + int levelFoundDirectory = foundDirectory?.Count(x => x == Path.DirectorySeparatorChar) ?? 0; + + bool shallowerPathFound = levelExistingDirectory > levelFoundDirectory; + Logger.LogWarning($"Found duplicate assemblies filenames: {filename} was found at {foundDirectory} " + + $"while it exists already at {AssemblyLocationsByFilename[filename]}. " + + $"Only the {(shallowerPathFound ? "first" : "second")} will be examined and resolved"); + + if (levelExistingDirectory > levelFoundDirectory) + AssemblyLocationsByFilename[filename] = foundDirectory; + } + else + { + AssemblyLocationsByFilename.Add(filename, foundDirectory); + } + loadContexts.Add(new BepInExPluginLoadContext { AssemblyHash = File.GetLastWriteTimeUtc(dll).ToString("O"), From b891cecab969ee6180b62eef74a9783bf0ddf1ae Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Tue, 17 Sep 2024 12:18:48 -0400 Subject: [PATCH 36/65] Upgrade the github workflows sdk to 8.0.x (#992) --- .github/workflows/build.yml | 4 ++-- .github/workflows/build_pr.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 63d2f62..5533275 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,9 +34,9 @@ jobs: branch: master workflow_id: build.yml github_token: ${{ secrets.GITHUB_TOKEN }} - - uses: actions/setup-dotnet@v1 + - uses: actions/setup-dotnet@v4 with: - dotnet-version: "6.0.x" + dotnet-version: "8.0.x" - name: Build run: | ./build.sh --target Publish --build-type ${{ steps.info.outputs.build_type }} --build-id ${{ steps.info.outputs.build_id }} --last-build-commit \"${{ steps.last_successful_commit.outputs.commit_hash }}\" --nuget-api-key \"${{ secrets.BEPIN_DEV_NUGET_KEY }}\" diff --git a/.github/workflows/build_pr.yml b/.github/workflows/build_pr.yml index 25a749b..fae513f 100644 --- a/.github/workflows/build_pr.yml +++ b/.github/workflows/build_pr.yml @@ -10,9 +10,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2.3.5 - - uses: actions/setup-dotnet@v1 + - uses: actions/setup-dotnet@v4 with: - dotnet-version: "6.0.x" + dotnet-version: "8.0.x" - name: Build run: | ./build.sh --target Publish From a5b7dabc1d8d18489d2835cbc25d888ac421268b Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Sun, 15 Sep 2024 18:31:17 -0400 Subject: [PATCH 37/65] Add error when using *nix build with Windows executable Originally commit 8c7dcf265c5bf52800fa356fd3403a4dcadb82f3 Co-authored-by: ghorsington --- Runtimes/Unity/Doorstop/run_bepinex_il2cpp.sh | 6 ++++++ Runtimes/Unity/Doorstop/run_bepinex_mono.sh | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/Runtimes/Unity/Doorstop/run_bepinex_il2cpp.sh b/Runtimes/Unity/Doorstop/run_bepinex_il2cpp.sh index e947ee4..86ef48c 100644 --- a/Runtimes/Unity/Doorstop/run_bepinex_il2cpp.sh +++ b/Runtimes/Unity/Doorstop/run_bepinex_il2cpp.sh @@ -183,6 +183,12 @@ echo "${executable_path}" # Figure out the arch of the executable with file file_out="$(LD_PRELOAD="" file -b "${executable_path}")" case "${file_out}" in + *PE32*) + echo "The executable is a Windows executable file. You must use Wine/Proton and BepInEx for Windows with this executable." + echo "Uninstall BepInEx for *nix and install BepInEx for Windows instead." + echo "More info: https://docs.bepinex.dev/articles/advanced/steam_interop.html#protonwine" + exit 1 + ;; *64-bit*) arch="x64" ;; diff --git a/Runtimes/Unity/Doorstop/run_bepinex_mono.sh b/Runtimes/Unity/Doorstop/run_bepinex_mono.sh index aaa7496..2f743c1 100644 --- a/Runtimes/Unity/Doorstop/run_bepinex_mono.sh +++ b/Runtimes/Unity/Doorstop/run_bepinex_mono.sh @@ -184,6 +184,12 @@ echo "${executable_path}" # Figure out the arch of the executable with file file_out="$(LD_PRELOAD="" file -b "${executable_path}")" case "${file_out}" in + *PE32*) + echo "The executable is a Windows executable file. You must use Wine/Proton and BepInEx for Windows with this executable." + echo "Uninstall BepInEx for *nix and install BepInEx for Windows instead." + echo "More info: https://docs.bepinex.dev/articles/advanced/steam_interop.html#protonwine" + exit 1 + ;; *64-bit*) arch="x64" ;; From 92f7d25698c4bb75484b935569dd3052f40b00f7 Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Sun, 15 Sep 2024 18:51:56 -0400 Subject: [PATCH 38/65] Print file write data in console Originally commit 6c5766d5abc230a4c9427ebb14acfca05255efb8 Co-authored-by: ghorsington --- BepInEx.Preloader.Core/Logging/ChainloaderLogHelper.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BepInEx.Preloader.Core/Logging/ChainloaderLogHelper.cs b/BepInEx.Preloader.Core/Logging/ChainloaderLogHelper.cs index c4a167d..43e48e0 100644 --- a/BepInEx.Preloader.Core/Logging/ChainloaderLogHelper.cs +++ b/BepInEx.Preloader.Core/Logging/ChainloaderLogHelper.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; using BepInEx.Logging; @@ -38,7 +39,7 @@ public static class ChainloaderLogHelper var versionMini = new SemanticVersioning.Version(bepinVersion.Major, bepinVersion.Minor, bepinVersion.Patch, bepinVersion.PreRelease); var consoleTitle = $"BepInEx {versionMini} - {Paths.ProcessName}"; - log.Log(LogLevel.Message, consoleTitle); + log.Log(LogLevel.Message, $"{consoleTitle} ({File.GetLastWriteTime(Paths.ExecutablePath)})"); if (ConsoleManager.ConsoleActive) ConsoleManager.SetConsoleTitle(consoleTitle); From 6e846fc70a6b3eb9865455f48570abb36e049728 Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Sun, 15 Sep 2024 18:55:23 -0400 Subject: [PATCH 39/65] Preload Harmony and MonoMod Originally commit a1bb117c86a15b5144eaad5575a3cd73d09e3283 Co-authored-by: ghorsington --- .../BepInEx.Unity.Mono.Preloader/UnityPreloaderRunner.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Runtimes/Unity/BepInEx.Unity.Mono.Preloader/UnityPreloaderRunner.cs b/Runtimes/Unity/BepInEx.Unity.Mono.Preloader/UnityPreloaderRunner.cs index 0bacd38..0f063da 100644 --- a/Runtimes/Unity/BepInEx.Unity.Mono.Preloader/UnityPreloaderRunner.cs +++ b/Runtimes/Unity/BepInEx.Unity.Mono.Preloader/UnityPreloaderRunner.cs @@ -22,7 +22,10 @@ internal static class UnityPreloaderRunner "Mono.Cecil.dll", "Mono.Cecil.Mdb.dll", "Mono.Cecil.Pdb.dll", - "Mono.Cecil.Rocks.dll" + "Mono.Cecil.Rocks.dll", + "MonoMod.Utils.dll", + "MonoMod.RuntimeDetour.dll", + "0Harmony.dll" }; private static void LoadCriticalAssemblies() From 8597ce7ebf9f18b024b6ef60419d3846f228c86e Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Sun, 15 Sep 2024 19:03:19 -0400 Subject: [PATCH 40/65] Add TomlTypeConverter for UnityEngine.Rect Originally pull request #510 Co-authored-by: ManlyMarco <39247311+manlymarco@users.noreply.github.com> --- .../UnityTomlTypeConverters.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/Runtimes/Unity/BepInEx.Unity.Mono/UnityTomlTypeConverters.cs b/Runtimes/Unity/BepInEx.Unity.Mono/UnityTomlTypeConverters.cs index 3f4da2e..56f51d3 100644 --- a/Runtimes/Unity/BepInEx.Unity.Mono/UnityTomlTypeConverters.cs +++ b/Runtimes/Unity/BepInEx.Unity.Mono/UnityTomlTypeConverters.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Runtime.CompilerServices; using BepInEx.Configuration; using UnityEngine; @@ -36,5 +37,47 @@ internal static class UnityTomlTypeConverters TomlTypeConverter.AddConverter(typeof(Vector3), jsonConverter); TomlTypeConverter.AddConverter(typeof(Vector4), jsonConverter); TomlTypeConverter.AddConverter(typeof(Quaternion), jsonConverter); + TomlTypeConverter.AddConverter(typeof(Rect), new TypeConverter + { + ConvertToObject = StringToRect, + ConvertToString = RectToString + }); + } + + private static object StringToRect(string s, Type type) + { + // JsonUtility doesn't work with Rect on all Unity versions, so parse it manually + Rect result = default(Rect); + + if (s == null) + return result; + + string cleaned = s.Trim('{', '}').Replace(" ", ""); + foreach (string part in cleaned.Split(',')) + { + string[] parts = part.Split(':'); + if (parts.Length != 2 || !float.TryParse(parts[1], out float value)) + continue; + + string id = parts[0].Trim('"'); + if (id == "x") + result.x = value; + else if (id == "y") + result.y = value; + else if (id == "width" || id == "z") + result.width = value; + else if (id == "height" || id == "w") + result.height = value; + } + + return result; + } + + private static string RectToString(object o, Type type) + { + Rect rect = (Rect)o; + return string.Format(CultureInfo.InvariantCulture, + "{{ \"x\":{0}, \"y\":{1}, \"width\":{2}, \"height\":{3} }}", + rect.x, rect.y, rect.width, rect.height); } } From af5d93917cfa0568ff40153009b2287ab564482f Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Sun, 15 Sep 2024 19:49:52 -0400 Subject: [PATCH 41/65] Make Logger.Listeners and Logger.Sources thread safe Originally pull request #656 Co-authored-by: ManlyMarco <39247311+manlymarco@users.noreply.github.com> --- BepInEx.Core/Logging/Logger.cs | 183 +++++++++++++++++++++++++++------ 1 file changed, 151 insertions(+), 32 deletions(-) diff --git a/BepInEx.Core/Logging/Logger.cs b/BepInEx.Core/Logging/Logger.cs index e78842e..6590059 100644 --- a/BepInEx.Core/Logging/Logger.cs +++ b/BepInEx.Core/Logging/Logger.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; using BepInEx.Core.Logging.Interpolation; @@ -25,7 +26,7 @@ public static class Logger /// /// Log levels that are currently listened to by at least one listener. /// - public static LogLevel ListenedLogLevels => listeners.activeLogLevels; + public static LogLevel ListenedLogLevels => listeners.ActiveLogLevels; /// /// Collection of all log listeners that receive log events. @@ -39,10 +40,7 @@ public static class Logger internal static void InternalLogEvent(object sender, LogEventArgs eventArgs) { - // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator Prevent extra allocations - foreach (var listener in listeners) - if ((eventArgs.Level & listener.LogLevelFilter) != LogLevel.None) - listener?.LogEvent(sender, eventArgs); + listeners.SendLogEvent(sender, eventArgs); } /// @@ -74,69 +72,190 @@ public static class Logger return source; } - private class LogListenerCollection : List, ICollection + private class LogListenerCollection : ThreadSafeCollection { - public LogLevel activeLogLevels = LogLevel.None; + internal LogLevel ActiveLogLevels = LogLevel.None; - void ICollection.Add(ILogListener item) + internal void SendLogEvent(object sender, LogEventArgs eventArgs) + { + // Do this instead of foreach to avoid boxing, also very slightly faster + var aListInTime = BaseList; + // ReSharper disable once ForCanBeConvertedToForeach + for (int i = 0; i < aListInTime.Count; i++) + { + if ((eventArgs.Level & aListInTime[i].LogLevelFilter) != LogLevel.None) + aListInTime[i].LogEvent(sender, eventArgs); + } + } + + public override void Add(ILogListener item) { if (item == null) throw new ArgumentNullException(nameof(item)); - activeLogLevels |= item.LogLevelFilter; - - base.Add(item); + lock (SpinLock) + { + ActiveLogLevels |= item.LogLevelFilter; + base.Add(item); + } } - void ICollection.Clear() + public override void Clear() { - activeLogLevels = LogLevel.None; - base.Clear(); + lock (SpinLock) + { + ActiveLogLevels = LogLevel.None; + base.Clear(); + } } - bool ICollection.Remove(ILogListener item) + public override bool Remove(ILogListener item) { if (item == null || !base.Remove(item)) return false; - activeLogLevels = LogLevel.None; + lock (SpinLock) + { + ActiveLogLevels = LogLevel.None; - foreach (var i in this) - activeLogLevels |= i.LogLevelFilter; + foreach (var i in this) + ActiveLogLevels |= i.LogLevelFilter; - return true; + return true; + } } } - private class LogSourceCollection : List, ICollection + private class LogSourceCollection : ThreadSafeCollection { - void ICollection.Add(ILogSource item) + public override void Add(ILogSource item) { if (item == null) throw new ArgumentNullException(nameof(item), "Log sources cannot be null when added to the source list."); - item.LogEvent += InternalLogEvent; - - base.Add(item); + lock (SpinLock) + { + item.LogEvent += InternalLogEvent; + base.Add(item); + var copy = new List(BaseList.Count + 1); + copy.AddRange(BaseList); + copy.Add(item); + BaseList = copy; + } } - void ICollection.Clear() + public override void Clear() { - foreach (var item in this) - item.LogEvent -= InternalLogEvent; + if (Count == 0) + return; - base.Clear(); + lock (SpinLock) + { + for (var i = 0; i < BaseList.Count; i++) + BaseList[i].LogEvent -= InternalLogEvent; + + BaseList = []; + } } - bool ICollection.Remove(ILogSource item) + public override bool Remove(ILogSource item) { - if (item == null || !Remove(item)) + if (item == null) return false; - item.LogEvent -= InternalLogEvent; - return true; + lock (SpinLock) + { + var wasPresent = base.Remove(item); + if (wasPresent) + item.LogEvent -= InternalLogEvent; + return wasPresent; + } + } + } + + /// + /// Simple thread safe list that prioritizes read speed over write speed. + /// Read is the same as a normal list, while write locks and allocates a copy of the list. + /// Logger lists are rarely updated so this tradeoff should be fine. + /// + /// + private class ThreadSafeCollection : ICollection where T : class + { + protected readonly object SpinLock = new(); + protected List BaseList = []; + + public int Count => BaseList.Count; + + public bool IsReadOnly => false; + + public IEnumerator GetEnumerator() + { + // Can't avoid boxing + return BaseList.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + // Can't avoid boxing + return BaseList.GetEnumerator(); + } + + public virtual void Add(T item) + { + if (item == null) + throw new ArgumentNullException(nameof(item), "item can't be null"); + + lock (SpinLock) + { + var copy = new List(BaseList.Count + 1); + copy.AddRange(BaseList); + copy.Add(item); + BaseList = copy; + } + } + + public virtual void Clear() + { + if (Count == 0) + return; + + lock (SpinLock) + BaseList = []; + } + + public bool Contains(T item) + { + return BaseList.Contains(item); + } + + public void CopyTo(T[] array, int arrayIndex) + { + BaseList.CopyTo(array, arrayIndex); + } + + public virtual bool Remove(T item) + { + if (item == null) + return false; + + lock (SpinLock) + { + var copy = new List(BaseList.Count); + var any = false; + for (int i = 0; i < BaseList.Count; i++) + { + var existingItem = BaseList[i]; + if (existingItem.Equals(item)) + any = true; + else + copy.Add(existingItem); + } + + BaseList = copy; + return any; + } } } } From d9b95a0ac76e53c4f0f2be93939adb02cfde16d4 Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Sun, 15 Sep 2024 22:03:32 -0400 Subject: [PATCH 42/65] Add input abstraction layer to support games with the Input class disabled Originally pull request #444 Co-authored-by: ManlyMarco <39247311+manlymarco@users.noreply.github.com> --- .../BepInEx.Unity.Mono.csproj | 1 + .../Configuration/KeyboardShortcut.cs | 26 +- .../Unity/BepInEx.Unity.Mono/UnityInput.cs | 854 ++++++++++++++++++ 3 files changed, 866 insertions(+), 15 deletions(-) create mode 100644 Runtimes/Unity/BepInEx.Unity.Mono/UnityInput.cs diff --git a/Runtimes/Unity/BepInEx.Unity.Mono/BepInEx.Unity.Mono.csproj b/Runtimes/Unity/BepInEx.Unity.Mono/BepInEx.Unity.Mono.csproj index 7f49885..558ed33 100644 --- a/Runtimes/Unity/BepInEx.Unity.Mono/BepInEx.Unity.Mono.csproj +++ b/Runtimes/Unity/BepInEx.Unity.Mono/BepInEx.Unity.Mono.csproj @@ -30,5 +30,6 @@ + diff --git a/Runtimes/Unity/BepInEx.Unity.Mono/Configuration/KeyboardShortcut.cs b/Runtimes/Unity/BepInEx.Unity.Mono/Configuration/KeyboardShortcut.cs index 5e7f37b..9867f0b 100644 --- a/Runtimes/Unity/BepInEx.Unity.Mono/Configuration/KeyboardShortcut.cs +++ b/Runtimes/Unity/BepInEx.Unity.Mono/Configuration/KeyboardShortcut.cs @@ -35,17 +35,8 @@ public struct KeyboardShortcut /// public static readonly KeyboardShortcut Empty = new(); - /// - /// All KeyCode values that can be used in a keyboard shortcut. - /// - public static readonly IEnumerable AllKeyCodes = (KeyCode[]) Enum.GetValues(typeof(KeyCode)); - // Don't block hotkeys if mouse is being pressed, e.g. when shooting and trying to strafe - private static readonly KeyCode[] _modifierBlockKeyCodes = AllKeyCodes.Except(new[] - { - KeyCode.Mouse0, KeyCode.Mouse1, KeyCode.Mouse2, KeyCode.Mouse3, KeyCode.Mouse4, KeyCode.Mouse5, - KeyCode.Mouse6, KeyCode.None - }).ToArray(); + private static KeyCode[] modifierBlockKeyCodes; private readonly KeyCode[] _allKeys; @@ -122,7 +113,7 @@ public struct KeyboardShortcut var mainKey = MainKey; if (mainKey == KeyCode.None) return false; - return Input.GetKeyDown(mainKey) && ModifierKeyTest(); + return UnityInput.Current.GetKeyDown(mainKey) && ModifierKeyTest(); } /// @@ -133,7 +124,7 @@ public struct KeyboardShortcut var mainKey = MainKey; if (mainKey == KeyCode.None) return false; - return Input.GetKey(mainKey) && ModifierKeyTest(); + return UnityInput.Current.GetKey(mainKey) && ModifierKeyTest(); } /// @@ -144,7 +135,7 @@ public struct KeyboardShortcut var mainKey = MainKey; if (mainKey == KeyCode.None) return false; - return Input.GetKeyUp(mainKey) && ModifierKeyTest(); + return UnityInput.Current.GetKeyUp(mainKey) && ModifierKeyTest(); } private bool ModifierKeyTest() @@ -152,10 +143,15 @@ public struct KeyboardShortcut var allKeys = _allKeys; var mainKey = MainKey; - var allModifiersPressed = allKeys.All(c => c == mainKey || Input.GetKey(c)); + var allModifiersPressed = allKeys.All(c => c == mainKey || UnityInput.Current.GetKey(c)); if (!allModifiersPressed) return false; - var noOtherModifiersPressed = _modifierBlockKeyCodes.All(c => !Input.GetKey(c) || allKeys.Contains(c)); + // Lazy init to make sure the game is initialized, and we're in main thread before calling UnityInput.Current + modifierBlockKeyCodes ??= UnityInput.Current.SupportedKeyCodes.Except([KeyCode.Mouse0, KeyCode.Mouse1, + KeyCode.Mouse2, KeyCode.Mouse3, KeyCode.Mouse4, KeyCode.Mouse5, KeyCode.Mouse6, KeyCode.None + ]).ToArray(); + + bool noOtherModifiersPressed = modifierBlockKeyCodes.All(c => !UnityInput.Current.GetKey(c) || allKeys.Contains(c)); return noOtherModifiersPressed; } diff --git a/Runtimes/Unity/BepInEx.Unity.Mono/UnityInput.cs b/Runtimes/Unity/BepInEx.Unity.Mono/UnityInput.cs new file mode 100644 index 0000000..4d72455 --- /dev/null +++ b/Runtimes/Unity/BepInEx.Unity.Mono/UnityInput.cs @@ -0,0 +1,854 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using BepInEx.Logging; +using UnityEngine; +using UnityEngine.InputSystem; +using UnityEngine.InputSystem.Controls; + +// ReSharper disable InconsistentNaming + +namespace BepInEx +{ + /// + /// Abstraction layer over Unity's input systems for use in universal plugins that need to use hotkeys. + /// It can use either Input or Unity.InputSystem, depending on what's available. Input is preferred. + /// WARNING: Use only inside of Unity's main thread! + /// + public class UnityInput + { + private static IInputSystem current; + /// + /// Best currently supported input system. + /// + public static IInputSystem Current + { + get + { + if (current == null) + { + try + { + Input.GetKeyDown(KeyCode.A); + current = new LegacyInputSystem(); + Logging.Logger.Log(LogLevel.Debug, "[UnityInput] Using LegacyInputSystem"); + } + catch (InvalidOperationException) + { + current = new NewInputSystem(); + Logging.Logger.Log(LogLevel.Debug, "[UnityInput] Using NewInputSystem"); + } + catch (Exception ex) + { + Logging.Logger.Log(LogLevel.Warning, "[UnityInput] Failed to detect available input systems - " + ex); + } + } + return current; + } + } + + /// + /// True if the Input class is not disabled. + /// + public bool LegacyInputSystemAvailable => Current is LegacyInputSystem; + } + + /// + /// Generic input system interface. Just barely good enough for hotkeys. + /// + public interface IInputSystem + { + // No easy way to use these with the new input system + //bool GetButton(string buttonName); + //bool GetButtonDown(string buttonName); + //bool GetButtonUp(string buttonName); + + /// + bool GetKey(string name); + /// + bool GetKey(KeyCode key); + + /// + bool GetKeyDown(string name); + /// + bool GetKeyDown(KeyCode key); + + /// + bool GetKeyUp(string name); + /// + bool GetKeyUp(KeyCode key); + + + /// + bool GetMouseButton(int button); + /// + bool GetMouseButtonDown(int button); + /// + bool GetMouseButtonUp(int button); + + /// + void ResetInputAxes(); + + /// + Vector3 mousePosition { get; } + /// + Vector2 mouseScrollDelta { get; } + + /// + bool mousePresent { get; } + + /// + bool anyKey { get; } + /// + bool anyKeyDown { get; } + + /// + /// All KeyCodes supported by the current input system. + /// + IEnumerable SupportedKeyCodes { get; } + } + + internal class NewInputSystem : IInputSystem + { + public bool GetKey(string name) => GetControl(name)?.isPressed ?? false; + + public bool GetKey(KeyCode key) => GetControl(key)?.isPressed ?? false; + + public bool GetKeyDown(string name) => GetControl(name)?.wasPressedThisFrame ?? false; + + public bool GetKeyDown(KeyCode key) => GetControl(key)?.wasPressedThisFrame ?? false; + + public bool GetKeyUp(string name) => GetControl(name)?.wasReleasedThisFrame ?? false; + + public bool GetKeyUp(KeyCode key) => GetControl(key)?.wasReleasedThisFrame ?? false; + + public bool GetMouseButton(int button) => GetControl(KeyCode.Mouse0 + button)?.isPressed ?? false; + + public bool GetMouseButtonDown(int button) => GetControl(KeyCode.Mouse0 + button)?.wasPressedThisFrame ?? false; + + public bool GetMouseButtonUp(int button) => GetControl(KeyCode.Mouse0 + button)?.wasReleasedThisFrame ?? false; + + public void ResetInputAxes() { /*Not supported*/ } + + public Vector3 mousePosition => Mouse.current.position.ReadValue(); + public Vector2 mouseScrollDelta => Mouse.current.scroll.ReadValue(); + public bool mousePresent => Mouse.current.enabled; + + public bool anyKey + { + get + { + if (Keyboard.current.anyKey.isPressed) + return true; + var current = Mouse.current; + return current.leftButton.isPressed || + current.rightButton.isPressed || + current.forwardButton.isPressed || + current.backButton.isPressed || + current.middleButton.isPressed; + } + } + + public bool anyKeyDown + { + get + { + if (Keyboard.current.anyKey.wasPressedThisFrame) + return true; + var current = Mouse.current; + return current.leftButton.wasPressedThisFrame || + current.rightButton.wasPressedThisFrame || + current.forwardButton.wasPressedThisFrame || + current.backButton.wasPressedThisFrame || + current.middleButton.wasPressedThisFrame; + } + } + + public IEnumerable SupportedKeyCodes { get; } = Enum.GetValues(typeof(KeyCode)).Cast().Where(x => GetControl(x, true) != null).ToList(); + + private static ButtonControl GetControl(string name) => GetControl((KeyCode)Enum.Parse(typeof(KeyCode), name, true)); + + private static ButtonControl GetControl(KeyCode key, bool silent = false) + { + switch (key) + { + case KeyCode.None: + break; + case KeyCode.Backspace: + return Keyboard.current.backspaceKey; + case KeyCode.Delete: + return Keyboard.current.deleteKey; + case KeyCode.Tab: + return Keyboard.current.tabKey; + case KeyCode.Clear: + break; + case KeyCode.Return: + return Keyboard.current.enterKey; + case KeyCode.Pause: + return Keyboard.current.pauseKey; + case KeyCode.Escape: + return Keyboard.current.escapeKey; + case KeyCode.Space: + return Keyboard.current.spaceKey; + case KeyCode.Keypad0: + return Keyboard.current.numpad0Key; + case KeyCode.Keypad1: + return Keyboard.current.numpad1Key; + case KeyCode.Keypad2: + return Keyboard.current.numpad2Key; + case KeyCode.Keypad3: + return Keyboard.current.numpad3Key; + case KeyCode.Keypad4: + return Keyboard.current.numpad4Key; + case KeyCode.Keypad5: + return Keyboard.current.numpad5Key; + case KeyCode.Keypad6: + return Keyboard.current.numpad6Key; + case KeyCode.Keypad7: + return Keyboard.current.numpad7Key; + case KeyCode.Keypad8: + return Keyboard.current.numpad8Key; + case KeyCode.Keypad9: + return Keyboard.current.numpad9Key; + case KeyCode.KeypadPeriod: + return Keyboard.current.numpadPeriodKey; + case KeyCode.KeypadDivide: + return Keyboard.current.numpadDivideKey; + case KeyCode.KeypadMultiply: + return Keyboard.current.numpadMultiplyKey; + case KeyCode.KeypadMinus: + return Keyboard.current.numpadMinusKey; + case KeyCode.KeypadPlus: + return Keyboard.current.numpadPlusKey; + case KeyCode.KeypadEnter: + return Keyboard.current.numpadEnterKey; + case KeyCode.KeypadEquals: + return Keyboard.current.numpadEqualsKey; + case KeyCode.UpArrow: + return Keyboard.current.upArrowKey; + case KeyCode.DownArrow: + return Keyboard.current.downArrowKey; + case KeyCode.RightArrow: + return Keyboard.current.rightArrowKey; + case KeyCode.LeftArrow: + return Keyboard.current.leftArrowKey; + case KeyCode.Insert: + return Keyboard.current.insertKey; + case KeyCode.Home: + return Keyboard.current.homeKey; + case KeyCode.End: + return Keyboard.current.endKey; + case KeyCode.PageUp: + return Keyboard.current.pageUpKey; + case KeyCode.PageDown: + return Keyboard.current.pageDownKey; + case KeyCode.F1: + return Keyboard.current.f1Key; + case KeyCode.F2: + return Keyboard.current.f2Key; + case KeyCode.F3: + return Keyboard.current.f3Key; + case KeyCode.F4: + return Keyboard.current.f4Key; + case KeyCode.F5: + return Keyboard.current.f5Key; + case KeyCode.F6: + return Keyboard.current.f6Key; + case KeyCode.F7: + return Keyboard.current.f7Key; + case KeyCode.F8: + return Keyboard.current.f8Key; + case KeyCode.F9: + return Keyboard.current.f9Key; + case KeyCode.F10: + return Keyboard.current.f10Key; + case KeyCode.F11: + return Keyboard.current.f11Key; + case KeyCode.F12: + return Keyboard.current.f12Key; + case KeyCode.F13: + break; + case KeyCode.F14: + break; + case KeyCode.F15: + break; + case KeyCode.Alpha0: + return Keyboard.current.digit0Key; + case KeyCode.Alpha1: + return Keyboard.current.digit1Key; + case KeyCode.Alpha2: + return Keyboard.current.digit2Key; + case KeyCode.Alpha3: + return Keyboard.current.digit3Key; + case KeyCode.Alpha4: + return Keyboard.current.digit4Key; + case KeyCode.Alpha5: + return Keyboard.current.digit5Key; + case KeyCode.Alpha6: + return Keyboard.current.digit6Key; + case KeyCode.Alpha7: + return Keyboard.current.digit7Key; + case KeyCode.Alpha8: + return Keyboard.current.digit8Key; + case KeyCode.Alpha9: + return Keyboard.current.digit9Key; + case KeyCode.Exclaim: + break; + case KeyCode.DoubleQuote: + break; + case KeyCode.Hash: + break; + case KeyCode.Dollar: + break; + case KeyCode.Ampersand: + break; + case KeyCode.Quote: + return Keyboard.current.quoteKey; + case KeyCode.LeftParen: + break; + case KeyCode.RightParen: + break; + case KeyCode.Asterisk: + break; + case KeyCode.Plus: + return Keyboard.current.numpadPlusKey; + case KeyCode.Comma: + return Keyboard.current.commaKey; + case KeyCode.Minus: + return Keyboard.current.minusKey; + case KeyCode.Period: + return Keyboard.current.periodKey; + case KeyCode.Slash: + return Keyboard.current.slashKey; + case KeyCode.Colon: + break; + case KeyCode.Semicolon: + return Keyboard.current.semicolonKey; + case KeyCode.Less: + break; + case KeyCode.Equals: + return Keyboard.current.equalsKey; + case KeyCode.Greater: + break; + case KeyCode.Question: + break; + case KeyCode.At: + break; + case KeyCode.LeftBracket: + return Keyboard.current.leftBracketKey; + case KeyCode.Backslash: + return Keyboard.current.backslashKey; + case KeyCode.RightBracket: + return Keyboard.current.rightBracketKey; + case KeyCode.Caret: + break; + case KeyCode.Underscore: + break; + case KeyCode.BackQuote: + return Keyboard.current.backquoteKey; + case KeyCode.A: + return Keyboard.current.aKey; + case KeyCode.B: + return Keyboard.current.bKey; + case KeyCode.C: + return Keyboard.current.cKey; + case KeyCode.D: + return Keyboard.current.dKey; + case KeyCode.E: + return Keyboard.current.eKey; + case KeyCode.F: + return Keyboard.current.fKey; + case KeyCode.G: + return Keyboard.current.gKey; + case KeyCode.H: + return Keyboard.current.hKey; + case KeyCode.I: + return Keyboard.current.iKey; + case KeyCode.J: + return Keyboard.current.jKey; + case KeyCode.K: + return Keyboard.current.kKey; + case KeyCode.L: + return Keyboard.current.lKey; + case KeyCode.M: + return Keyboard.current.mKey; + case KeyCode.N: + return Keyboard.current.nKey; + case KeyCode.O: + return Keyboard.current.oKey; + case KeyCode.P: + return Keyboard.current.pKey; + case KeyCode.Q: + return Keyboard.current.qKey; + case KeyCode.R: + return Keyboard.current.rKey; + case KeyCode.S: + return Keyboard.current.sKey; + case KeyCode.T: + return Keyboard.current.tKey; + case KeyCode.U: + return Keyboard.current.uKey; + case KeyCode.V: + return Keyboard.current.vKey; + case KeyCode.W: + return Keyboard.current.wKey; + case KeyCode.X: + return Keyboard.current.xKey; + case KeyCode.Y: + return Keyboard.current.yKey; + case KeyCode.Z: + return Keyboard.current.zKey; + case KeyCode.Numlock: + return Keyboard.current.numLockKey; + case KeyCode.CapsLock: + return Keyboard.current.capsLockKey; + case KeyCode.ScrollLock: + return Keyboard.current.scrollLockKey; + case KeyCode.RightShift: + return Keyboard.current.rightShiftKey; + case KeyCode.LeftShift: + return Keyboard.current.leftShiftKey; + case KeyCode.RightControl: + return Keyboard.current.rightCtrlKey; + case KeyCode.LeftControl: + return Keyboard.current.leftCtrlKey; + case KeyCode.RightAlt: + return Keyboard.current.rightAltKey; + case KeyCode.LeftAlt: + return Keyboard.current.leftAltKey; + case KeyCode.LeftCommand: + return Keyboard.current.leftCommandKey; + case KeyCode.LeftWindows: + return Keyboard.current.leftWindowsKey; + case KeyCode.RightCommand: + return Keyboard.current.rightCommandKey; + case KeyCode.RightWindows: + return Keyboard.current.rightWindowsKey; + case KeyCode.AltGr: + break; + case KeyCode.Help: + break; + case KeyCode.Print: + return Keyboard.current.printScreenKey; + case KeyCode.SysReq: + break; + case KeyCode.Break: + break; + case KeyCode.Menu: + return Keyboard.current.contextMenuKey; + case KeyCode.Mouse0: + return Mouse.current.leftButton; + case KeyCode.Mouse1: + return Mouse.current.rightButton; + case KeyCode.Mouse2: + return Mouse.current.middleButton; + case KeyCode.Mouse3: + return Mouse.current.backButton; + case KeyCode.Mouse4: + return Mouse.current.forwardButton; + case KeyCode.Mouse5: + break; + case KeyCode.Mouse6: + break; + case KeyCode.JoystickButton0: + break; + case KeyCode.JoystickButton1: + break; + case KeyCode.JoystickButton2: + break; + case KeyCode.JoystickButton3: + break; + case KeyCode.JoystickButton4: + break; + case KeyCode.JoystickButton5: + break; + case KeyCode.JoystickButton6: + break; + case KeyCode.JoystickButton7: + break; + case KeyCode.JoystickButton8: + break; + case KeyCode.JoystickButton9: + break; + case KeyCode.JoystickButton10: + break; + case KeyCode.JoystickButton11: + break; + case KeyCode.JoystickButton12: + break; + case KeyCode.JoystickButton13: + break; + case KeyCode.JoystickButton14: + break; + case KeyCode.JoystickButton15: + break; + case KeyCode.JoystickButton16: + break; + case KeyCode.JoystickButton17: + break; + case KeyCode.JoystickButton18: + break; + case KeyCode.JoystickButton19: + break; + case KeyCode.Joystick1Button0: + break; + case KeyCode.Joystick1Button1: + break; + case KeyCode.Joystick1Button2: + break; + case KeyCode.Joystick1Button3: + break; + case KeyCode.Joystick1Button4: + break; + case KeyCode.Joystick1Button5: + break; + case KeyCode.Joystick1Button6: + break; + case KeyCode.Joystick1Button7: + break; + case KeyCode.Joystick1Button8: + break; + case KeyCode.Joystick1Button9: + break; + case KeyCode.Joystick1Button10: + break; + case KeyCode.Joystick1Button11: + break; + case KeyCode.Joystick1Button12: + break; + case KeyCode.Joystick1Button13: + break; + case KeyCode.Joystick1Button14: + break; + case KeyCode.Joystick1Button15: + break; + case KeyCode.Joystick1Button16: + break; + case KeyCode.Joystick1Button17: + break; + case KeyCode.Joystick1Button18: + break; + case KeyCode.Joystick1Button19: + break; + case KeyCode.Joystick2Button0: + break; + case KeyCode.Joystick2Button1: + break; + case KeyCode.Joystick2Button2: + break; + case KeyCode.Joystick2Button3: + break; + case KeyCode.Joystick2Button4: + break; + case KeyCode.Joystick2Button5: + break; + case KeyCode.Joystick2Button6: + break; + case KeyCode.Joystick2Button7: + break; + case KeyCode.Joystick2Button8: + break; + case KeyCode.Joystick2Button9: + break; + case KeyCode.Joystick2Button10: + break; + case KeyCode.Joystick2Button11: + break; + case KeyCode.Joystick2Button12: + break; + case KeyCode.Joystick2Button13: + break; + case KeyCode.Joystick2Button14: + break; + case KeyCode.Joystick2Button15: + break; + case KeyCode.Joystick2Button16: + break; + case KeyCode.Joystick2Button17: + break; + case KeyCode.Joystick2Button18: + break; + case KeyCode.Joystick2Button19: + break; + case KeyCode.Joystick3Button0: + break; + case KeyCode.Joystick3Button1: + break; + case KeyCode.Joystick3Button2: + break; + case KeyCode.Joystick3Button3: + break; + case KeyCode.Joystick3Button4: + break; + case KeyCode.Joystick3Button5: + break; + case KeyCode.Joystick3Button6: + break; + case KeyCode.Joystick3Button7: + break; + case KeyCode.Joystick3Button8: + break; + case KeyCode.Joystick3Button9: + break; + case KeyCode.Joystick3Button10: + break; + case KeyCode.Joystick3Button11: + break; + case KeyCode.Joystick3Button12: + break; + case KeyCode.Joystick3Button13: + break; + case KeyCode.Joystick3Button14: + break; + case KeyCode.Joystick3Button15: + break; + case KeyCode.Joystick3Button16: + break; + case KeyCode.Joystick3Button17: + break; + case KeyCode.Joystick3Button18: + break; + case KeyCode.Joystick3Button19: + break; + case KeyCode.Joystick4Button0: + break; + case KeyCode.Joystick4Button1: + break; + case KeyCode.Joystick4Button2: + break; + case KeyCode.Joystick4Button3: + break; + case KeyCode.Joystick4Button4: + break; + case KeyCode.Joystick4Button5: + break; + case KeyCode.Joystick4Button6: + break; + case KeyCode.Joystick4Button7: + break; + case KeyCode.Joystick4Button8: + break; + case KeyCode.Joystick4Button9: + break; + case KeyCode.Joystick4Button10: + break; + case KeyCode.Joystick4Button11: + break; + case KeyCode.Joystick4Button12: + break; + case KeyCode.Joystick4Button13: + break; + case KeyCode.Joystick4Button14: + break; + case KeyCode.Joystick4Button15: + break; + case KeyCode.Joystick4Button16: + break; + case KeyCode.Joystick4Button17: + break; + case KeyCode.Joystick4Button18: + break; + case KeyCode.Joystick4Button19: + break; + case KeyCode.Joystick5Button0: + break; + case KeyCode.Joystick5Button1: + break; + case KeyCode.Joystick5Button2: + break; + case KeyCode.Joystick5Button3: + break; + case KeyCode.Joystick5Button4: + break; + case KeyCode.Joystick5Button5: + break; + case KeyCode.Joystick5Button6: + break; + case KeyCode.Joystick5Button7: + break; + case KeyCode.Joystick5Button8: + break; + case KeyCode.Joystick5Button9: + break; + case KeyCode.Joystick5Button10: + break; + case KeyCode.Joystick5Button11: + break; + case KeyCode.Joystick5Button12: + break; + case KeyCode.Joystick5Button13: + break; + case KeyCode.Joystick5Button14: + break; + case KeyCode.Joystick5Button15: + break; + case KeyCode.Joystick5Button16: + break; + case KeyCode.Joystick5Button17: + break; + case KeyCode.Joystick5Button18: + break; + case KeyCode.Joystick5Button19: + break; + case KeyCode.Joystick6Button0: + break; + case KeyCode.Joystick6Button1: + break; + case KeyCode.Joystick6Button2: + break; + case KeyCode.Joystick6Button3: + break; + case KeyCode.Joystick6Button4: + break; + case KeyCode.Joystick6Button5: + break; + case KeyCode.Joystick6Button6: + break; + case KeyCode.Joystick6Button7: + break; + case KeyCode.Joystick6Button8: + break; + case KeyCode.Joystick6Button9: + break; + case KeyCode.Joystick6Button10: + break; + case KeyCode.Joystick6Button11: + break; + case KeyCode.Joystick6Button12: + break; + case KeyCode.Joystick6Button13: + break; + case KeyCode.Joystick6Button14: + break; + case KeyCode.Joystick6Button15: + break; + case KeyCode.Joystick6Button16: + break; + case KeyCode.Joystick6Button17: + break; + case KeyCode.Joystick6Button18: + break; + case KeyCode.Joystick6Button19: + break; + case KeyCode.Joystick7Button0: + break; + case KeyCode.Joystick7Button1: + break; + case KeyCode.Joystick7Button2: + break; + case KeyCode.Joystick7Button3: + break; + case KeyCode.Joystick7Button4: + break; + case KeyCode.Joystick7Button5: + break; + case KeyCode.Joystick7Button6: + break; + case KeyCode.Joystick7Button7: + break; + case KeyCode.Joystick7Button8: + break; + case KeyCode.Joystick7Button9: + break; + case KeyCode.Joystick7Button10: + break; + case KeyCode.Joystick7Button11: + break; + case KeyCode.Joystick7Button12: + break; + case KeyCode.Joystick7Button13: + break; + case KeyCode.Joystick7Button14: + break; + case KeyCode.Joystick7Button15: + break; + case KeyCode.Joystick7Button16: + break; + case KeyCode.Joystick7Button17: + break; + case KeyCode.Joystick7Button18: + break; + case KeyCode.Joystick7Button19: + break; + case KeyCode.Joystick8Button0: + break; + case KeyCode.Joystick8Button1: + break; + case KeyCode.Joystick8Button2: + break; + case KeyCode.Joystick8Button3: + break; + case KeyCode.Joystick8Button4: + break; + case KeyCode.Joystick8Button5: + break; + case KeyCode.Joystick8Button6: + break; + case KeyCode.Joystick8Button7: + break; + case KeyCode.Joystick8Button8: + break; + case KeyCode.Joystick8Button9: + break; + case KeyCode.Joystick8Button10: + break; + case KeyCode.Joystick8Button11: + break; + case KeyCode.Joystick8Button12: + break; + case KeyCode.Joystick8Button13: + break; + case KeyCode.Joystick8Button14: + break; + case KeyCode.Joystick8Button15: + break; + case KeyCode.Joystick8Button16: + break; + case KeyCode.Joystick8Button17: + break; + case KeyCode.Joystick8Button18: + break; + case KeyCode.Joystick8Button19: + break; + //default: + // throw new ArgumentOutOfRangeException(nameof(key), key, null); + } + + if (!silent) + Logging.Logger.Log(LogLevel.Debug, $"[{nameof(NewInputSystem)}] Unsupported key: {key}"); + + return null; + } + } + + internal class LegacyInputSystem : IInputSystem + { + public bool GetKey(string name) => Input.GetKey(name); + + public bool GetKey(KeyCode key) => Input.GetKey(key); + + public bool GetKeyDown(string name) => Input.GetKeyDown(name); + + public bool GetKeyDown(KeyCode key) => Input.GetKeyDown(key); + + public bool GetKeyUp(string name) => Input.GetKeyUp(name); + + public bool GetKeyUp(KeyCode key) => Input.GetKeyUp(key); + + public bool GetMouseButton(int button) => Input.GetMouseButton(button); + + public bool GetMouseButtonDown(int button) => Input.GetMouseButtonDown(button); + + public bool GetMouseButtonUp(int button) => Input.GetMouseButtonUp(button); + + public void ResetInputAxes() => Input.ResetInputAxes(); + + public Vector3 mousePosition => Input.mousePosition; + public Vector2 mouseScrollDelta => Input.mouseScrollDelta; + public bool mousePresent => Input.mousePresent; + public bool anyKey => Input.anyKey; + public bool anyKeyDown => Input.anyKeyDown; + + public IEnumerable SupportedKeyCodes { get; } = (KeyCode[])Enum.GetValues(typeof(KeyCode)); + } +} From 544f909a80e19d42c85b7169031a58baf65cb9fc Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Tue, 24 Sep 2024 14:25:56 -0400 Subject: [PATCH 43/65] Revert "Merge pull request #960 from aldelaro5/metaplugins" (#995) This reverts commit 2a61b614978b1f3388c41f7784c43178ba98b58e, reversing changes made to 6b38cee1277f4e3da5f2f685aee8b503a14d4054. --- BepInEx.Core/Bootstrap/BaseChainloader.cs | 294 +++++++----------- BepInEx.Core/Bootstrap/TypeLoader.cs | 142 +-------- BepInEx.Core/Contract/Attributes.cs | 48 --- BepInEx.Core/Contract/BasePluginProvider.cs | 71 ----- BepInEx.Core/Contract/IPlugin.cs | 32 +- BepInEx.Core/Contract/IPluginLoadContext.cs | 41 --- BepInEx.Core/Contract/PluginInfo.cs | 35 +-- BepInEx.Core/Paths.cs | 12 - BepInEx.Core/Properties/AssemblyInfo.cs | 1 - BepInEx.Core/Utility.cs | 57 ---- .../BepInEx.PatcherProvider.csproj | 17 - .../BepInExPatcherLoadContext.cs | 32 -- .../BepInExPatcherProvider.cs | 71 +---- .../BepInEx.PluginProvider.csproj | 17 - .../BepInExPluginLoadContext.cs | 32 -- .../BepInExPluginProvider.cs | 70 +---- .../Patching/AssemblyPatcher.cs | 253 ++++----------- BepInEx.Preloader.Core/Patching/Attributes.cs | 25 -- .../Patching/BasePatcherProvider.cs | 55 ---- .../Patching/PatcherPluginMetadata.cs | 5 - BepInEx.sln | 20 -- Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs | 21 +- .../BepInEx.NET.Common.csproj | 12 - .../NET/BepInEx.NET.Common/NetChainloader.cs | 3 + .../BepInEx.NET.CoreCLR/NetCorePreloader.cs | 2 +- .../NetPreloader.cs | 2 +- .../Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs | 17 +- .../BepInEx.Unity.IL2CPP.csproj | 12 - .../BepInEx.Unity.IL2CPP/IL2CPPChainloader.cs | 6 +- .../Unity/BepInEx.Unity.IL2CPP/Preloader.cs | 2 +- .../UnityPreloader.cs | 2 +- .../BepInEx.Unity.Mono/BaseUnityPlugin.cs | 18 +- .../BepInEx.Unity.Mono.csproj | 12 - .../Bootstrap/UnityChainloader.cs | 15 +- build/Program.cs | 2 - 35 files changed, 257 insertions(+), 1199 deletions(-) delete mode 100644 BepInEx.Core/Contract/BasePluginProvider.cs delete mode 100644 BepInEx.Core/Contract/IPluginLoadContext.cs delete mode 100644 BepInEx.PatcherProvider/BepInEx.PatcherProvider.csproj delete mode 100644 BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs delete mode 100644 BepInEx.PluginProvider/BepInEx.PluginProvider.csproj delete mode 100644 BepInEx.PluginProvider/BepInExPluginLoadContext.cs delete mode 100644 BepInEx.Preloader.Core/Patching/BasePatcherProvider.cs diff --git a/BepInEx.Core/Bootstrap/BaseChainloader.cs b/BepInEx.Core/Bootstrap/BaseChainloader.cs index 6b62ffd..c6fe182 100644 --- a/BepInEx.Core/Bootstrap/BaseChainloader.cs +++ b/BepInEx.Core/Bootstrap/BaseChainloader.cs @@ -19,23 +19,20 @@ public abstract class BaseChainloader private static Regex allowedGuidRegex { get; } = new(@"^[a-zA-Z0-9\._\-]+$"); - internal BaseChainloader() { } - /// /// Analyzes the given type definition and attempts to convert it to a valid /// /// Type definition to analyze. - /// The load context of the plugin /// The filepath of the assembly, to keep as metadata. /// If the type represent a valid plugin, returns a instance. Otherwise, return null. - public static PluginInfo ToPluginInfo(TypeDefinition type, IPluginLoadContext loadContext, string assemblyLocation) + public static PluginInfo ToPluginInfo(TypeDefinition type, string assemblyLocation) { if (type.IsInterface || type.IsAbstract) return null; try { - if (!type.IsSubtypeOf(typeof(T))) + if (!type.IsSubtypeOf(typeof(TPlugin))) return null; } catch (AssemblyResolutionException) @@ -44,13 +41,8 @@ public abstract class BaseChainloader return null; } - if (type.IsSubtypeOf(typeof(BasePluginProvider))) - return GetPluginInfo(type, BepInPluginProvider.FromCecilType(type), loadContext, assemblyLocation); - return GetPluginInfo(type, BepInPlugin.FromCecilType(type), loadContext, assemblyLocation); - } + var metadata = BepInPlugin.FromCecilType(type); - private static PluginInfo GetPluginInfo(TypeDefinition type, BepInPlugin metadata, IPluginLoadContext loadContext, string assemblyLocation) - { // Perform checks that will prevent the plugin from being loaded in ALL cases if (metadata == null) { @@ -93,16 +85,15 @@ public abstract class BaseChainloader Incompatibilities = incompatibilities, TypeName = type.FullName, TargettedBepInExVersion = bepinVersion, - _location = assemblyLocation, - LoadContext = loadContext + Location = assemblyLocation }; } - protected static bool HasBepinPluginType(AssemblyDefinition ass) + protected 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(T).FullName)) + if (ass.MainModule.GetTypeReferences().All(r => r.FullName != typeof(BepInPlugin).FullName)) return false; return true; @@ -125,47 +116,16 @@ public abstract class BaseChainloader private bool _initialized; /// - /// The current chainloader instance - /// - public static BaseChainloader Instance { get; protected set; } - - /// - /// List of all instances of all plugins loaded via the chainloader. - /// If a plugin fails to load, it is removed. This list only contains the plugins that were actually - /// loaded and have their dependencies satisfied. + /// List of all instances loaded via the chainloader. /// public Dictionary Plugins { get; } = new(); - /// - /// List of all instances of all providers loaded via the chainloader. - /// If a provider fails to load, it is removed. This list only contains the providers that were actually - /// loaded and have their dependencies satisfied. - /// - public Dictionary Providers { get; } = new(); - - /// - /// List of all objects received from plugin providers. - /// Value holds info of the originating provider. These contexts are used by chainloader to - /// load plugins. Plugins that are loaded successfully are added to . - /// - internal Dictionary LoadContexts { get; } = new(); - /// /// Collection of error chainloader messages that occured during plugin loading. /// Contains information about what certain plugins were not loaded. /// public List DependencyErrors { get; } = new(); - /// - /// Occurs after a plugin provider is loaded. - /// - public event Action ProviderLoaded; - - /// - /// Occurs after all plugins providers are loaded. - /// - public event Action FinishedProviders; - /// /// Occurs after a plugin is loaded. /// @@ -189,12 +149,12 @@ public abstract class BaseChainloader InitializeLoggers(); - if (!Directory.Exists(Paths.PluginProviderPath)) - Directory.CreateDirectory(Paths.PluginProviderPath); - if (!Directory.Exists(Paths.PluginPath)) Directory.CreateDirectory(Paths.PluginPath); + if (!Directory.Exists(Paths.PatcherPluginPath)) + Directory.CreateDirectory(Paths.PatcherPluginPath); + _initialized = true; Logger.Log(LogLevel.Message, "Chainloader initialized"); @@ -234,20 +194,29 @@ public abstract class BaseChainloader /// Path from which to search the plugins. /// Cache name to use. If null, results are not cached. /// List of discovered plugins and their metadata. - protected List DiscoverPluginProvidersFrom(string path, string cacheName = "chainloader_meta") + protected IList DiscoverPluginsFrom(string path, string cacheName = "chainloader") { var pluginsToLoad = - TypeLoader.FindPluginTypes(path, ToPluginInfo, HasBepinPluginType, cacheName); + TypeLoader.FindPluginTypes(path, ToPluginInfo, HasBepinPlugins, cacheName); return pluginsToLoad.SelectMany(p => p.Value).ToList(); } + /// + /// Discovers plugins to load. + /// + /// List of plugins to be loaded. + protected virtual IList DiscoverPlugins() + { + return DiscoverPluginsFrom(Paths.PluginPath); + } + /// /// Preprocess the plugins and modify the load order. /// /// Some plugins may be skipped if they cannot be loaded (wrong metadata, etc). /// Plugins to process. /// List of plugins to load in the correct load order. - protected virtual List ModifyLoadOrder(IList plugins, Dictionary existingPlugins) + protected virtual IList ModifyLoadOrder(IList plugins) { // We use a sorted dictionary to ensure consistent load order var dependencyDict = @@ -256,7 +225,7 @@ public abstract class BaseChainloader foreach (var pluginInfoGroup in plugins.GroupBy(info => info.Metadata.GUID)) { - if (existingPlugins.TryGetValue(pluginInfoGroup.Key, out var loadedPlugin)) + if (Plugins.TryGetValue(pluginInfoGroup.Key, out var loadedPlugin)) { Logger.Log(LogLevel.Warning, $"Skipping [{pluginInfoGroup.Key}] because a plugin with a similar GUID ([{loadedPlugin}]) has been already loaded."); @@ -297,7 +266,7 @@ public abstract class BaseChainloader foreach (var pluginInfo in pluginsByGuid.Values.ToList()) if (pluginInfo.Incompatibilities.Any(incompatibility => pluginsByGuid.ContainsKey(incompatibility.IncompatibilityGUID) - || existingPlugins.ContainsKey(incompatibility.IncompatibilityGUID)) + || Plugins.ContainsKey(incompatibility.IncompatibilityGUID)) ) { pluginsByGuid.Remove(pluginInfo.Metadata.GUID); @@ -306,7 +275,7 @@ public abstract class BaseChainloader var incompatiblePluginsNew = pluginInfo.Incompatibilities.Select(x => x.IncompatibilityGUID) .Where(x => pluginsByGuid.ContainsKey(x)); var incompatiblePluginsExisting = pluginInfo.Incompatibilities.Select(x => x.IncompatibilityGUID) - .Where(x => existingPlugins.ContainsKey(x)); + .Where(x => Plugins.ContainsKey(x)); var incompatiblePlugins = incompatiblePluginsNew.Concat(incompatiblePluginsExisting).ToArray(); var message = $@"Could not load [{pluginInfo}] because it is incompatible with: {string.Join(", ", incompatiblePlugins)}"; @@ -337,24 +306,15 @@ public abstract class BaseChainloader } /// - /// Run the chainloader and load all plugins from all providers. + /// Run the chainloader and load all plugins from the plugins folder. /// public virtual void Execute() { try { - var builtinProviders = DiscoverPluginProvidersFrom(Paths.BepInExAssemblyDirectory); - var providers = DiscoverPluginProvidersFrom(Paths.PluginProviderPath); - Logger.Log(LogLevel.Info, $"{providers.Count + builtinProviders.Count} " + - $"plugin provider{(providers.Count + builtinProviders.Count == 1 ? "" : "s")} to load"); - - LoadPlugins(builtinProviders, provider: true); - LoadPlugins(providers, provider: true); - FinishedProviders?.Invoke(); - - var plugins = TypeLoader.GetPluginsFromLoadContexts(LoadContexts.Keys.Cast(), ToPluginInfo, HasBepinPluginType, "chainloader"); + var plugins = DiscoverPlugins(); Logger.Log(LogLevel.Info, $"{plugins.Count} plugin{(plugins.Count == 1 ? "" : "s")} to load"); - LoadPlugins(plugins, provider: false); + LoadPlugins(plugins); Finished?.Invoke(); } catch (Exception ex) @@ -371,143 +331,117 @@ public abstract class BaseChainloader Logger.Log(LogLevel.Message, "Chainloader startup complete"); } - [Obsolete("Use a plugin provider instead", true)] - public IList LoadPlugins(params string[] pluginsPaths) + private IList LoadPlugins(IList plugins) { - throw new NotSupportedException("Use a plugin provider instead"); - } - - private void LoadPlugins(List plugins, bool provider) - { - var existingPluginInfos = provider ? Providers : Plugins; - var sortedPluginInfos = ModifyLoadOrder(plugins, existingPluginInfos); - + var sortedPlugins = ModifyLoadOrder(plugins); + var invalidPlugins = new HashSet(); var processedPlugins = new Dictionary(); var loadedAssemblies = new Dictionary(); + var loadedPlugins = new List(); - foreach (var pluginInfo in sortedPluginInfos) + foreach (var plugin in sortedPlugins) { - if (!CheckDependencies(pluginInfo, processedPlugins, invalidPlugins, existingPluginInfos)) + var dependsOnInvalidPlugin = false; + var missingDependencies = new List(); + foreach (var dependency in plugin.Dependencies) { + static bool IsHardDependency(BepInDependency dep) => + (dep.Flags & BepInDependency.DependencyFlags.HardDependency) != 0; + + // If the dependency wasn't already processed, it's missing altogether + var dependencyExists = + processedPlugins.TryGetValue(dependency.DependencyGUID, out var pluginVersion); + // Alternatively, if the dependency hasn't been loaded before, it's missing too + if (!dependencyExists) + { + dependencyExists = Plugins.TryGetValue(dependency.DependencyGUID, out var pluginInfo); + pluginVersion = pluginInfo?.Metadata.Version; + } + + if (!dependencyExists || dependency.VersionRange != null && + !dependency.VersionRange.IsSatisfied(pluginVersion)) + { + // If the dependency is hard, collect it into a list to show + if (IsHardDependency(dependency)) + missingDependencies.Add(dependency); + continue; + } + + // If the dependency is a hard and is invalid (e.g. has missing dependencies), report that to the user + if (invalidPlugins.Contains(dependency.DependencyGUID) && IsHardDependency(dependency)) + { + dependsOnInvalidPlugin = true; + break; + } + } + + processedPlugins.Add(plugin.Metadata.GUID, plugin.Metadata.Version); + + if (dependsOnInvalidPlugin) + { + var message = + $"Skipping [{plugin}] because it has a dependency that was not loaded. See previous errors for details."; + DependencyErrors.Add(message); + Logger.Log(LogLevel.Warning, message); + continue; + } + + if (missingDependencies.Count != 0) + { + var message = $@"Could not load [{plugin}] because it has missing dependencies: { + string.Join(", ", missingDependencies.Select(s => s.VersionRange == null ? s.DependencyGUID : $"{s.DependencyGUID} ({s.VersionRange})").ToArray()) + }"; + DependencyErrors.Add(message); + Logger.Log(LogLevel.Error, message); + + invalidPlugins.Add(plugin.Metadata.GUID); continue; } try { - Logger.Log(LogLevel.Info, $"Loading [{pluginInfo}]"); + Logger.Log(LogLevel.Info, $"Loading [{plugin}]"); - Assembly ass = null; - if (provider && !loadedAssemblies.TryGetValue(pluginInfo._location, out ass)) - { - ass = Assembly.LoadFrom(pluginInfo._location); - loadedAssemblies[pluginInfo._location] = ass; - } - else if (!provider && !loadedAssemblies.TryGetValue(pluginInfo.LoadContext.AssemblyIdentifier, out ass)) - { - var symbols = pluginInfo.LoadContext.GetAssemblySymbolsData(); - ass = symbols != null ? Assembly.Load(pluginInfo.LoadContext.GetAssemblyData(), symbols) - : Assembly.Load(pluginInfo.LoadContext.GetAssemblyData()); - loadedAssemblies[pluginInfo.LoadContext.AssemblyIdentifier] = ass; - } + if (!loadedAssemblies.TryGetValue(plugin.Location, out var ass)) + loadedAssemblies[plugin.Location] = ass = Assembly.LoadFrom(plugin.Location); - existingPluginInfos[pluginInfo.Metadata.GUID] = pluginInfo; - TryRunModuleCtor(pluginInfo, ass); + Plugins[plugin.Metadata.GUID] = plugin; + TryRunModuleCtor(plugin, ass); + plugin.Instance = LoadPlugin(plugin, ass); + loadedPlugins.Add(plugin); - if (provider) - { - var type = ass.GetType(pluginInfo.TypeName); - pluginInfo.Instance = Activator.CreateInstance(type); - AppDomain.CurrentDomain.AssemblyResolve += (_, args) => ((BasePluginProvider)pluginInfo.Instance).ResolveAssembly(args.Name); - var pluginLoadContexts = ((BasePluginProvider)pluginInfo.Instance).GetPlugins(); - foreach (IPluginLoadContext context in pluginLoadContexts) - { - var cachedContext = new CachedPluginLoadContext(context); - LoadContexts[cachedContext] = pluginInfo; - } - ProviderLoaded?.Invoke(pluginInfo); - } - else - { - pluginInfo.Source = LoadContexts[(CachedPluginLoadContext)pluginInfo.LoadContext]; - pluginInfo.Instance = LoadPlugin(pluginInfo, ass); - PluginLoaded?.Invoke(pluginInfo); - foreach (CachedPluginLoadContext context in LoadContexts.Keys) - context.Dispose(); - } + PluginLoaded?.Invoke(plugin); } catch (Exception ex) { - invalidPlugins.Add(pluginInfo.Metadata.GUID); - existingPluginInfos.Remove(pluginInfo.Metadata.GUID); + invalidPlugins.Add(plugin.Metadata.GUID); + Plugins.Remove(plugin.Metadata.GUID); Logger.Log(LogLevel.Error, - $"Error loading [{pluginInfo}]: {(ex is ReflectionTypeLoadException re ? TypeLoader.TypeLoadExceptionToString(re) : ex.ToString())}"); + $"Error loading [{plugin}]: {(ex is ReflectionTypeLoadException re ? TypeLoader.TypeLoadExceptionToString(re) : ex.ToString())}"); } } + + return loadedPlugins; } - private bool CheckDependencies(T plugin, Dictionary processedPlugins, HashSet invalidPlugins, Dictionary existingPlugins) - where T : PluginInfo + /// + /// Detects and loads all plugins in the specified directories. + /// + /// + /// It is better to collect all paths at once and use a single call to LoadPlugins than multiple calls. + /// This allows to run proper dependency resolving and to load all plugins in one go. + /// + /// Directories to search the plugins from. + /// List of loaded plugin infos. + public IList LoadPlugins(params string[] pluginsPaths) { - var dependsOnInvalidPlugin = false; - var missingDependencies = new List(); - foreach (var dependency in plugin.Dependencies) - { - static bool IsHardDependency(BepInDependency dep) => - (dep.Flags & BepInDependency.DependencyFlags.HardDependency) != 0; - - // If the dependency wasn't already processed, it's missing altogether - var dependencyExists = - processedPlugins.TryGetValue(dependency.DependencyGUID, out var pluginVersion); - // Alternatively, if the dependency hasn't been loaded before, it's missing too - if (!dependencyExists) - { - dependencyExists = existingPlugins.TryGetValue(dependency.DependencyGUID, out var pluginInfo); - pluginVersion = pluginInfo?.Metadata.Version; - } - - if (!dependencyExists || dependency.VersionRange != null && - !dependency.VersionRange.IsSatisfied(pluginVersion)) - { - // If the dependency is hard, collect it into a list to show - if (IsHardDependency(dependency)) - missingDependencies.Add(dependency); - continue; - } - - // If the dependency is a hard and is invalid (e.g. has missing dependencies), report that to the user - if (invalidPlugins.Contains(dependency.DependencyGUID) && IsHardDependency(dependency)) - { - dependsOnInvalidPlugin = true; - break; - } - } - - processedPlugins.Add(plugin.Metadata.GUID, plugin.Metadata.Version); - - if (dependsOnInvalidPlugin) - { - var message = - $"Skipping [{plugin}] because it has a dependency that was not loaded. See previous errors for details."; - DependencyErrors.Add(message); - Logger.Log(LogLevel.Warning, message); - return false; - } - - if (missingDependencies.Count != 0) - { - var message = $@"Could not load [{plugin}] because it has missing dependencies: { - string.Join(", ", missingDependencies.Select(s => s.VersionRange == null ? s.DependencyGUID : $"{s.DependencyGUID} ({s.VersionRange})").ToArray()) - }"; - DependencyErrors.Add(message); - Logger.Log(LogLevel.Error, message); - - invalidPlugins.Add(plugin.Metadata.GUID); - return false; - } - - return true; + // TODO: This is a temporary solution for 3rd party loaders. Instead, this should be done via metaplugins. + var plugins = new List(); + foreach (var pluginsPath in pluginsPaths) + plugins.AddRange(DiscoverPluginsFrom(pluginsPath)); + return LoadPlugins(plugins); } private static void TryRunModuleCtor(PluginInfo plugin, Assembly assembly) diff --git a/BepInEx.Core/Bootstrap/TypeLoader.cs b/BepInEx.Core/Bootstrap/TypeLoader.cs index 63c1295..69a7775 100644 --- a/BepInEx.Core/Bootstrap/TypeLoader.cs +++ b/BepInEx.Core/Bootstrap/TypeLoader.cs @@ -10,41 +10,6 @@ using Mono.Cecil; namespace BepInEx.Bootstrap; -internal class CachedPluginLoadContext : IPluginLoadContext, IDisposable -{ - public IPluginLoadContext PluginLoadContext { get; } - private byte[] assemblyData; - private byte[] assemblySymbolsData; - - public CachedPluginLoadContext(IPluginLoadContext pluginLoadContext) - { - PluginLoadContext = pluginLoadContext; - } - - public string AssemblyIdentifier => PluginLoadContext.AssemblyIdentifier; - public string AssemblyHash => PluginLoadContext.AssemblyHash; - public byte[] GetAssemblyData() - { - return assemblyData ??= PluginLoadContext.GetAssemblyData(); - } - - public byte[] GetAssemblySymbolsData() - { - return assemblySymbolsData ??= PluginLoadContext.GetAssemblySymbolsData(); - } - - public byte[] GetFile(string relativePath) - { - return PluginLoadContext.GetFile(relativePath); - } - - public void Dispose() - { - assemblyData = null; - assemblySymbolsData = null; - } -} - /// /// A cacheable metadata item. Can be used with and /// to cache plugin metadata. @@ -70,11 +35,6 @@ public interface ICacheable /// public class CachedAssembly where T : ICacheable { - /// - /// The version of the cache which increments on each format changes - /// - public const int Version = 0; - /// /// List of cached items inside the assembly. /// @@ -129,9 +89,7 @@ public static class TypeLoader { Paths.BepInExAssemblyDirectory, Paths.PluginPath, - Paths.PluginProviderPath, Paths.PatcherPluginPath, - Paths.PatcherProviderPath, Paths.ManagedPath }.Concat(SearchDirectories); @@ -168,8 +126,9 @@ public static class TypeLoader /// selector. /// public static Dictionary> FindPluginTypes(string directory, - Func typeSelector, - Func assemblyFilter = null, + Func typeSelector, + Func assemblyFilter = + null, string cacheName = null) where T : ICacheable, new() { @@ -194,7 +153,19 @@ public static class TypeLoader continue; } - result[dll] = ExamineStream(typeSelector, assemblyFilter, dllMs, null, dll); + using var ass = AssemblyDefinition.ReadAssembly(dllMs, ReaderParameters); + Logger.Log(LogLevel.Debug, $"Examining '{dll}'"); + + if (!assemblyFilter?.Invoke(ass) ?? false) + { + result[dll] = new List(); + continue; + } + + var matches = ass.MainModule.Types + .Select(t => typeSelector(t, dll)) + .Where(t => t != null).ToList(); + result[dll] = matches; } catch (BadImageFormatException e) { @@ -212,82 +183,6 @@ public static class TypeLoader return result; } - /// - /// Looks up assemblies using the given loaders and locates all types that can be loaded and collects their metadata. - /// - /// The specific base type to search for. - /// The load contexts to obtain the assemblies from. - /// 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 dictionary of all assemblies in the directory and the list of type metadatas of types that match the - /// selector. - /// - public static List GetPluginsFromLoadContexts(IEnumerable loadContexts, - Func typeSelector, - Func assemblyFilter = null, - string cacheName = null) - where T : ICacheable, new() - { - var result = new Dictionary>(); - var hashes = new Dictionary(); - Dictionary> cache = null; - - if (cacheName != null) - cache = LoadAssemblyCache(cacheName); - - foreach (IPluginLoadContext loadContext in loadContexts) - { - IList plugins; - if (cache != null && loadContext.AssemblyHash != null && - cache.TryGetValue(loadContext.AssemblyIdentifier, out var cacheEntry) && - loadContext.AssemblyHash == cacheEntry.Hash) - { - plugins = cacheEntry.CacheItems; - } - else - { - var assemblyData = loadContext.GetAssemblyData(); - using var memory = new MemoryStream(assemblyData); - plugins = ExamineStream(typeSelector, assemblyFilter, memory, loadContext, null); - } - - foreach (T pluginInfo in plugins) - { - if (!result.ContainsKey(loadContext.AssemblyIdentifier)) - result[loadContext.AssemblyIdentifier] = new(); - result[loadContext.AssemblyIdentifier].Add(pluginInfo); - } - } - - if (cache != null) - SaveAssemblyCache(cacheName, result, hashes); - - return result.SelectMany(x => x.Value).ToList(); - } - - private static List ExamineStream(Func typeSelector, - Func assemblyFilter, - MemoryStream dllMs, - IPluginLoadContext loadContext, - string location) - where T : ICacheable, new() - { - using var ass = AssemblyDefinition.ReadAssembly(dllMs, ReaderParameters); - Logger.Log(LogLevel.Debug, $"Examining '{ass.Name}'"); - - if (!assemblyFilter?.Invoke(ass) ?? false) - { - return new List(); - } - - var matches = ass.MainModule.Types - .Select(t => typeSelector(t, loadContext, location)) - .Where(t => t != null).ToList(); - return matches; - } - /// /// Loads an index of type metadatas from a cache. /// @@ -310,9 +205,7 @@ public static class TypeLoader if (!File.Exists(path)) return null; - using var br = new BinaryReader(File.OpenRead(path)); - var version = br.ReadInt32(); - if (version == CachedAssembly.Version) + using (var br = new BinaryReader(File.OpenRead(path))) { var entriesCount = br.ReadInt32(); @@ -366,7 +259,6 @@ public static class TypeLoader var path = Path.Combine(Paths.CachePath, $"{cacheName}_typeloader.dat"); using var bw = new BinaryWriter(File.OpenWrite(path)); - bw.Write(CachedAssembly.Version); bw.Write(entries.Count); foreach (var kv in entries) diff --git a/BepInEx.Core/Contract/Attributes.cs b/BepInEx.Core/Contract/Attributes.cs index 793f456..0de9e93 100644 --- a/BepInEx.Core/Contract/Attributes.cs +++ b/BepInEx.Core/Contract/Attributes.cs @@ -76,32 +76,6 @@ public class BepInPlugin : Attribute } } -/// -/// This attribute denotes that a class is a plugin provider, and specifies the required metadata. -/// -[AttributeUsage(AttributeTargets.Class)] -public class BepInPluginProvider : BepInPlugin -{ - /// The unique identifier of the plugin. Should not change between plugin versions. - /// The user friendly name of the plugin. Is able to be changed between versions. - /// The specific version of the plugin. - public BepInPluginProvider(string GUID, string Name, string Version) : base(GUID, Name, Version) - { - } - - internal new static BepInPluginProvider FromCecilType(TypeDefinition td) - { - var attr = MetadataHelper.GetCustomAttributes(td, false).FirstOrDefault(); - - if (attr == null) - return null; - - return new BepInPluginProvider((string) attr.ConstructorArguments[0].Value, - (string) attr.ConstructorArguments[1].Value, - (string) attr.ConstructorArguments[2].Value); - } -} - /// /// This attribute specifies any dependencies that this plugin has on other plugins. /// @@ -307,28 +281,6 @@ public static class MetadataHelper /// The BepInPlugin metadata of the plugin instance. public static BepInPlugin GetMetadata(object plugin) => GetMetadata(plugin.GetType()); - /// - /// Retrieves the BepInPluginProvider metadata from a plugin type. - /// - /// The plugin type. - /// The BepInPluginProvider metadata of the plugin type. - public static BepInPlugin GetPluginProviderMetadata(Type pluginType) - { - var attributes = pluginType.GetCustomAttributes(typeof(BepInPluginProvider), false); - - if (attributes.Length == 0) - return null; - - return (BepInPlugin) attributes[0]; - } - - /// - /// Retrieves the BepInPluginProvider metadata from a plugin instance. - /// - /// The plugin instance. - /// The BepInPluginProvider metadata of the plugin instance. - public static BepInPlugin GetPluginProviderMetadata(object plugin) => GetPluginProviderMetadata(plugin.GetType()); - /// /// Gets the specified attributes of a type, if they exist. /// diff --git a/BepInEx.Core/Contract/BasePluginProvider.cs b/BepInEx.Core/Contract/BasePluginProvider.cs deleted file mode 100644 index 594044a..0000000 --- a/BepInEx.Core/Contract/BasePluginProvider.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using BepInEx.Configuration; -using BepInEx.Logging; - -namespace BepInEx; - -/// -/// The base plugin provider type that is used by the BepInEx plugin loader. -/// -public abstract class BasePluginProvider -{ - /// - /// Create a new instance of a plugin provider and all of its tied in objects. - /// - /// BepInPluginProvider attribute is missing. - protected BasePluginProvider() - { - var metadata = MetadataHelper.GetPluginProviderMetadata(this); - if (metadata == null) - throw new InvalidOperationException("Can't create an instance of " + GetType().FullName + - " because it inherits from BasePluginProvider and the BepInPluginProvider attribute is missing."); - - Info = new PluginInfo - { - Metadata = metadata, - Instance = this, - Dependencies = MetadataHelper.GetDependencies(GetType()), - Processes = MetadataHelper.GetAttributes(GetType()), - }; - - Logger = BepInEx.Logging.Logger.CreateLogSource(metadata.Name); - - Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, metadata.GUID + ".cfg"), false, metadata); - } - - /// - /// Information about this plugin provider as it was loaded. - /// - public PluginInfo Info { get; } - - /// - /// Logger instance tied to this plugin provider. - /// - protected ManualLogSource Logger { get; } - - /// - /// Default config file tied to this plugin provider. The config file will not be created until - /// any settings are added and changed, or is called. - /// - public ConfigFile Config { get; } - - /// - /// Obtains a list of assemblies containing plugins to load - /// - /// A list of load context, one per assembly - public abstract IList GetPlugins(); - - /// - /// A custom assembly resolver that can be used by this provider to resolve assemblies - /// that have failed to resolve - /// - /// The assembly's name - /// The resolved assembly or null if the assembly couldn't be resolved - public virtual Assembly ResolveAssembly(string name) - { - return null; - } -} diff --git a/BepInEx.Core/Contract/IPlugin.cs b/BepInEx.Core/Contract/IPlugin.cs index 150d151..6043fb0 100644 --- a/BepInEx.Core/Contract/IPlugin.cs +++ b/BepInEx.Core/Contract/IPlugin.cs @@ -7,22 +7,22 @@ using BepInEx.Logging; namespace BepInEx.Contract { - public interface IPlugin - { - /// - /// Information about this plugin as it was loaded. - /// - PluginInfo Info { get; } + public interface IPlugin + { + /// + /// Information about this plugin as it was loaded. + /// + PluginInfo Info { get; } - /// - /// Logger instance tied to this plugin. - /// - ManualLogSource Logger { get; } + /// + /// Logger instance tied to this plugin. + /// + ManualLogSource Logger { get; } - /// - /// Default config file tied to this plugin. The config file will not be created until - /// any settings are added and changed, or is called. - /// - ConfigFile Config { get; } - } + /// + /// Default config file tied to this plugin. The config file will not be created until + /// any settings are added and changed, or is called. + /// + ConfigFile Config { get; } + } } diff --git a/BepInEx.Core/Contract/IPluginLoadContext.cs b/BepInEx.Core/Contract/IPluginLoadContext.cs deleted file mode 100644 index 1812ea8..0000000 --- a/BepInEx.Core/Contract/IPluginLoadContext.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; - -namespace BepInEx; - -/// -/// An interface that allows to dynamically load and track an assembly -/// which can be obtained from a provider -/// -public interface IPluginLoadContext -{ - /// - /// An identifier that uniquely identifies an assembly from a provider no matter its revision - /// - public string AssemblyIdentifier { get; } - - /// - /// An optional hash that changes each time the assembly changes which can be tracked for cache - /// invalidation purposes. If this is null, no caching occurs for this assembly load - /// - public string AssemblyHash { get; } - - /// - /// Obtains the assembly's raw data without loading it into the appdomain. - /// This may be called multiple times by the chainloader - /// - /// The assembly's raw data in bytes - public byte[] GetAssemblyData(); - - /// - /// Obtains the assembly's symbols data which will be loaded by the chainloader alongside the assembly - /// - /// The assembly's symbols data in bytes (either in portable pdb or mdb format). Null if no symbols exists for this assembly - public byte[] GetAssemblySymbolsData(); - - /// - /// Obtains a file's raw data using a relative path from the plugin assembly - /// - /// The relative path from the plugin assembly used to locate the file - /// The file's raw data if resolved successfully, null if the file couldn't be loaded - public byte[] GetFile(string relativePath); -} diff --git a/BepInEx.Core/Contract/PluginInfo.cs b/BepInEx.Core/Contract/PluginInfo.cs index 3c5dfc8..0aa7585 100644 --- a/BepInEx.Core/Contract/PluginInfo.cs +++ b/BepInEx.Core/Contract/PluginInfo.cs @@ -16,17 +16,6 @@ public class PluginInfo : ICacheable /// General metadata about a plugin. /// public BepInPlugin Metadata { get; internal set; } - - /// - /// The of the plugin provider that loaded this plugin. - /// This is null if this plugin is a provider - /// - public PluginInfo Source { get; internal set; } - - /// - /// The load context used to load this plugin, null if it is a provider - /// - public IPluginLoadContext LoadContext { get; internal set; } /// /// Collection of attributes that describe what processes the plugin can run on. @@ -44,26 +33,24 @@ public class PluginInfo : ICacheable /// public IEnumerable Incompatibilities { get; internal set; } - public string TypeName { get; internal set; } - + /// + /// File path to the plugin DLL + /// + public string Location { get; internal set; } + /// /// Instance of the plugin that represents this info. NULL if no plugin is instantiated from info (yet) /// public object Instance { get; internal set; } - + + public string TypeName { get; internal set; } + internal Version TargettedBepInExVersion { get; set; } - - [Obsolete("Use LoadContext.AssemblyIdentifier instead", true)] - public string Location => LoadContext?.AssemblyIdentifier; - - internal string _location { get; set; } - - /// - public override string ToString() => $"{Metadata?.Name} {Metadata?.Version}"; void ICacheable.Save(BinaryWriter bw) { bw.Write(TypeName); + bw.Write(Location); bw.Write(Metadata.GUID); bw.Write(Metadata.Name); @@ -90,6 +77,7 @@ public class PluginInfo : ICacheable void ICacheable.Load(BinaryReader br) { TypeName = br.ReadString(); + Location = br.ReadString(); Metadata = new BepInPlugin(br.ReadString(), br.ReadString(), br.ReadString()); @@ -123,4 +111,7 @@ public class PluginInfo : ICacheable TargettedBepInExVersion = new Version(br.ReadString()); } + + /// + public override string ToString() => $"{Metadata?.Name} {Metadata?.Version}"; } diff --git a/BepInEx.Core/Paths.cs b/BepInEx.Core/Paths.cs index 3371986..6a37342 100644 --- a/BepInEx.Core/Paths.cs +++ b/BepInEx.Core/Paths.cs @@ -75,16 +75,6 @@ public static class Paths /// public static string PatcherPluginPath { get; private set; } - /// - /// The path to the plugin provider folder which resides in the BepInEx folder. - /// - public static string PluginProviderPath { get; private set; } - - /// - /// The path to the patcher plugin provider folder which resides in the BepInEx folder. - /// - public static string PatcherProviderPath { get; private set; } - /// /// The path to the plugin folder which resides in the BepInEx folder. /// @@ -137,9 +127,7 @@ public static class Paths BepInExRootPath = bepinRootPath ?? Path.Combine(GameRootPath, "BepInEx"); ConfigPath = Path.Combine(BepInExRootPath, "config"); BepInExConfigPath = Path.Combine(ConfigPath, "BepInEx.cfg"); - PluginProviderPath = Path.Combine(BepInExRootPath, "plugins-providers"); PluginPath = Path.Combine(BepInExRootPath, "plugins"); - PatcherProviderPath = Path.Combine(BepInExRootPath, "patchers-providers"); PatcherPluginPath = Path.Combine(BepInExRootPath, "patchers"); BepInExAssemblyDirectory = Path.Combine(BepInExRootPath, "core"); BepInExAssemblyPath = Path.Combine(BepInExAssemblyDirectory, diff --git a/BepInEx.Core/Properties/AssemblyInfo.cs b/BepInEx.Core/Properties/AssemblyInfo.cs index 96d56c1..749dcc2 100644 --- a/BepInEx.Core/Properties/AssemblyInfo.cs +++ b/BepInEx.Core/Properties/AssemblyInfo.cs @@ -2,7 +2,6 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("BepInEx.Preloader.Core")] [assembly: InternalsVisibleTo("BepInEx.Unity.Mono")] -[assembly: InternalsVisibleTo("BepInEx.NET.Common")] [assembly: InternalsVisibleTo("BepInEx.NET.Framework.Launcher")] [assembly: InternalsVisibleTo("BepInEx.NET.CoreCLR")] [assembly: InternalsVisibleTo("BepInEx.Unity.IL2CPP")] diff --git a/BepInEx.Core/Utility.cs b/BepInEx.Core/Utility.cs index 08e6c35..a6b93bd 100644 --- a/BepInEx.Core/Utility.cs +++ b/BepInEx.Core/Utility.cs @@ -221,53 +221,6 @@ public static class Utility return false; } - /// - /// Try to resolve the symbols data from an assembly DLL. - /// - /// File path of the DLL assembly. - /// The symbols of the DLL assembly. - /// True, if the symbols were found. Otherwise, false. - public static bool TryResolveAssemblySymbols(string dllPath, out byte[] symbolsData) - { - var subDirectory = Path.GetDirectoryName(dllPath); - - var potentialSymbolsFileNames = new[] - { - $"{dllPath}.mdb", - Path.ChangeExtension(dllPath, ".pdb") - }; - - foreach (var symbolFileName in potentialSymbolsFileNames) - { - var symbolPath = Path.Combine(subDirectory, symbolFileName); - if (!File.Exists(symbolPath)) - continue; - - try - { - symbolsData = File.ReadAllBytes(symbolPath); - } - catch (Exception) - { - continue; - } - - return true; - } - - symbolsData = null; - return false; - } - - private static Assembly LoadAssemblyWithSymbols(string assemblyFilePath) - { - var assemblyData = File.ReadAllBytes(assemblyFilePath); - - return TryResolveAssemblySymbols(assemblyFilePath, out byte[] symbolsData) - ? Assembly.Load(assemblyData, symbolsData) - : Assembly.Load(assemblyData); - } - /// /// Checks whether a given cecil type definition is a subtype of a provided type. /// @@ -306,16 +259,6 @@ public static class Utility TryResolveDllAssembly(assemblyName, directory, s => AssemblyDefinition.ReadAssembly(s, readerParameters), out assembly); - /// - /// Try to resolve and load the given assembly DLL including their symbols file. - /// - /// 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 TryResolveDllAssemblyWithSymbols(AssemblyName assemblyName, string directory, out Assembly assembly) => - TryResolveDllAssembly(assemblyName, directory, LoadAssemblyWithSymbols, out assembly); - /// /// Tries to create a file with the given name /// diff --git a/BepInEx.PatcherProvider/BepInEx.PatcherProvider.csproj b/BepInEx.PatcherProvider/BepInEx.PatcherProvider.csproj deleted file mode 100644 index c37d3e6..0000000 --- a/BepInEx.PatcherProvider/BepInEx.PatcherProvider.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - net35;netstandard2.0 - true - BepInEx patcher provider module - false - - - - - - - - - - diff --git a/BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs b/BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs deleted file mode 100644 index 9cb4cbc..0000000 --- a/BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.IO; - -namespace BepInEx.PatcherProvider; - -internal class BepInExPatcherLoadContext : IPluginLoadContext -{ - public string AssemblyIdentifier { get; internal set; } - public string AssemblyHash { get; internal set; } - public byte[] GetAssemblyData() - { - return File.ReadAllBytes(AssemblyIdentifier); - } - - public byte[] GetAssemblySymbolsData() - { - if (Utility.TryResolveAssemblySymbols(AssemblyIdentifier, out var assemblySymbolsData)) - return assemblySymbolsData; - - return assemblySymbolsData; - } - - public byte[] GetFile(string relativePath) - { - if (relativePath == null) - throw new ArgumentNullException(nameof(relativePath)); - - string assemblyFolder = Path.GetDirectoryName(AssemblyIdentifier); - string filePath = Path.Combine(assemblyFolder, relativePath); - return File.ReadAllBytes(filePath); - } -} diff --git a/BepInEx.PatcherProvider/BepInExPatcherProvider.cs b/BepInEx.PatcherProvider/BepInExPatcherProvider.cs index 7685874..5f28270 100644 --- a/BepInEx.PatcherProvider/BepInExPatcherProvider.cs +++ b/BepInEx.PatcherProvider/BepInExPatcherProvider.cs @@ -1,70 +1 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using BepInEx.Logging; -using BepInEx.Preloader.Core.Patching; - -namespace BepInEx.PatcherProvider; - -[PatcherProviderPluginInfo("BepInExPatcherProvider", "BepInExPatcherProvider", "1.0")] -internal class BepInExPatcherProvider : BasePatcherProvider -{ - private static readonly Dictionary AssemblyLocationsByFilename = new(); - - public override IList GetPatchers() - { - var loadContexts = new List(); - foreach (var dll in Directory.GetFiles(Path.GetFullPath(Paths.PatcherPluginPath), "*.dll", SearchOption.AllDirectories)) - { - try - { - var filename = Path.GetFileNameWithoutExtension(dll); - var foundDirectory = Path.GetDirectoryName(dll); - - // Prioritize the shallowest path of each assembly name - if (AssemblyLocationsByFilename.TryGetValue(filename, out var existingDirectory)) - { - int levelExistingDirectory = existingDirectory?.Count(x => x == Path.DirectorySeparatorChar) ?? 0; - int levelFoundDirectory = foundDirectory?.Count(x => x == Path.DirectorySeparatorChar) ?? 0; - - bool shallowerPathFound = levelExistingDirectory > levelFoundDirectory; - Log.LogWarning($"Found duplicate assemblies filenames: {filename} was found at {foundDirectory} " + - $"while it exists already at {AssemblyLocationsByFilename[filename]}. " + - $"Only the {(shallowerPathFound ? "first" : "second")} will be examined and resolved"); - - if (levelExistingDirectory > levelFoundDirectory) - AssemblyLocationsByFilename[filename] = foundDirectory; - } - else - { - AssemblyLocationsByFilename.Add(filename, foundDirectory); - } - - loadContexts.Add(new BepInExPatcherLoadContext - { - AssemblyHash = File.GetLastWriteTimeUtc(dll).ToString("O"), - AssemblyIdentifier = dll - }); - } - catch (Exception e) - { - Log.Log(LogLevel.Error, e); - } - } - - return loadContexts; - } - - public override Assembly ResolveAssembly(string name) - { - if (!AssemblyLocationsByFilename.TryGetValue(name, out var location)) - return null; - - if (!Utility.TryResolveDllAssemblyWithSymbols(new(name), location, out var ass)) - return null; - - return ass; - } -} + \ No newline at end of file diff --git a/BepInEx.PluginProvider/BepInEx.PluginProvider.csproj b/BepInEx.PluginProvider/BepInEx.PluginProvider.csproj deleted file mode 100644 index e22ed74..0000000 --- a/BepInEx.PluginProvider/BepInEx.PluginProvider.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - net35;netstandard2.0 - true - BepInEx plugin provider module - false - - - - - - - - - - diff --git a/BepInEx.PluginProvider/BepInExPluginLoadContext.cs b/BepInEx.PluginProvider/BepInExPluginLoadContext.cs deleted file mode 100644 index 354e972..0000000 --- a/BepInEx.PluginProvider/BepInExPluginLoadContext.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.IO; - -namespace BepInEx.PluginProvider; - -internal class BepInExPluginLoadContext : IPluginLoadContext -{ - public string AssemblyIdentifier { get; internal set; } - public string AssemblyHash { get; internal set; } - public byte[] GetAssemblyData() - { - return File.ReadAllBytes(AssemblyIdentifier); - } - - public byte[] GetAssemblySymbolsData() - { - if (Utility.TryResolveAssemblySymbols(AssemblyIdentifier, out var assemblySymbolsData)) - return assemblySymbolsData; - - return assemblySymbolsData; - } - - public byte[] GetFile(string relativePath) - { - if (relativePath == null) - throw new ArgumentNullException(nameof(relativePath)); - - string assemblyFolder = Path.GetDirectoryName(AssemblyIdentifier); - string filePath = Path.Combine(assemblyFolder, relativePath); - return File.ReadAllBytes(filePath); - } -} diff --git a/BepInEx.PluginProvider/BepInExPluginProvider.cs b/BepInEx.PluginProvider/BepInExPluginProvider.cs index 6f134ab..5f28270 100644 --- a/BepInEx.PluginProvider/BepInExPluginProvider.cs +++ b/BepInEx.PluginProvider/BepInExPluginProvider.cs @@ -1,69 +1 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using BepInEx.Logging; - -namespace BepInEx.PluginProvider; - -[BepInPluginProvider("BepInExPluginProvider", "BepInExPluginProvider", "1.0")] -internal class BepInExPluginProvider : BasePluginProvider -{ - private static readonly Dictionary AssemblyLocationsByFilename = new(); - - public override IList GetPlugins() - { - var loadContexts = new List(); - foreach (var dll in Directory.GetFiles(Path.GetFullPath(Paths.PluginPath), "*.dll", SearchOption.AllDirectories)) - { - try - { - var filename = Path.GetFileNameWithoutExtension(dll); - var foundDirectory = Path.GetDirectoryName(dll); - - // Prioritize the shallowest path of each assembly name - if (AssemblyLocationsByFilename.TryGetValue(filename, out var existingDirectory)) - { - int levelExistingDirectory = existingDirectory?.Count(x => x == Path.DirectorySeparatorChar) ?? 0; - int levelFoundDirectory = foundDirectory?.Count(x => x == Path.DirectorySeparatorChar) ?? 0; - - bool shallowerPathFound = levelExistingDirectory > levelFoundDirectory; - Logger.LogWarning($"Found duplicate assemblies filenames: {filename} was found at {foundDirectory} " + - $"while it exists already at {AssemblyLocationsByFilename[filename]}. " + - $"Only the {(shallowerPathFound ? "first" : "second")} will be examined and resolved"); - - if (levelExistingDirectory > levelFoundDirectory) - AssemblyLocationsByFilename[filename] = foundDirectory; - } - else - { - AssemblyLocationsByFilename.Add(filename, foundDirectory); - } - - loadContexts.Add(new BepInExPluginLoadContext - { - AssemblyHash = File.GetLastWriteTimeUtc(dll).ToString("O"), - AssemblyIdentifier = dll - }); - } - catch (Exception e) - { - Logger.Log(LogLevel.Error, e); - } - } - - return loadContexts; - } - - public override Assembly ResolveAssembly(string name) - { - if (!AssemblyLocationsByFilename.TryGetValue(name, out var location)) - return null; - - if (!Utility.TryResolveDllAssemblyWithSymbols(new(name), location, out var ass)) - return null; - - return ass; - } -} + \ No newline at end of file diff --git a/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs b/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs index 3022e6a..ad25d31 100644 --- a/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs +++ b/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs @@ -11,7 +11,6 @@ using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Mono.Cecil; -using MonoMod.Utils; namespace BepInEx.Preloader.Core.Patching; @@ -25,18 +24,9 @@ public class AssemblyPatcher : IDisposable private Func assemblyLoader; - private readonly List loadContexts = new(); - private readonly List providers = new(); - public AssemblyPatcher(Func assemblyLoader) { this.assemblyLoader = assemblyLoader; - - if (!Directory.Exists(Paths.PatcherProviderPath)) - Directory.CreateDirectory(Paths.PatcherProviderPath); - - if (!Directory.Exists(Paths.PatcherPluginPath)) - Directory.CreateDirectory(Paths.PatcherPluginPath); } /// @@ -71,18 +61,16 @@ public class AssemblyPatcher : IDisposable // Clear to allow GC collection. PatcherContext.PatcherPlugins.Clear(); - - AppDomain.CurrentDomain.AssemblyResolve -= PatcherProvidersAssemblyResolver; } - private PatcherPluginMetadata ToPatcherPlugin(TypeDefinition type, IPluginLoadContext loadContext, string assemblyPath) + private PatcherPluginMetadata ToPatcherPlugin(TypeDefinition type, string assemblyPath) { if (type.IsInterface || type.IsAbstract && !type.IsSealed) return null; try { - if (!type.IsSubtypeOf(typeof(T))) + if (!type.IsSubtypeOf(typeof(BasePatcher))) return null; } catch (AssemblyResolutionException) @@ -91,13 +79,8 @@ public class AssemblyPatcher : IDisposable return null; } - if (type.IsSubtypeOf(typeof(BasePatcherProvider))) - return GetPatcherInfo(type, loadContext, PatcherProviderPluginInfoAttribute.FromCecilType(type)); - return GetPatcherInfo(type, loadContext, PatcherPluginInfoAttribute.FromCecilType(type)); - } + var metadata = PatcherPluginInfoAttribute.FromCecilType(type); - private PatcherPluginMetadata GetPatcherInfo(TypeDefinition type, IPluginLoadContext loadContext, PatcherPluginInfoAttribute metadata) - { // Perform checks that will prevent the plugin from being loaded in ALL cases if (metadata == null) { @@ -126,176 +109,20 @@ public class AssemblyPatcher : IDisposable return new PatcherPluginMetadata { - TypeName = type.FullName, - LoadContext = loadContext + TypeName = type.FullName }; } - private bool HasPatcherType(AssemblyDefinition ass) + private bool HasPatcherPlugins(AssemblyDefinition ass) { if (ass.MainModule.AssemblyReferences.All(r => r.Name != CurrentAssemblyName) && ass.Name.Name != CurrentAssemblyName) return false; - if (ass.MainModule.GetTypeReferences().All(r => r.FullName != typeof(T).FullName)) + if (ass.MainModule.GetTypeReferences().All(r => r.FullName != typeof(BasePatcher).FullName)) return false; return true; } - - /// - /// Adds all patchers from all managed assemblies obtained from all patcher providers. - /// - public void AddPatchersFromProviders() - { - var allProviders = TypeLoader.FindPluginTypes(Paths.BepInExAssemblyDirectory, ToPatcherPlugin, HasPatcherType); - var foundProviders = TypeLoader.FindPluginTypes(Paths.PatcherProviderPath, ToPatcherPlugin, HasPatcherType); - allProviders.AddRange(foundProviders); - AppDomain.CurrentDomain.AssemblyResolve += PatcherProvidersAssemblyResolver; - - foreach (var keyValuePair in allProviders) - { - var assemblyPath = keyValuePair.Key; - var patcherCollection = keyValuePair.Value; - - if (patcherCollection.Count == 0) - continue; - - var ass = Assembly.LoadFrom(assemblyPath); - - foreach (var patcherPlugin in patcherCollection) - { - try - { - var type = ass.GetType(patcherPlugin.TypeName); - - var instance = (BasePatcherProvider) Activator.CreateInstance(type); - providers.Add(instance); - loadContexts.AddRange(instance.GetPatchers().Select(x => new CachedPluginLoadContext(x))); - } - catch (Exception e) - { - Logger.Log(LogLevel.Error, - $"Failed to load patchers from type [{patcherPlugin.TypeName}]: {(e is ReflectionTypeLoadException re ? TypeLoader.TypeLoadExceptionToString(re) : e.ToString())}"); - } - } - } - - var patchers = TypeLoader.GetPluginsFromLoadContexts(loadContexts.Cast(), ToPatcherPlugin, HasPatcherType); - - var sortedPatchers = new List(); - - // TODO: Add dependency ordering and process attribute filtering - - foreach (var patcherPlugin in patchers) - { - var ass = Assembly.Load(patcherPlugin.LoadContext.GetAssemblyData()); - var patchDefinitions = LoadPatcherPlugin(ass, patcherPlugin); - sortedPatchers.AddRange(patchDefinitions); - - var assName = ass.GetName(); - Logger.Log(LogLevel.Debug, $"Loaded patcher {patcherPlugin.TypeName} from [{assName.Name} {assName.Version}]"); - } - - PatcherContext.PatchDefinitions.AddRange(sortedPatchers); - Logger.Log(LogLevel.Info, $"Loaded {sortedPatchers.Count} patcher{(sortedPatchers.Count > 0 ? "s" : "")}"); - - foreach (CachedPluginLoadContext loadContext in loadContexts) - { - loadContext.Dispose(); - } - } - - private Assembly PatcherProvidersAssemblyResolver(object sender, ResolveEventArgs args) - { - BasePatcherProvider currentProvider = null; - try - { - foreach (BasePatcherProvider provider in providers) - { - currentProvider = provider; - var ass = provider.ResolveAssembly(args.Name); - if (ass != null) - return ass; - } - } - catch (Exception e) - { - Logger.Log(LogLevel.Error, $"The assembly resolver of the patcher provider [{currentProvider?.Info.GUID}] " + - $"threw an exception while resolving {args.Name}: {e}"); - return null; - } - - return null; - } - - private List LoadPatcherPlugin(Assembly ass, PatcherPluginMetadata patcher) - { - var patchDefinitions = new List(); - try - { - var type = ass.GetType(patcher.TypeName); - - var instance = (BasePatcher) Activator.CreateInstance(type); - instance.Context = PatcherContext; - - PatcherContext.PatcherPlugins.Add(instance); - - var methods = - type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - - foreach (var method in methods) - { - var targetAssemblies = MetadataHelper.GetAttributes(method); - var targetTypes = MetadataHelper.GetAttributes(method); - - if (targetAssemblies.Length == 0 && targetTypes.Length == 0) - continue; - - var parameters = method.GetParameters(); - - if (parameters.Length < 1 || parameters.Length > 2 - // Next few lines ensure that the first parameter is AssemblyDefinition and does not have any - // target type attributes, and vice versa - || !( - parameters[0].ParameterType == typeof(AssemblyDefinition) - || parameters[0].ParameterType == - typeof(AssemblyDefinition).MakeByRefType() - && targetTypes.Length == 0 - || parameters[0].ParameterType == typeof(TypeDefinition) - && targetAssemblies.Length == 0 - ) - || parameters.Length == 2 && - parameters[1].ParameterType != typeof(string) - || method.ReturnType != typeof(void) && - method.ReturnType != typeof(bool) - ) - { - Logger - .Log(LogLevel.Warning, - $"Skipping method [{method.FullDescription()}] as it is not a valid patcher method"); - continue; - } - - void AddDefinition(PatchDefinition definition) - { - Logger.Log(LogLevel.Debug, $"Discovered patch [{definition.FullName}]"); - patchDefinitions.Add(definition); - } - - foreach (var targetAssembly in targetAssemblies) - AddDefinition(new PatchDefinition(targetAssembly, instance, method)); - foreach (var targetType in targetTypes) - AddDefinition(new PatchDefinition(targetType, instance, method)); - } - } - catch (Exception e) - { - Logger.Log(LogLevel.Error, - $"Failed to load patchers from type [{patcher.TypeName}]: {(e is ReflectionTypeLoadException re ? TypeLoader.TypeLoadExceptionToString(re) : e.ToString())}"); - } - - return patchDefinitions; - } /// /// Adds all patchers from all managed assemblies specified in a directory. @@ -308,7 +135,7 @@ public class AssemblyPatcher : IDisposable var sortedPatchers = new List(); - var patchers = TypeLoader.FindPluginTypes(directory, ToPatcherPlugin, HasPatcherType); + var patchers = TypeLoader.FindPluginTypes(directory, ToPatcherPlugin, HasPatcherPlugins); // TODO: Add dependency ordering and process attribute filtering @@ -323,10 +150,68 @@ public class AssemblyPatcher : IDisposable var ass = Assembly.LoadFrom(assemblyPath); foreach (var patcherPlugin in patcherCollection) - { - var patchDefinitions = LoadPatcherPlugin(ass, patcherPlugin); - sortedPatchers.AddRange(patchDefinitions); - } + try + { + var type = ass.GetType(patcherPlugin.TypeName); + + var instance = (BasePatcher) Activator.CreateInstance(type); + instance.Context = PatcherContext; + + PatcherContext.PatcherPlugins.Add(instance); + + var methods = + type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + foreach (var method in methods) + { + var targetAssemblies = MetadataHelper.GetAttributes(method); + var targetTypes = MetadataHelper.GetAttributes(method); + + if (targetAssemblies.Length == 0 && targetTypes.Length == 0) + continue; + + var parameters = method.GetParameters(); + + if (parameters.Length < 1 || parameters.Length > 2 + // Next few lines ensure that the first parameter is AssemblyDefinition and does not have any + // target type attributes, and vice versa + || !( + parameters[0].ParameterType == typeof(AssemblyDefinition) + || parameters[0].ParameterType == + typeof(AssemblyDefinition).MakeByRefType() + && targetTypes.Length == 0 + || parameters[0].ParameterType == typeof(TypeDefinition) + && targetAssemblies.Length == 0 + ) + || parameters.Length == 2 && + parameters[1].ParameterType != typeof(string) + || method.ReturnType != typeof(void) && + method.ReturnType != typeof(bool) + ) + { + Logger + .Log(LogLevel.Warning, + $"Skipping method [{method.FullDescription()}] as it is not a valid patcher method"); + continue; + } + + void AddDefinition(PatchDefinition definition) + { + Logger.Log(LogLevel.Debug, $"Discovered patch [{definition.FullName}]"); + sortedPatchers.Add(definition); + } + + foreach (var targetAssembly in targetAssemblies) + AddDefinition(new PatchDefinition(targetAssembly, instance, method)); + foreach (var targetType in targetTypes) + AddDefinition(new PatchDefinition(targetType, instance, method)); + } + } + catch (Exception e) + { + Logger.Log(LogLevel.Error, + $"Failed to load patchers from type [{patcherPlugin.TypeName}]: {(e is ReflectionTypeLoadException re ? TypeLoader.TypeLoadExceptionToString(re) : e.ToString())}"); + } var assName = ass.GetName(); Logger.Log(patcherCollection.Any() ? LogLevel.Info : LogLevel.Debug, diff --git a/BepInEx.Preloader.Core/Patching/Attributes.cs b/BepInEx.Preloader.Core/Patching/Attributes.cs index 1b2ccab..95b21c4 100644 --- a/BepInEx.Preloader.Core/Patching/Attributes.cs +++ b/BepInEx.Preloader.Core/Patching/Attributes.cs @@ -79,31 +79,6 @@ public class PatcherPluginInfoAttribute : Attribute } } -/// -/// This attribute denotes that a class is a patcher plugin provider, and specifies the required metadata. -/// -[AttributeUsage(AttributeTargets.Class)] -public class PatcherProviderPluginInfoAttribute : PatcherPluginInfoAttribute { - /// The unique identifier of the plugin. Should not change between plugin versions. - /// The user friendly name of the plugin. Is able to be changed between versions. - /// The specific version of the plugin. - public PatcherProviderPluginInfoAttribute(string GUID, string Name, string Version) : base(GUID, Name, Version) - { - } - - internal new static PatcherProviderPluginInfoAttribute FromCecilType(TypeDefinition td) - { - var attr = MetadataHelper.GetCustomAttributes(td, false).FirstOrDefault(); - - if (attr == null) - return null; - - return new PatcherProviderPluginInfoAttribute((string) attr.ConstructorArguments[0].Value, - (string) attr.ConstructorArguments[1].Value, - (string) attr.ConstructorArguments[2].Value); - } -} - /// /// Defines an assembly that a patch method will target. /// diff --git a/BepInEx.Preloader.Core/Patching/BasePatcherProvider.cs b/BepInEx.Preloader.Core/Patching/BasePatcherProvider.cs deleted file mode 100644 index 08c3503..0000000 --- a/BepInEx.Preloader.Core/Patching/BasePatcherProvider.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Collections.Generic; -using System.Reflection; -using BepInEx.Configuration; -using BepInEx.Logging; - -namespace BepInEx.Preloader.Core.Patching; - -/// -/// A patcher provider that can discover and load assemblies containing patcher plugins -/// -public abstract class BasePatcherProvider -{ - protected BasePatcherProvider() - { - Info = PatcherPluginInfoAttribute.FromType(GetType()); - - Log = Logger.CreateLogSource(Info.Name); - - Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, Info.GUID + ".cfg"), false, - new BepInPlugin(Info.GUID, Info.Name, Info.Version.ToString())); - } - - /// - /// A instance created for use by this patcher plugin provider. - /// - public ManualLogSource Log { get; } - - /// - /// A configuration file binding created with the of this provider as the - /// filename. - /// - public ConfigFile Config { get; } - - /// - /// Metadata associated with this patcher plugin provider. - /// - public PatcherPluginInfoAttribute Info { get; } - - /// - /// Obtains a list of assemblies containing patchers to load - /// - /// A list of load context, one per assembly - public abstract IList GetPatchers(); - - /// - /// A custom assembly resolver that can be used by this provider to resolve assemblies - /// that have failed to resolve - /// - /// The assembly's name - /// The resolved assembly or null if the assembly couldn't be resolved - public virtual Assembly ResolveAssembly(string name) - { - return null; - } -} diff --git a/BepInEx.Preloader.Core/Patching/PatcherPluginMetadata.cs b/BepInEx.Preloader.Core/Patching/PatcherPluginMetadata.cs index ceb88b7..6994dea 100644 --- a/BepInEx.Preloader.Core/Patching/PatcherPluginMetadata.cs +++ b/BepInEx.Preloader.Core/Patching/PatcherPluginMetadata.cs @@ -13,11 +13,6 @@ internal class PatcherPluginMetadata : ICacheable /// public string TypeName { get; set; } = string.Empty; - /// - /// The load context used to load this patcher, null if it is a provider - /// - public IPluginLoadContext LoadContext { get; set; } - /// public void Save(BinaryWriter bw) => bw.Write(TypeName); diff --git a/BepInEx.sln b/BepInEx.sln index e723eb1..356b05d 100644 --- a/BepInEx.sln +++ b/BepInEx.sln @@ -51,10 +51,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Doorstop", "Doorstop", "{CE EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Build", "build\Build.csproj", "{0C38DDF2-EAAA-43FA-BA0A-985636AC6040}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BepInEx.PluginProvider", "BepInEx.PluginProvider\BepInEx.PluginProvider.csproj", "{CBE495BC-0258-4F36-8B3D-1CA64D97E90A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BepInEx.PatcherProvider", "BepInEx.PatcherProvider\BepInEx.PatcherProvider.csproj", "{F88995B3-A5A2-4FC1-A11B-E2AA17483B20}" -EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution BepInEx.NetLauncher.Shared\BepInEx.NetLauncher.Shared.projitems*{067bcc8c-fc25-4929-b4f4-c6a5a7496994}*SharedItemsImports = 13 @@ -151,22 +147,6 @@ Global {0C38DDF2-EAAA-43FA-BA0A-985636AC6040}.Release_Unity|Any CPU.ActiveCfg = Debug|Any CPU {0C38DDF2-EAAA-43FA-BA0A-985636AC6040}.Release_Unity|Any CPU.Build.0 = Debug|Any CPU {0C38DDF2-EAAA-43FA-BA0A-985636AC6040}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CBE495BC-0258-4F36-8B3D-1CA64D97E90A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CBE495BC-0258-4F36-8B3D-1CA64D97E90A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CBE495BC-0258-4F36-8B3D-1CA64D97E90A}.Release_NetFramework|Any CPU.ActiveCfg = Debug|Any CPU - {CBE495BC-0258-4F36-8B3D-1CA64D97E90A}.Release_NetFramework|Any CPU.Build.0 = Debug|Any CPU - {CBE495BC-0258-4F36-8B3D-1CA64D97E90A}.Release_Unity|Any CPU.ActiveCfg = Debug|Any CPU - {CBE495BC-0258-4F36-8B3D-1CA64D97E90A}.Release_Unity|Any CPU.Build.0 = Debug|Any CPU - {CBE495BC-0258-4F36-8B3D-1CA64D97E90A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CBE495BC-0258-4F36-8B3D-1CA64D97E90A}.Release|Any CPU.Build.0 = Release|Any CPU - {F88995B3-A5A2-4FC1-A11B-E2AA17483B20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F88995B3-A5A2-4FC1-A11B-E2AA17483B20}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F88995B3-A5A2-4FC1-A11B-E2AA17483B20}.Release_NetFramework|Any CPU.ActiveCfg = Debug|Any CPU - {F88995B3-A5A2-4FC1-A11B-E2AA17483B20}.Release_NetFramework|Any CPU.Build.0 = Debug|Any CPU - {F88995B3-A5A2-4FC1-A11B-E2AA17483B20}.Release_Unity|Any CPU.ActiveCfg = Debug|Any CPU - {F88995B3-A5A2-4FC1-A11B-E2AA17483B20}.Release_Unity|Any CPU.Build.0 = Debug|Any CPU - {F88995B3-A5A2-4FC1-A11B-E2AA17483B20}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F88995B3-A5A2-4FC1-A11B-E2AA17483B20}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs b/Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs index d9b375a..5d40b10 100644 --- a/Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs +++ b/Runtimes/NET/BepInEx.NET.Common/BasePlugin.cs @@ -1,5 +1,3 @@ -using System; -using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; @@ -14,28 +12,13 @@ namespace BepInEx.NET.Common HarmonyInstance = new Harmony("BepInEx.Plugin." + metadata.GUID); - if (BaseChainloader.Instance.Plugins.TryGetValue(metadata.GUID, out var pluginInfo)) - { - Info = pluginInfo; - Info.Instance = this; - } - else - { - throw new InvalidOperationException($"The plugin information for {metadata.GUID} couldn't be found on the chainloader"); - } - - Log = BepInEx.Logging.Logger.CreateLogSource(metadata.Name); + Log = Logger.CreateLogSource(metadata.Name); Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, metadata.GUID + ".cfg"), false, metadata); } - - /// - public PluginInfo Info { get; } - - /// + public ManualLogSource Log { get; } - /// public ConfigFile Config { get; } public Harmony HarmonyInstance { get; set; } diff --git a/Runtimes/NET/BepInEx.NET.Common/BepInEx.NET.Common.csproj b/Runtimes/NET/BepInEx.NET.Common/BepInEx.NET.Common.csproj index f715bf1..6884e40 100644 --- a/Runtimes/NET/BepInEx.NET.Common/BepInEx.NET.Common.csproj +++ b/Runtimes/NET/BepInEx.NET.Common/BepInEx.NET.Common.csproj @@ -12,18 +12,6 @@ - - false - None - PreserveNewest - Build;DocumentationProjectOutputGroup - - - false - None - PreserveNewest - Build;DocumentationProjectOutputGroup - diff --git a/Runtimes/NET/BepInEx.NET.Common/NetChainloader.cs b/Runtimes/NET/BepInEx.NET.Common/NetChainloader.cs index 9f9717c..5e4af66 100644 --- a/Runtimes/NET/BepInEx.NET.Common/NetChainloader.cs +++ b/Runtimes/NET/BepInEx.NET.Common/NetChainloader.cs @@ -7,6 +7,9 @@ namespace BepInEx.NET.Common { public class NetChainloader : BaseChainloader { + // TODO: Remove once proper instance handling exists + public static NetChainloader Instance { get; set; } + public override void Initialize(string gameExePath = null) { Instance = this; diff --git a/Runtimes/NET/BepInEx.NET.CoreCLR/NetCorePreloader.cs b/Runtimes/NET/BepInEx.NET.CoreCLR/NetCorePreloader.cs index 3448cdb..90e7aa7 100644 --- a/Runtimes/NET/BepInEx.NET.CoreCLR/NetCorePreloader.cs +++ b/Runtimes/NET/BepInEx.NET.CoreCLR/NetCorePreloader.cs @@ -54,7 +54,7 @@ namespace BepInEx.NET.CoreCLR using (var assemblyPatcher = new AssemblyPatcher((data, _) => Assembly.Load(data))) { - assemblyPatcher.AddPatchersFromProviders(); + assemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath); Log.LogInfo($"{assemblyPatcher.PatcherContext.PatchDefinitions.Count} patcher definition(s) loaded"); diff --git a/Runtimes/NET/BepInEx.NET.Framework.Launcher/NetPreloader.cs b/Runtimes/NET/BepInEx.NET.Framework.Launcher/NetPreloader.cs index 51fbe08..f7396bb 100644 --- a/Runtimes/NET/BepInEx.NET.Framework.Launcher/NetPreloader.cs +++ b/Runtimes/NET/BepInEx.NET.Framework.Launcher/NetPreloader.cs @@ -110,7 +110,7 @@ public static class NetPreloader using (var assemblyPatcher = new AssemblyPatcher((data, _) => Assembly.Load(data))) { - assemblyPatcher.AddPatchersFromProviders(); + assemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath); Log.Log(LogLevel.Info, $"{assemblyPatcher.PatcherContext.PatchDefinitions.Count} patcher definition(s) loaded"); diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs index 1a0fda3..3b51e33 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BasePlugin.cs @@ -1,5 +1,4 @@ -using System; -using BepInEx.Configuration; +using BepInEx.Configuration; using BepInEx.Logging; using Il2CppInterop.Runtime.InteropTypes; @@ -11,23 +10,11 @@ public abstract class BasePlugin { var metadata = MetadataHelper.GetMetadata(this); - if (IL2CPPChainloader.Instance.Plugins.TryGetValue(metadata.GUID, out var pluginInfo)) - { - Info = pluginInfo; - Info.Instance = this; - } - else - { - throw new InvalidOperationException($"The plugin information for {metadata.GUID} couldn't be found on the chainloader"); - } - - Log = BepInEx.Logging.Logger.CreateLogSource(metadata.Name); + Log = Logger.CreateLogSource(metadata.Name); Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, metadata.GUID + ".cfg"), false, metadata); } - public PluginInfo Info { get; } - public ManualLogSource Log { get; } public ConfigFile Config { get; } diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj index c92c6b8..bfc326a 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj @@ -9,18 +9,6 @@ - - false - None - PreserveNewest - Build;DocumentationProjectOutputGroup - - - false - None - PreserveNewest - Build;DocumentationProjectOutputGroup - diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/IL2CPPChainloader.cs b/Runtimes/Unity/BepInEx.Unity.IL2CPP/IL2CPPChainloader.cs index bc130b6..1ccfb9e 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/IL2CPPChainloader.cs +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/IL2CPPChainloader.cs @@ -32,11 +32,7 @@ public class IL2CPPChainloader : BaseChainloader private static INativeDetour RuntimeInvokeDetour { get; set; } - public static new IL2CPPChainloader Instance - { - get => (IL2CPPChainloader)BaseChainloader.Instance; - set => BaseChainloader.Instance = value; - } + public static IL2CPPChainloader Instance { get; set; } /// /// Register and add a Unity Component (for example MonoBehaviour) into BepInEx global manager. diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/Preloader.cs b/Runtimes/Unity/BepInEx.Unity.IL2CPP/Preloader.cs index 0f4f52e..690d4ef 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/Preloader.cs +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/Preloader.cs @@ -67,7 +67,7 @@ public static class Preloader using (var assemblyPatcher = new AssemblyPatcher((data, _) => Assembly.Load(data))) { - assemblyPatcher.AddPatchersFromProviders(); + assemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath); Log.LogInfo($"{assemblyPatcher.PatcherContext.PatcherPlugins.Count} patcher plugin{(assemblyPatcher.PatcherContext.PatcherPlugins.Count == 1 ? "" : "s")} loaded"); diff --git a/Runtimes/Unity/BepInEx.Unity.Mono.Preloader/UnityPreloader.cs b/Runtimes/Unity/BepInEx.Unity.Mono.Preloader/UnityPreloader.cs index b6b2a42..6cc3b33 100644 --- a/Runtimes/Unity/BepInEx.Unity.Mono.Preloader/UnityPreloader.cs +++ b/Runtimes/Unity/BepInEx.Unity.Mono.Preloader/UnityPreloader.cs @@ -77,7 +77,7 @@ internal static class UnityPreloader using (var assemblyPatcher = new AssemblyPatcher(MonoAssemblyHelper.LoadFromMemory)) { assemblyPatcher.AddPatchersFromDirectory(Paths.BepInExAssemblyDirectory); - assemblyPatcher.AddPatchersFromProviders(); + assemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath); Log.Log(LogLevel.Info, $"{assemblyPatcher.PatcherContext.PatcherPlugins.Count} patcher plugin{(assemblyPatcher.PatcherContext.PatcherPlugins.Count == 1 ? "" : "s")} loaded"); diff --git a/Runtimes/Unity/BepInEx.Unity.Mono/BaseUnityPlugin.cs b/Runtimes/Unity/BepInEx.Unity.Mono/BaseUnityPlugin.cs index 21fde4f..4043771 100644 --- a/Runtimes/Unity/BepInEx.Unity.Mono/BaseUnityPlugin.cs +++ b/Runtimes/Unity/BepInEx.Unity.Mono/BaseUnityPlugin.cs @@ -1,5 +1,4 @@ using System; -using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using UnityEngine; @@ -22,15 +21,14 @@ public abstract class BaseUnityPlugin : MonoBehaviour throw new InvalidOperationException("Can't create an instance of " + GetType().FullName + " because it inherits from BaseUnityPlugin and the BepInPlugin attribute is missing."); - if (BaseChainloader.Instance.Plugins.TryGetValue(metadata.GUID, out var pluginInfo)) + Info = new PluginInfo { - Info = pluginInfo; - Info.Instance = this; - } - else - { - throw new InvalidOperationException($"The plugin information for {metadata.GUID} couldn't be found on the chainloader"); - } + Metadata = metadata, + Instance = this, + Dependencies = MetadataHelper.GetDependencies(GetType()), + Processes = MetadataHelper.GetAttributes(GetType()), + Location = GetType().Assembly.Location + }; Logger = BepInEx.Logging.Logger.CreateLogSource(metadata.Name); @@ -45,7 +43,7 @@ public abstract class BaseUnityPlugin : MonoBehaviour /// /// Logger instance tied to this plugin. /// - public ManualLogSource Logger { get; } + protected ManualLogSource Logger { get; } /// /// Default config file tied to this plugin. The config file will not be created until diff --git a/Runtimes/Unity/BepInEx.Unity.Mono/BepInEx.Unity.Mono.csproj b/Runtimes/Unity/BepInEx.Unity.Mono/BepInEx.Unity.Mono.csproj index 558ed33..81b0d72 100644 --- a/Runtimes/Unity/BepInEx.Unity.Mono/BepInEx.Unity.Mono.csproj +++ b/Runtimes/Unity/BepInEx.Unity.Mono/BepInEx.Unity.Mono.csproj @@ -11,18 +11,6 @@ - - false - None - PreserveNewest - Build;DocumentationProjectOutputGroup - - - false - None - PreserveNewest - Build;DocumentationProjectOutputGroup - diff --git a/Runtimes/Unity/BepInEx.Unity.Mono/Bootstrap/UnityChainloader.cs b/Runtimes/Unity/BepInEx.Unity.Mono/Bootstrap/UnityChainloader.cs index 19769e8..c08a8de 100644 --- a/Runtimes/Unity/BepInEx.Unity.Mono/Bootstrap/UnityChainloader.cs +++ b/Runtimes/Unity/BepInEx.Unity.Mono/Bootstrap/UnityChainloader.cs @@ -37,17 +37,14 @@ public class UnityChainloader : BaseChainloader private string _consoleTitle; + // TODO: Remove once proper instance handling exists + public static UnityChainloader Instance { get; set; } + /// /// The GameObject that all plugins are attached to as components. /// public static GameObject ManagerObject { get; private set; } - public static new UnityChainloader Instance - { - get => (UnityChainloader)BaseChainloader.Instance; - set => BaseChainloader.Instance = value; - } - protected override string ConsoleTitle => _consoleTitle; @@ -73,9 +70,9 @@ public class UnityChainloader : BaseChainloader Logger.Log(LogLevel.Debug, "Entering chainloader StaticStart"); - Instance = new UnityChainloader(); - Instance.Initialize(gameExePath); - Instance.Execute(); + var instance = new UnityChainloader(); + instance.Initialize(gameExePath); + instance.Execute(); Logger.Log(LogLevel.Debug, "Exiting chainloader StaticStart"); } diff --git a/build/Program.cs b/build/Program.cs index ab63c15..0ba5f0a 100644 --- a/build/Program.cs +++ b/build/Program.cs @@ -239,10 +239,8 @@ public sealed class MakeDistTask : FrostingTask var bepInExCoreDir = bepInExDir.Combine("core"); ctx.CreateDirectory(bepInExDir); ctx.CreateDirectory(bepInExCoreDir); - ctx.CreateDirectory(bepInExDir.Combine("plugins-providers")); ctx.CreateDirectory(bepInExDir.Combine("plugins")); ctx.CreateDirectory(bepInExDir.Combine("patchers")); - ctx.CreateDirectory(bepInExDir.Combine("patchers-providers")); File.WriteAllText(targetDir.CombineWithFilePath("changelog.txt").FullPath, changelog); From e2d5ab8ebcff26478b35e6c84f37d808b9f01312 Mon Sep 17 00:00:00 2001 From: ManlyMarco <39247311+ManlyMarco@users.noreply.github.com> Date: Tue, 24 Sep 2024 20:32:25 +0200 Subject: [PATCH 44/65] Clean up empty files --- BepInEx.PatcherProvider/BepInExPatcherProvider.cs | 1 - BepInEx.PluginProvider/BepInExPluginProvider.cs | 1 - 2 files changed, 2 deletions(-) delete mode 100644 BepInEx.PatcherProvider/BepInExPatcherProvider.cs delete mode 100644 BepInEx.PluginProvider/BepInExPluginProvider.cs diff --git a/BepInEx.PatcherProvider/BepInExPatcherProvider.cs b/BepInEx.PatcherProvider/BepInExPatcherProvider.cs deleted file mode 100644 index 5f28270..0000000 --- a/BepInEx.PatcherProvider/BepInExPatcherProvider.cs +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/BepInEx.PluginProvider/BepInExPluginProvider.cs b/BepInEx.PluginProvider/BepInExPluginProvider.cs deleted file mode 100644 index 5f28270..0000000 --- a/BepInEx.PluginProvider/BepInExPluginProvider.cs +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file From e1974e26fd7702c66b54c0d6879c90b988cc4920 Mon Sep 17 00:00:00 2001 From: ManlyMarco <39247311+ManlyMarco@users.noreply.github.com> Date: Tue, 24 Sep 2024 20:44:45 +0200 Subject: [PATCH 45/65] Use joelwmale/webhook-action@2.4.1 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5533275..45378b4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,7 +54,7 @@ jobs: changelog=$(git --no-pager log --no-merges --pretty="format:- (\`%h\`) [%an] %s" ${{ steps.last_successful_commit.outputs.commit_hash }}..HEAD | sed ':a;N;$!ba;s/"/\"/g' | sed ':a;N;$!ba;s/\n/\\n/g') echo "::set-output name=changelog::$changelog" - name: Notify success - uses: joelwmale/webhook-action@2.1.0 + uses: joelwmale/webhook-action@2.4.1 env: BEPINEX_DISCORD_WEBHOOK: ${{ secrets.BEPINEX_DISCORD_WEBHOOK }} if: ${{ steps.info.outputs.build_type == 'BleedingEdge' && env.BEPINEX_DISCORD_WEBHOOK != null }} From 35f6b1b541819897e2d80fc08b98e0f8e4880f33 Mon Sep 17 00:00:00 2001 From: itectori Date: Mon, 2 Dec 2024 13:48:56 -0500 Subject: [PATCH 46/65] include DeobfuscationMap.csv.gz in ComputeHash (#1020) --- .../BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs b/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs index 1ebf4c9..4212195 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs @@ -137,6 +137,8 @@ internal static partial class Il2CppInteropManager internal static string IL2CPPInteropAssemblyPath => Path.Combine(IL2CPPBasePath, "interop"); + private static string RenameMapPath => Path.Combine(Paths.BepInExRootPath, "DeobfuscationMap.csv.gz"); + private static ILoggerFactory LoggerFactory { get; } = MSLoggerFactory.Create(b => { b.AddProvider(new BepInExLoggerProvider()) @@ -173,6 +175,11 @@ internal static partial class Il2CppInteropManager HashFile(md5, file); } + if (File.Exists(RenameMapPath)) + { + HashFile(md5, RenameMapPath); + } + // Hash some common dependencies as they can affect output HashString(md5, typeof(InteropAssemblyGenerator).Assembly.GetName().Version.ToString()); HashString(md5, typeof(Cpp2IlApi).Assembly.GetName().Version.ToString()); @@ -361,11 +368,10 @@ internal static partial class Il2CppInteropManager : null, }; - var renameMapLocation = Path.Combine(Paths.BepInExRootPath, "DeobfuscationMap.csv.gz"); - if (File.Exists(renameMapLocation)) + if (File.Exists(RenameMapPath)) { Logger.LogInfo("Parsing deobfuscation rename mappings"); - opts.ReadRenameMap(renameMapLocation); + opts.ReadRenameMap(RenameMapPath); } Logger.LogInfo("Generating interop assemblies"); From 28559fe708a056abc180f5c386b8d1b0612fd062 Mon Sep 17 00:00:00 2001 From: Howard Lam Date: Sat, 11 Jan 2025 21:51:47 +0000 Subject: [PATCH 47/65] Checks if target Unity binaries archive exists before attempting download in case game's network conenction is blocked (#1024) --- .../Il2CppInteropManager.cs | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs b/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs index 4212195..0ddba45 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.Compression; +using System.Linq; using System.Net.Http; using System.Reflection; using System.Security.Cryptography; @@ -282,25 +283,29 @@ internal static partial class Il2CppInteropManager } } - private static void DownloadUnityAssemblies() - { + private static void DownloadUnityAssemblies() { var unityVersion = UnityInfo.Version; - var source = - UnityBaseLibrariesSource.Value.Replace("{VERSION}", - $"{unityVersion.Major}.{unityVersion.Minor}.{unityVersion.Build}"); + var version = $"{unityVersion.Major}.{unityVersion.Minor}.{unityVersion.Build}"; + var source = UnityBaseLibrariesSource.Value.Replace("{VERSION}", version); + if (string.IsNullOrEmpty(source)) return; - if (!string.IsNullOrEmpty(source)) - { - Logger.LogMessage("Downloading unity base libraries"); - - Directory.CreateDirectory(UnityBaseLibsDirectory); - Directory.EnumerateFiles(UnityBaseLibsDirectory, "*.dll").Do(File.Delete); + var uri = new Uri(source); + string file = Path.GetFileName(uri.AbsolutePath); + var baseFolder = Directory.CreateDirectory(UnityBaseLibsDirectory); + baseFolder.EnumerateFiles("*.dll").Do(a=>a.Delete()); + var target = baseFolder.GetFiles(file).FirstOrDefault(); + if (target != null) { + Logger.LogMessage($"Reading unity base libraries from file {source}"); + using var fStream = target.OpenRead(); + using var zipArchive = new ZipArchive(fStream, ZipArchiveMode.Read); + zipArchive.ExtractToDirectory(UnityBaseLibsDirectory); + } else { + Logger.LogMessage($"Downloading unity base libraries {source}"); using var httpClient = new HttpClient(); using var zipStream = httpClient.GetStreamAsync(source).GetAwaiter().GetResult(); - using var zipArchive = new ZipArchive(zipStream, ZipArchiveMode.Read); - Logger.LogMessage("Extracting downloaded unity base libraries"); + using var zipArchive = new ZipArchive(zipStream, ZipArchiveMode.Read); zipArchive.ExtractToDirectory(UnityBaseLibsDirectory); } } From 0c2a3f13c388852177b813c280212d0e23bb5670 Mon Sep 17 00:00:00 2001 From: ManlyMarco <39247311+ManlyMarco@users.noreply.github.com> Date: Sun, 12 Jan 2025 16:25:43 +0100 Subject: [PATCH 48/65] Switch Build.csproj TargetFramework to net8.0 (#1039) Fixes CI broken by net6.0 being deprecated. --- build/Build.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Build.csproj b/build/Build.csproj index 755ccc4..af64d1a 100644 --- a/build/Build.csproj +++ b/build/Build.csproj @@ -1,7 +1,7 @@ Exe - net6 + net8.0 $(MSBuildProjectDirectory) false From 995f04991b2b5ace4ddef07e12c3e99d4b2766a6 Mon Sep 17 00:00:00 2001 From: js6pak Date: Sun, 12 Jan 2025 17:13:13 +0100 Subject: [PATCH 49/65] Fix CI webhook --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 45378b4..ed90072 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -51,7 +51,7 @@ jobs: id: generate_changelog if: ${{ steps.info.outputs.build_type == 'BleedingEdge' }} run: | - changelog=$(git --no-pager log --no-merges --pretty="format:- (\`%h\`) [%an] %s" ${{ steps.last_successful_commit.outputs.commit_hash }}..HEAD | sed ':a;N;$!ba;s/"/\"/g' | sed ':a;N;$!ba;s/\n/\\n/g') + changelog=$(git --no-pager log --no-merges --pretty="format:- (\`%h\`) [%an] %s" ${{ steps.last_successful_commit.outputs.commit_hash }}..HEAD | sed ':a;N;$!ba;s/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g') echo "::set-output name=changelog::$changelog" - name: Notify success uses: joelwmale/webhook-action@2.4.1 From 5fef3570f212b2fb5fbe9c1d20487c13c2fa90cb Mon Sep 17 00:00:00 2001 From: ManlyMarco <39247311+ManlyMarco@users.noreply.github.com> Date: Fri, 9 May 2025 19:09:55 +0200 Subject: [PATCH 50/65] Bump Cpp2IL to pre-release.19 (#1103) --- Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj index bfc326a..71b3f11 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj @@ -22,7 +22,7 @@ - + From af0cba71fb69324aa4dfd651422bfa020309ce15 Mon Sep 17 00:00:00 2001 From: ManlyMarco <39247311+ManlyMarco@users.noreply.github.com> Date: Sat, 21 Jun 2025 02:59:18 +0200 Subject: [PATCH 51/65] v6 - Bump Il2CppInterop to 1.5.0-ci.620 (#1106) --- .../BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj | 6 +++--- .../BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs | 11 ++++------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj index 71b3f11..b22655f 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj @@ -15,10 +15,10 @@ - - + + - + diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs b/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs index 0ddba45..4f68060 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs @@ -263,17 +263,16 @@ internal static partial class Il2CppInteropManager AppDomain.CurrentDomain.AddCecilPlatformAssemblies(UnityBaseLibsDirectory); DownloadUnityAssemblies(); var asmResolverAssemblies = RunCpp2Il(); - var cecilAssemblies = new AsmToCecilConverter(asmResolverAssemblies).ConvertAll(); if (DumpDummyAssemblies.Value) { var dummyPath = Path.Combine(Paths.BepInExRootPath, "dummy"); Directory.CreateDirectory(dummyPath); - foreach (var assemblyDefinition in cecilAssemblies) - assemblyDefinition.Write(Path.Combine(dummyPath, $"{assemblyDefinition.Name.Name}.dll")); + foreach (var assemblyDefinition in asmResolverAssemblies) + assemblyDefinition.Write(Path.Combine(dummyPath, $"{assemblyDefinition.Name}.dll")); } - RunIl2CppInteropGenerator(cecilAssemblies); + RunIl2CppInteropGenerator(asmResolverAssemblies); File.WriteAllText(HashPath, ComputeHash()); } @@ -360,7 +359,7 @@ internal static partial class Il2CppInteropManager return assemblies; } - private static void RunIl2CppInteropGenerator(List sourceAssemblies) + private static void RunIl2CppInteropGenerator(List sourceAssemblies) { var opts = new GeneratorOptions { @@ -387,8 +386,6 @@ internal static partial class Il2CppInteropManager .AddLogger(logger) .AddInteropAssemblyGenerator() .Run(); - - sourceAssemblies.Do(x => x.Dispose()); } internal static void PreloadInteropAssemblies() From 88b4838fef3eac5baebd0824416246c9b0855394 Mon Sep 17 00:00:00 2001 From: ManlyMarco <39247311+ManlyMarco@users.noreply.github.com> Date: Fri, 14 Nov 2025 12:23:18 +0100 Subject: [PATCH 52/65] Fix assembly preloading overriding preloader patches (#1212) --- .../Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs b/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs index 4f68060..c2d78ef 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs @@ -400,11 +400,13 @@ internal static partial class Il2CppInteropManager Parallel.ForEach(files, file => { if (!file.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) return; - if (file.Equals("netstandard.dll", StringComparison.OrdinalIgnoreCase)) return; - if (file.Equals("Il2Cppnetstandard.dll", StringComparison.OrdinalIgnoreCase)) return; + var name = Path.GetFileNameWithoutExtension(file); + if (name.Equals("netstandard", StringComparison.OrdinalIgnoreCase)) return; + if (name.Equals("Il2Cppnetstandard", StringComparison.OrdinalIgnoreCase)) return; try { - Assembly.LoadFrom(file); + // Do not use LoadFrom since it overrides preloader patches + Assembly.Load(name); Interlocked.Increment(ref loaded); } catch (Exception e) From dd0655fe9d7473b18b0af759448adaf272878fcf Mon Sep 17 00:00:00 2001 From: ManlyMarco <39247311+ManlyMarco@users.noreply.github.com> Date: Sun, 16 Nov 2025 19:49:19 +0100 Subject: [PATCH 53/65] Cache downloaded base libraries in the `unity-libs` directory (#1213) --- .../Il2CppInteropManager.cs | 48 ++++++++++++------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs b/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs index c2d78ef..69fa85c 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs @@ -58,9 +58,10 @@ internal static partial class Il2CppInteropManager "IL2CPP", "UnityBaseLibrariesSource", "https://unity.bepinex.dev/libraries/{VERSION}.zip", new StringBuilder() - .AppendLine("URL to the ZIP of managed Unity base libraries.") - .AppendLine("The base libraries are used by Il2CppInterop to generate interop assemblies.") + .AppendLine("URL to a ZIP file with managed Unity base libraries. They are used by Il2CppInterop to generate interop assemblies.") .AppendLine("The URL can include {VERSION} template which will be replaced with the game's Unity engine version.") + .AppendLine("If a .zip file with the same filename as the URL (after template replacement) already exists in unity-libs, it will be used instead of downloading a new copy.") + .AppendLine("If you want to ensure BepInEx doesn't try to connect to the internet, set this to only the .zip filename (without a URL) and manually place the file in the unity-libs directory.") .ToString()); private static readonly ConfigEntry ConfigUnhollowerDeobfuscationRegex = ConfigFile.CoreConfig.Bind( @@ -288,25 +289,38 @@ internal static partial class Il2CppInteropManager var source = UnityBaseLibrariesSource.Value.Replace("{VERSION}", version); if (string.IsNullOrEmpty(source)) return; - var uri = new Uri(source); - string file = Path.GetFileName(uri.AbsolutePath); - var baseFolder = Directory.CreateDirectory(UnityBaseLibsDirectory); baseFolder.EnumerateFiles("*.dll").Do(a=>a.Delete()); - var target = baseFolder.GetFiles(file).FirstOrDefault(); - if (target != null) { - Logger.LogMessage($"Reading unity base libraries from file {source}"); - using var fStream = target.OpenRead(); - using var zipArchive = new ZipArchive(fStream, ZipArchiveMode.Read); - zipArchive.ExtractToDirectory(UnityBaseLibsDirectory); - } else { - Logger.LogMessage($"Downloading unity base libraries {source}"); + + var uriIsValid = Uri.TryCreate(source, UriKind.Absolute, out var uri) && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps); + var fileName = Path.GetFileName(uriIsValid ? uri.AbsolutePath : source); + var zipFilePath = Path.Combine(baseFolder.FullName, fileName); + if (!File.Exists(zipFilePath)) + { + // Check if URI is valid before attempting download + if (!uriIsValid) + throw new ArgumentException($"Unity base libraries source \"{source}\" is not a valid URL and the .zip file does not exist locally. Either provide a valid HTTP/HTTPS URL, or place the .zip file in the unity-libs directory."); + + Logger.LogMessage($"Downloading unity base libraries from {source}"); using var httpClient = new HttpClient(); - using var zipStream = httpClient.GetStreamAsync(source).GetAwaiter().GetResult(); - Logger.LogMessage("Extracting downloaded unity base libraries"); - using var zipArchive = new ZipArchive(zipStream, ZipArchiveMode.Read); - zipArchive.ExtractToDirectory(UnityBaseLibsDirectory); + using var zipStream = httpClient.GetStreamAsync(uri).GetAwaiter().GetResult(); + try + { + using var writeStream = new FileStream(zipFilePath, FileMode.Create, FileAccess.Write, FileShare.None); + zipStream.CopyTo(writeStream); + } + catch + { + // Delete the incomplete file to avoid issues on next startup + try { File.Delete(zipFilePath); } catch { } + throw; + } } + + Logger.LogMessage($"Extracting unity base libraries from {zipFilePath}"); + using var readStream = File.OpenRead(zipFilePath); + using var zipArchive = new ZipArchive(readStream, ZipArchiveMode.Read); + zipArchive.ExtractToDirectory(UnityBaseLibsDirectory); } private static List RunCpp2Il() From 0d275a4df3b6bbcb990a9715d0f037789624c377 Mon Sep 17 00:00:00 2001 From: The0x539 Date: Tue, 27 Jan 2026 08:26:39 -0600 Subject: [PATCH 54/65] Avoid accidentally (indirectly) reading PlatformHelper.Current before setting it (#1254) --- BepInEx.Preloader.Core/PlatformUtils.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/BepInEx.Preloader.Core/PlatformUtils.cs b/BepInEx.Preloader.Core/PlatformUtils.cs index a0d33c2..917a7a8 100644 --- a/BepInEx.Preloader.Core/PlatformUtils.cs +++ b/BepInEx.Preloader.Core/PlatformUtils.cs @@ -80,7 +80,12 @@ internal static class PlatformUtils if (wineGetVersion != IntPtr.Zero) { current |= Platform.Wine; - var getVersion = wineGetVersion.AsDelegate(); + // It's not safe to use the AsDelegate() extension method here because: + // - It comes from the MonoMod.Utils.DynDll class, defined in MonoMod.Common. + // - The DynDll class has a static constructor that reads PlatformHelper.Current. + // - Reading from that property freezes it: subsequent writes will throw an exception. + // - This method only sets PlatformHelper.Current at the very end. + var getVersion = Marshal.GetDelegateForFunctionPointer(wineGetVersion, typeof(GetWineVersionDelegate)) as GetWineVersionDelegate; WineVersion = getVersion(); } } From c0386134c69e9a1b9935db7264a1e1e35ebb2b88 Mon Sep 17 00:00:00 2001 From: Arrowmaster Date: Tue, 24 Feb 2026 11:05:40 -0500 Subject: [PATCH 55/65] Upgrade Doorstop to version 4.5.0 (#1260) --- README.md | 2 +- .../Unity/Doorstop/doorstop_config_il2cpp.ini | 4 - .../Unity/Doorstop/doorstop_config_mono.ini | 4 - Runtimes/Unity/Doorstop/run_bepinex_il2cpp.sh | 200 ++++++++++-------- Runtimes/Unity/Doorstop/run_bepinex_mono.sh | 199 +++++++++-------- build/Program.cs | 7 +- 6 files changed, 234 insertions(+), 182 deletions(-) diff --git a/README.md b/README.md index 18a1145..550c5e1 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ A more comprehensive comparison list of features and compatibility is available ## Used libraries -- [NeighTools/UnityDoorstop](https://github.com/NeighTools/UnityDoorstop) - v4.3.0 +- [NeighTools/UnityDoorstop](https://github.com/NeighTools/UnityDoorstop) - v4.5.0 - [BepInEx/HarmonyX](https://github.com/BepInEx/HarmonyX) - v2.10.2 - [0x0ade/MonoMod](https://github.com/0x0ade/MonoMod) - v22.7.31.1 - [jbevain/cecil](https://github.com/jbevain/cecil) - v0.10.4 diff --git a/Runtimes/Unity/Doorstop/doorstop_config_il2cpp.ini b/Runtimes/Unity/Doorstop/doorstop_config_il2cpp.ini index 8af1026..abe4c83 100644 --- a/Runtimes/Unity/Doorstop/doorstop_config_il2cpp.ini +++ b/Runtimes/Unity/Doorstop/doorstop_config_il2cpp.ini @@ -31,10 +31,6 @@ dll_search_path_override = # If true, Mono debugger server will be enabled debug_enabled = false -# When debug_enabled is true, this option specifies whether Doorstop should initialize the debugger server -# If you experience crashes when starting the debugger on debug UnityPlayer builds, try setting this to false -debug_start_server = true - # When debug_enabled is true, specifies the address to use for the debugger server debug_address = 127.0.0.1:10000 diff --git a/Runtimes/Unity/Doorstop/doorstop_config_mono.ini b/Runtimes/Unity/Doorstop/doorstop_config_mono.ini index 342d479..b9a579b 100644 --- a/Runtimes/Unity/Doorstop/doorstop_config_mono.ini +++ b/Runtimes/Unity/Doorstop/doorstop_config_mono.ini @@ -32,10 +32,6 @@ dll_search_path_override = "BepInEx\core" # If true, Mono debugger server will be enabled debug_enabled = false -# When debug_enabled is true, this option specifies whether Doorstop should initialize the debugger server -# If you experience crashes when starting the debugger on debug UnityPlayer builds, try setting this to false -debug_start_server = true - # When debug_enabled is true, specifies the address to use for the debugger server debug_address = 127.0.0.1:10000 diff --git a/Runtimes/Unity/Doorstop/run_bepinex_il2cpp.sh b/Runtimes/Unity/Doorstop/run_bepinex_il2cpp.sh index 86ef48c..27f1ecf 100644 --- a/Runtimes/Unity/Doorstop/run_bepinex_il2cpp.sh +++ b/Runtimes/Unity/Doorstop/run_bepinex_il2cpp.sh @@ -8,8 +8,6 @@ # 1. Via CLI: Run ./run_bepinex.sh [doorstop arguments] [game arguments] # 2. Via config: edit the options below and run ./run.sh without any arguments -# 0 is false, 1 is true - # LINUX: name of Unity executable # MACOS: name of the .app directory executable_name="" @@ -19,6 +17,7 @@ executable_name="" # General Config Options # Enable Doorstop? +# 0 is false, 1 is true enabled="1" # Path to the assembly to load and execute @@ -39,15 +38,12 @@ ignore_disable_switch="0" # (e.g. mscorlib is stripped in original game) # This option causes Mono to seek mscorlib and core libraries from a different folder before Managed # Original Managed folder is added as a secondary folder in the search path +# To specify multiple paths, separate them with colons (:) dll_search_path_override="" # If 1, Mono debugger server will be enabled debug_enable="0" -# When debug_enabled is 1, this option specifies whether Doorstop should initialize the debugger server -# If you experience crashes when starting the debugger on debug UnityPlayer builds, try setting this to 0 -debug_start_server="0" - # When debug_enabled is 1, specifies the address to use for the debugger server debug_address="127.0.0.1:10000" @@ -64,43 +60,43 @@ corlib_dir="dotnet" ################################################################################ # Everything past this point is the actual script +set -e -# Special case: program is launched via Steam -# In that case rerun the script via their bootstrapper to ensure Steam overlay works -if [ "$2" = "SteamLaunch" ]; then - # Conceptually: exec "$1" "$2" "$3" "$4" "$0" "rest of $@" - # But newer versions of Steam interleave the $1..$4 with some "--" arguments, so preserve them as well - # Bash has array subscripting, but POSIX sh doesn't, so avoid it - to_rotate=4 - rotated=0 - while [ $((to_rotate-=1)) -ge 0 ]; do - while [ "z$1" = "z--" ]; do - set -- "$@" "$1" - shift - rotated=$((rotated+1)) +# Special case: program is launched via Steam on Linux +# In that case rerun the script via their bootstrapper to delay adding Doorstop to LD_PRELOAD +# This is required until https://github.com/NeighTools/UnityDoorstop/issues/88 is resolved +for a in "$@"; do + if [ "$a" = "SteamLaunch" ]; then + rotated=0; max=$# + while [ $rotated -lt $max ]; do + # Test if argument is prefixed with the value of $PWD + if [ "$1" != "${1#"${PWD%/}/"}" ]; then + to_rotate=$(($# - rotated)) + set -- "$@" "$0" + while [ $((to_rotate-=1)) -ge 0 ]; do + set -- "$@" "$1" + shift + done + exec "$@" + else + set -- "$@" "$1" + shift + rotated=$((rotated+1)) + fi done - set -- "$@" "$1" - shift - rotated=$((rotated+1)) - done - to_rotate=$(($# - rotated)) - set -- "$@" "$0" - while [ $((to_rotate-=1)) -ge 0 ]; do - set -- "$@" "$1" - shift - done - exec "$@" -fi + echo "Could not determine game executable launched by Steam" 1>&2 + exit 1 + fi +done # Handle first param being executable name if [ -x "$1" ] ; then executable_name="$1" - echo "Target executable: $1" shift fi if [ -z "${executable_name}" ] || [ ! -x "${executable_name}" ]; then - echo "Please set executable_name to a valid name in a text editor or as the first command line parameter" + echo "Please set executable_name to a valid name in a text editor or as the first command line parameter" 1>&2 exit 1 fi @@ -111,50 +107,56 @@ arch="" executable_path="" lib_extension="" -# Set executable path and the extension to use for the libdoorstop shared object +abs_path() { + # Resolve relative path to absolute from BASEDIR + if [ "$1" = "${1#/}" ]; then + set -- "${BASEDIR}/${1}" + fi + echo "$(cd "$(dirname "$1")" && pwd)/$(basename "$1")" +} + +# Set executable path and the extension to use for the libdoorstop shared object as well as check whether we're running on Apple Silicon os_type="$(uname -s)" case ${os_type} in Linux*) - executable_path="${executable_name}" - # Handle relative paths - if ! echo "$executable_path" | grep "^/.*$"; then - executable_path="${BASEDIR}/${executable_path}" - fi + executable_path="$(abs_path "$executable_name")" lib_extension="so" ;; Darwin*) - real_executable_name="${executable_name}" - - # Handle relative directories - if ! echo "$real_executable_name" | grep "^/.*$"; then - real_executable_name="${BASEDIR}/${real_executable_name}" - fi + real_executable_name="$(abs_path "$executable_name")" # If we're not even an actual executable, check .app Info for actual executable - if ! echo "$real_executable_name" | grep "^.*\.app/Contents/MacOS/.*"; then - # Add .app to the end if not given - if ! echo "$real_executable_name" | grep "^.*\.app$"; then - real_executable_name="${real_executable_name}.app" - fi - inner_executable_name=$(defaults read "${real_executable_name}/Contents/Info" CFBundleExecutable) - executable_path="${real_executable_name}/Contents/MacOS/${inner_executable_name}" - else - executable_path="${executable_name}" - fi + case $real_executable_name in + *.app/Contents/MacOS/*) + executable_path="${executable_name}" + ;; + *) + # Add .app to the end if not given + if [ "$real_executable_name" = "${real_executable_name%.app}" ]; then + real_executable_name="${real_executable_name}.app" + fi + inner_executable_name=$(defaults read "${real_executable_name}/Contents/Info" CFBundleExecutable) + executable_path="${real_executable_name}/Contents/MacOS/${inner_executable_name}" + ;; + esac lib_extension="dylib" + + # CPUs for Apple Silicon are in the format "Apple M.." + cpu_type="$(sysctl -n machdep.cpu.brand_string)" + case "${cpu_type}" in + Apple*) + is_apple_silicon=1 + ;; + esac ;; *) # alright whos running games on freebsd - echo "Unknown operating system ($(uname -s))" - echo "Make an issue at https://github.com/NeighTools/UnityDoorstop" + echo "Unknown operating system ($(uname -s))" 1>&2 + echo "Make an issue at https://github.com/NeighTools/UnityDoorstop" 1>&2 exit 1 ;; esac -abs_path() { - echo "$(cd "$(dirname "$1")" && pwd)/$(basename "$1")" -} - _readlink() { # relative links with readlink (without -f) do not preserve the path info ab_path="$(abs_path "$1")" @@ -166,7 +168,6 @@ _readlink() { echo "$link" } - resolve_executable_path () { e_path="$(abs_path "$1")" @@ -176,19 +177,23 @@ resolve_executable_path () { echo "${e_path}" } -# Get absolute path of executable and show to user +# Get absolute path of executable executable_path=$(resolve_executable_path "${executable_path}") -echo "${executable_path}" # Figure out the arch of the executable with file file_out="$(LD_PRELOAD="" file -b "${executable_path}")" case "${file_out}" in *PE32*) - echo "The executable is a Windows executable file. You must use Wine/Proton and BepInEx for Windows with this executable." - echo "Uninstall BepInEx for *nix and install BepInEx for Windows instead." - echo "More info: https://docs.bepinex.dev/articles/advanced/steam_interop.html#protonwine" + echo "The executable is a Windows executable file. You must use Wine/Proton and BepInEx for Windows with this executable." 1>&2 + echo "Uninstall BepInEx for *nix and install BepInEx for Windows instead." 1>&2 + echo "More info: https://docs.bepinex.dev/articles/advanced/steam_interop.html#protonwine" 1>&2 exit 1 ;; + *shell\ script*) + # Fallback for games that launch a shell script from Steam + # default to x64, change as needed + arch="x64" + ;; *64-bit*) arch="x64" ;; @@ -196,10 +201,10 @@ case "${file_out}" in arch="x86" ;; *) - echo "The executable \"${executable_path}\" is not compiled for x86 or x64 (might be ARM?)" - echo "If you think this is a mistake (or would like to encourage support for other architectures)" - echo "Please make an issue at https://github.com/NeighTools/UnityDoorstop" - echo "Got: ${file_out}" + echo "The executable \"${executable_path}\" is not compiled for x86 or x64 (might be ARM?)" 1>&2 + echo "If you think this is a mistake (or would like to encourage support for other architectures)" 1>&2 + echo "Please make an issue at https://github.com/NeighTools/UnityDoorstop" 1>&2 + echo "Got: ${file_out}" 1>&2 exit 1 ;; esac @@ -217,65 +222,81 @@ doorstop_bool() { } # Read from command line -while :; do +i=0; max=$# +while [ $i -lt $max ]; do case "$1" in - --doorstop_enabled) + --doorstop_enabled) # For backwards compatibility. Renamed to --doorstop-enabled enabled="$(doorstop_bool "$2")" shift + i=$((i+1)) ;; - --doorstop_target_assembly) + --doorstop_target_assembly) # For backwards compatibility. Renamed to --doorstop-target-assembly target_assembly="$2" shift + i=$((i+1)) + ;; + --doorstop-enabled) + enabled="$(doorstop_bool "$2")" + shift + i=$((i+1)) + ;; + --doorstop-target-assembly) + target_assembly="$2" + shift + i=$((i+1)) ;; --doorstop-boot-config-override) boot_config_override="$2" shift + i=$((i+1)) ;; --doorstop-mono-dll-search-path-override) dll_search_path_override="$2" shift + i=$((i+1)) ;; --doorstop-mono-debug-enabled) debug_enable="$(doorstop_bool "$2")" shift - ;; - --doorstop-mono-debug-start-server) - debug_start_server="$(doorstop_bool "$2")" - shift + i=$((i+1)) ;; --doorstop-mono-debug-suspend) debug_suspend="$(doorstop_bool "$2")" shift + i=$((i+1)) ;; --doorstop-mono-debug-address) debug_address="$2" shift + i=$((i+1)) ;; --doorstop-clr-runtime-coreclr-path) coreclr_path="$2" shift + i=$((i+1)) ;; --doorstop-clr-corlib-dir) corlib_dir="$2" shift + i=$((i+1)) ;; *) - if [ -z "$1" ]; then - break - fi - rest_args="$rest_args $1" + set -- "$@" "$1" ;; esac shift + i=$((i+1)) done +target_assembly="$(abs_path "$target_assembly")" + # Move variables to environment export DOORSTOP_ENABLED="$enabled" export DOORSTOP_TARGET_ASSEMBLY="$target_assembly" +export DOORSTOP_BOOT_CONFIG_OVERRIDE="$boot_config_override" export DOORSTOP_IGNORE_DISABLED_ENV="$ignore_disable_switch" export DOORSTOP_MONO_DLL_SEARCH_PATH_OVERRIDE="$dll_search_path_override" export DOORSTOP_MONO_DEBUG_ENABLED="$debug_enable" -export DOORSTOP_MONO_DEBUG_START_SERVER="$debug_start_server" export DOORSTOP_MONO_DEBUG_ADDRESS="$debug_address" export DOORSTOP_MONO_DEBUG_SUSPEND="$debug_suspend" export DOORSTOP_CLR_RUNTIME_CORECLR_PATH="$coreclr_path.$lib_extension" @@ -299,5 +320,14 @@ else export DYLD_INSERT_LIBRARIES="${doorstop_name}:${DYLD_INSERT_LIBRARIES}" fi -# shellcheck disable=SC2086 -exec "$executable_path" $rest_args +if [ -n "${is_apple_silicon}" ]; then + export ARCHPREFERENCE="arm64,x86_64" + + # We need to use arch for Apple Silicon to allow the executable to be run natively as otherwise if + # the executable is universal, supporting both x86_64 and arm64, MacOs will still run it as x86_64 + # if the parent process is running as x86. + # arch also strips the DYLD_INSERT_LIBRARIES env var so we have to pass that in manually + exec arch -e DYLD_INSERT_LIBRARIES="${DYLD_INSERT_LIBRARIES}" "$executable_path" "$@" +else + exec "$executable_path" "$@" +fi diff --git a/Runtimes/Unity/Doorstop/run_bepinex_mono.sh b/Runtimes/Unity/Doorstop/run_bepinex_mono.sh index 2f743c1..893aeea 100644 --- a/Runtimes/Unity/Doorstop/run_bepinex_mono.sh +++ b/Runtimes/Unity/Doorstop/run_bepinex_mono.sh @@ -8,8 +8,6 @@ # 1. Via CLI: Run ./run_bepinex.sh [doorstop arguments] [game arguments] # 2. Via config: edit the options below and run ./run.sh without any arguments -# 0 is false, 1 is true - # LINUX: name of Unity executable # MACOS: name of the .app directory executable_name="" @@ -19,6 +17,7 @@ executable_name="" # General Config Options # Enable Doorstop? +# 0 is false, 1 is true enabled="1" # Path to the assembly to load and execute @@ -45,10 +44,6 @@ dll_search_path_override="BepInEx/core" # If 1, Mono debugger server will be enabled debug_enable="0" -# When debug_enabled is 1, this option specifies whether Doorstop should initialize the debugger server -# If you experience crashes when starting the debugger on debug UnityPlayer builds, try setting this to 0 -debug_start_server="0" - # When debug_enabled is 1, specifies the address to use for the debugger server debug_address="127.0.0.1:10000" @@ -65,43 +60,43 @@ corlib_dir="" ################################################################################ # Everything past this point is the actual script +set -e -# Special case: program is launched via Steam -# In that case rerun the script via their bootstrapper to ensure Steam overlay works -if [ "$2" = "SteamLaunch" ]; then - # Conceptually: exec "$1" "$2" "$3" "$4" "$0" "rest of $@" - # But newer versions of Steam interleave the $1..$4 with some "--" arguments, so preserve them as well - # Bash has array subscripting, but POSIX sh doesn't, so avoid it - to_rotate=4 - rotated=0 - while [ $((to_rotate-=1)) -ge 0 ]; do - while [ "z$1" = "z--" ]; do - set -- "$@" "$1" - shift - rotated=$((rotated+1)) +# Special case: program is launched via Steam on Linux +# In that case rerun the script via their bootstrapper to delay adding Doorstop to LD_PRELOAD +# This is required until https://github.com/NeighTools/UnityDoorstop/issues/88 is resolved +for a in "$@"; do + if [ "$a" = "SteamLaunch" ]; then + rotated=0; max=$# + while [ $rotated -lt $max ]; do + # Test if argument is prefixed with the value of $PWD + if [ "$1" != "${1#"${PWD%/}/"}" ]; then + to_rotate=$(($# - rotated)) + set -- "$@" "$0" + while [ $((to_rotate-=1)) -ge 0 ]; do + set -- "$@" "$1" + shift + done + exec "$@" + else + set -- "$@" "$1" + shift + rotated=$((rotated+1)) + fi done - set -- "$@" "$1" - shift - rotated=$((rotated+1)) - done - to_rotate=$(($# - rotated)) - set -- "$@" "$0" - while [ $((to_rotate-=1)) -ge 0 ]; do - set -- "$@" "$1" - shift - done - exec "$@" -fi + echo "Could not determine game executable launched by Steam" 1>&2 + exit 1 + fi +done # Handle first param being executable name if [ -x "$1" ] ; then executable_name="$1" - echo "Target executable: $1" shift fi if [ -z "${executable_name}" ] || [ ! -x "${executable_name}" ]; then - echo "Please set executable_name to a valid name in a text editor or as the first command line parameter" + echo "Please set executable_name to a valid name in a text editor or as the first command line parameter" 1>&2 exit 1 fi @@ -112,50 +107,56 @@ arch="" executable_path="" lib_extension="" -# Set executable path and the extension to use for the libdoorstop shared object +abs_path() { + # Resolve relative path to absolute from BASEDIR + if [ "$1" = "${1#/}" ]; then + set -- "${BASEDIR}/${1}" + fi + echo "$(cd "$(dirname "$1")" && pwd)/$(basename "$1")" +} + +# Set executable path and the extension to use for the libdoorstop shared object as well as check whether we're running on Apple Silicon os_type="$(uname -s)" case ${os_type} in Linux*) - executable_path="${executable_name}" - # Handle relative paths - if ! echo "$executable_path" | grep "^/.*$"; then - executable_path="${BASEDIR}/${executable_path}" - fi + executable_path="$(abs_path "$executable_name")" lib_extension="so" ;; Darwin*) - real_executable_name="${executable_name}" - - # Handle relative directories - if ! echo "$real_executable_name" | grep "^/.*$"; then - real_executable_name="${BASEDIR}/${real_executable_name}" - fi + real_executable_name="$(abs_path "$executable_name")" # If we're not even an actual executable, check .app Info for actual executable - if ! echo "$real_executable_name" | grep "^.*\.app/Contents/MacOS/.*"; then - # Add .app to the end if not given - if ! echo "$real_executable_name" | grep "^.*\.app$"; then - real_executable_name="${real_executable_name}.app" - fi - inner_executable_name=$(defaults read "${real_executable_name}/Contents/Info" CFBundleExecutable) - executable_path="${real_executable_name}/Contents/MacOS/${inner_executable_name}" - else - executable_path="${executable_name}" - fi + case $real_executable_name in + *.app/Contents/MacOS/*) + executable_path="${executable_name}" + ;; + *) + # Add .app to the end if not given + if [ "$real_executable_name" = "${real_executable_name%.app}" ]; then + real_executable_name="${real_executable_name}.app" + fi + inner_executable_name=$(defaults read "${real_executable_name}/Contents/Info" CFBundleExecutable) + executable_path="${real_executable_name}/Contents/MacOS/${inner_executable_name}" + ;; + esac lib_extension="dylib" + + # CPUs for Apple Silicon are in the format "Apple M.." + cpu_type="$(sysctl -n machdep.cpu.brand_string)" + case "${cpu_type}" in + Apple*) + is_apple_silicon=1 + ;; + esac ;; *) # alright whos running games on freebsd - echo "Unknown operating system ($(uname -s))" - echo "Make an issue at https://github.com/NeighTools/UnityDoorstop" + echo "Unknown operating system ($(uname -s))" 1>&2 + echo "Make an issue at https://github.com/NeighTools/UnityDoorstop" 1>&2 exit 1 ;; esac -abs_path() { - echo "$(cd "$(dirname "$1")" && pwd)/$(basename "$1")" -} - _readlink() { # relative links with readlink (without -f) do not preserve the path info ab_path="$(abs_path "$1")" @@ -167,7 +168,6 @@ _readlink() { echo "$link" } - resolve_executable_path () { e_path="$(abs_path "$1")" @@ -177,19 +177,23 @@ resolve_executable_path () { echo "${e_path}" } -# Get absolute path of executable and show to user +# Get absolute path of executable executable_path=$(resolve_executable_path "${executable_path}") -echo "${executable_path}" # Figure out the arch of the executable with file file_out="$(LD_PRELOAD="" file -b "${executable_path}")" case "${file_out}" in *PE32*) - echo "The executable is a Windows executable file. You must use Wine/Proton and BepInEx for Windows with this executable." - echo "Uninstall BepInEx for *nix and install BepInEx for Windows instead." - echo "More info: https://docs.bepinex.dev/articles/advanced/steam_interop.html#protonwine" + echo "The executable is a Windows executable file. You must use Wine/Proton and BepInEx for Windows with this executable." 1>&2 + echo "Uninstall BepInEx for *nix and install BepInEx for Windows instead." 1>&2 + echo "More info: https://docs.bepinex.dev/articles/advanced/steam_interop.html#protonwine" 1>&2 exit 1 ;; + *shell\ script*) + # Fallback for games that launch a shell script from Steam + # default to x64, change as needed + arch="x64" + ;; *64-bit*) arch="x64" ;; @@ -197,10 +201,10 @@ case "${file_out}" in arch="x86" ;; *) - echo "The executable \"${executable_path}\" is not compiled for x86 or x64 (might be ARM?)" - echo "If you think this is a mistake (or would like to encourage support for other architectures)" - echo "Please make an issue at https://github.com/NeighTools/UnityDoorstop" - echo "Got: ${file_out}" + echo "The executable \"${executable_path}\" is not compiled for x86 or x64 (might be ARM?)" 1>&2 + echo "If you think this is a mistake (or would like to encourage support for other architectures)" 1>&2 + echo "Please make an issue at https://github.com/NeighTools/UnityDoorstop" 1>&2 + echo "Got: ${file_out}" 1>&2 exit 1 ;; esac @@ -218,65 +222,81 @@ doorstop_bool() { } # Read from command line -while :; do +i=0; max=$# +while [ $i -lt $max ]; do case "$1" in - --doorstop_enabled) + --doorstop_enabled) # For backwards compatibility. Renamed to --doorstop-enabled enabled="$(doorstop_bool "$2")" shift + i=$((i+1)) ;; - --doorstop_target_assembly) + --doorstop_target_assembly) # For backwards compatibility. Renamed to --doorstop-target-assembly target_assembly="$2" shift + i=$((i+1)) + ;; + --doorstop-enabled) + enabled="$(doorstop_bool "$2")" + shift + i=$((i+1)) + ;; + --doorstop-target-assembly) + target_assembly="$2" + shift + i=$((i+1)) ;; --doorstop-boot-config-override) boot_config_override="$2" shift + i=$((i+1)) ;; --doorstop-mono-dll-search-path-override) dll_search_path_override="$2" shift + i=$((i+1)) ;; --doorstop-mono-debug-enabled) debug_enable="$(doorstop_bool "$2")" shift - ;; - --doorstop-mono-debug-start-server) - debug_start_server="$(doorstop_bool "$2")" - shift + i=$((i+1)) ;; --doorstop-mono-debug-suspend) debug_suspend="$(doorstop_bool "$2")" shift + i=$((i+1)) ;; --doorstop-mono-debug-address) debug_address="$2" shift + i=$((i+1)) ;; --doorstop-clr-runtime-coreclr-path) coreclr_path="$2" shift + i=$((i+1)) ;; --doorstop-clr-corlib-dir) corlib_dir="$2" shift + i=$((i+1)) ;; *) - if [ -z "$1" ]; then - break - fi - rest_args="$rest_args $1" + set -- "$@" "$1" ;; esac shift + i=$((i+1)) done +target_assembly="$(abs_path "$target_assembly")" + # Move variables to environment export DOORSTOP_ENABLED="$enabled" export DOORSTOP_TARGET_ASSEMBLY="$target_assembly" +export DOORSTOP_BOOT_CONFIG_OVERRIDE="$boot_config_override" export DOORSTOP_IGNORE_DISABLED_ENV="$ignore_disable_switch" export DOORSTOP_MONO_DLL_SEARCH_PATH_OVERRIDE="$dll_search_path_override" export DOORSTOP_MONO_DEBUG_ENABLED="$debug_enable" -export DOORSTOP_MONO_DEBUG_START_SERVER="$debug_start_server" export DOORSTOP_MONO_DEBUG_ADDRESS="$debug_address" export DOORSTOP_MONO_DEBUG_SUSPEND="$debug_suspend" export DOORSTOP_CLR_RUNTIME_CORECLR_PATH="$coreclr_path.$lib_extension" @@ -300,5 +320,14 @@ else export DYLD_INSERT_LIBRARIES="${doorstop_name}:${DYLD_INSERT_LIBRARIES}" fi -# shellcheck disable=SC2086 -exec "$executable_path" $rest_args +if [ -n "${is_apple_silicon}" ]; then + export ARCHPREFERENCE="arm64,x86_64" + + # We need to use arch for Apple Silicon to allow the executable to be run natively as otherwise if + # the executable is universal, supporting both x86_64 and arm64, MacOs will still run it as x86_64 + # if the parent process is running as x86. + # arch also strips the DYLD_INSERT_LIBRARIES env var so we have to pass that in manually + exec arch -e DYLD_INSERT_LIBRARIES="${DYLD_INSERT_LIBRARIES}" "$executable_path" "$@" +else + exec "$executable_path" "$@" +fi diff --git a/build/Program.cs b/build/Program.cs index 0ba5f0a..ee498b1 100644 --- a/build/Program.cs +++ b/build/Program.cs @@ -32,7 +32,7 @@ public class BuildContext : FrostingContext BleedingEdge } - public const string DoorstopVersion = "4.3.0"; + public const string DoorstopVersion = "4.5.0"; public const string DotnetRuntimeVersion = "6.0.7"; public const string DobbyVersion = "1.0.5"; @@ -253,8 +253,9 @@ public sealed class MakeDistTask : FrostingTask if (dist.Engine == "Unity") { - var doorstopPath = - ctx.CacheDirectory.Combine("doorstop").Combine($"doorstop_{dist.Os}").Combine(dist.Arch); + var doorstopPath = dist.Os == "macos" + ? ctx.CacheDirectory.Combine("doorstop").Combine("doorstop_macos").Combine("universal") + : ctx.CacheDirectory.Combine("doorstop").Combine($"doorstop_{dist.Os}").Combine(dist.Arch); foreach (var filePath in ctx.GetFiles(doorstopPath.Combine($"*.{dist.DllExtension}").FullPath)) ctx.CopyFileToDirectory(filePath, targetDir); ctx.CopyFileToDirectory(doorstopPath.CombineWithFilePath(".doorstop_version"), targetDir); From 3fab71a1914132a1ce3a545caf3192da603f2258 Mon Sep 17 00:00:00 2001 From: tobey Date: Sat, 7 Mar 2026 13:10:33 +0000 Subject: [PATCH 56/65] Improved support for IL2CPP metadata v23-106 (Unity 6+) (#1284) chore(deps): update Cpp2IL and Il2CppInterop --- .../BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj index b22655f..9c3ca31 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj @@ -15,14 +15,14 @@ - - + + - + - + From f8ba0a41afb8eb6a790f1ce2583c00ce421f1a66 Mon Sep 17 00:00:00 2001 From: HetCreep Date: Mon, 8 Jun 2026 19:36:17 +0700 Subject: [PATCH 57/65] Give actionable error when base-libs download fails (#1332) When the Unity base-libraries download fails (commonly a 404 for a Unity version not yet hosted on unity.bepinex.dev), the uncaught HttpRequestException surfaced only as a cryptic "Failed to generate Il2Cpp interop assemblies" and the game would not launch -- even though a local fallback (the unity-libs directory) already exists. Catch the download failure and throw a clear message naming the Unity version, the URL and the two workarounds: place a matching base-libraries .zip in the unity-libs directory, or point the [IL2CPP] UnityBaseLibrariesSource config at a valid URL. Refs #1295, #1084, #986, #941, #515 --- .../Il2CppInteropManager.cs | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs b/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs index 69fa85c..fe9de89 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs @@ -303,17 +303,34 @@ internal static partial class Il2CppInteropManager Logger.LogMessage($"Downloading unity base libraries from {source}"); using var httpClient = new HttpClient(); - using var zipStream = httpClient.GetStreamAsync(uri).GetAwaiter().GetResult(); + + Stream zipStream; try { - using var writeStream = new FileStream(zipFilePath, FileMode.Create, FileAccess.Write, FileShare.None); - zipStream.CopyTo(writeStream); + zipStream = httpClient.GetStreamAsync(uri).GetAwaiter().GetResult(); } - catch + catch (Exception e) { - // Delete the incomplete file to avoid issues on next startup - try { File.Delete(zipFilePath); } catch { } - throw; + throw new IOException( + $"Failed to download Unity base libraries for Unity {unityVersion} from {source}: {e.Message}. " + + $"Base libraries for this Unity version may not be available on the server yet. Download a matching " + + $"base-libraries .zip and place it in the \"{UnityBaseLibsDirectory}\" (unity-libs) directory, or set " + + $"the [IL2CPP] UnityBaseLibrariesSource config option to a valid URL.", e); + } + + using (zipStream) + { + try + { + using var writeStream = new FileStream(zipFilePath, FileMode.Create, FileAccess.Write, FileShare.None); + zipStream.CopyTo(writeStream); + } + catch + { + // Delete the incomplete file to avoid issues on next startup + try { File.Delete(zipFilePath); } catch { } + throw; + } } } From 77d438cf696397afc29ed15a3601ead7e971617b Mon Sep 17 00:00:00 2001 From: HetCreep Date: Mon, 8 Jun 2026 19:48:55 +0700 Subject: [PATCH 58/65] Clarify BepInDependency version errors and docs (#1330) The chainloader collected both not-installed dependencies and installed-but-incompatible-version dependencies into one list and reported them all as "missing dependencies", which is confusing when the plugin is present but on a version outside the requested range. Separate the two cases and log distinct, actionable messages: one for dependencies that are not installed, and one for dependencies present with an incompatible version (showing the found version and the required range). Also expand the BepInDependency(string, string) xmldoc: the version is a SemVer range, link the range syntax, and warn about the BepInEx 5 -> 6 behaviour change (a bare version was a minimum, now it is exact). Fixes #779 --- BepInEx.Core/Bootstrap/BaseChainloader.cs | 38 +++++++++++++++++------ BepInEx.Core/Contract/Attributes.cs | 14 +++++++-- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/BepInEx.Core/Bootstrap/BaseChainloader.cs b/BepInEx.Core/Bootstrap/BaseChainloader.cs index c6fe182..bec5b6b 100644 --- a/BepInEx.Core/Bootstrap/BaseChainloader.cs +++ b/BepInEx.Core/Bootstrap/BaseChainloader.cs @@ -344,6 +344,7 @@ public abstract class BaseChainloader { var dependsOnInvalidPlugin = false; var missingDependencies = new List(); + var incompatibleDependencies = new List>(); foreach (var dependency in plugin.Dependencies) { static bool IsHardDependency(BepInDependency dep) => @@ -359,15 +360,22 @@ public abstract class BaseChainloader pluginVersion = pluginInfo?.Metadata.Version; } - if (!dependencyExists || dependency.VersionRange != null && - !dependency.VersionRange.IsSatisfied(pluginVersion)) + // The dependency is not installed at all. + if (!dependencyExists) { - // If the dependency is hard, collect it into a list to show if (IsHardDependency(dependency)) missingDependencies.Add(dependency); continue; } + // The dependency is installed, but its version does not satisfy the requested range. + if (dependency.VersionRange != null && !dependency.VersionRange.IsSatisfied(pluginVersion)) + { + if (IsHardDependency(dependency)) + incompatibleDependencies.Add(new KeyValuePair(dependency, pluginVersion?.ToString())); + continue; + } + // If the dependency is a hard and is invalid (e.g. has missing dependencies), report that to the user if (invalidPlugins.Contains(dependency.DependencyGUID) && IsHardDependency(dependency)) { @@ -387,13 +395,25 @@ public abstract class BaseChainloader continue; } - if (missingDependencies.Count != 0) + if (missingDependencies.Count != 0 || incompatibleDependencies.Count != 0) { - var message = $@"Could not load [{plugin}] because it has missing dependencies: { - string.Join(", ", missingDependencies.Select(s => s.VersionRange == null ? s.DependencyGUID : $"{s.DependencyGUID} ({s.VersionRange})").ToArray()) - }"; - DependencyErrors.Add(message); - Logger.Log(LogLevel.Error, message); + if (missingDependencies.Count != 0) + { + var message = $@"Could not load [{plugin}] because it has missing dependencies: { + string.Join(", ", missingDependencies.Select(s => s.VersionRange == null ? s.DependencyGUID : $"{s.DependencyGUID} ({s.VersionRange})").ToArray()) + }. Install the listed plugin(s) and restart the game."; + DependencyErrors.Add(message); + Logger.Log(LogLevel.Error, message); + } + + if (incompatibleDependencies.Count != 0) + { + var message = $@"Could not load [{plugin}] because the following dependencies are installed with an incompatible version: { + string.Join(", ", incompatibleDependencies.Select(s => $"{s.Key.DependencyGUID} (found {s.Value}, requires {s.Key.VersionRange})").ToArray()) + }. Update the listed plugin(s) to a version that satisfies the requirement."; + DependencyErrors.Add(message); + Logger.Log(LogLevel.Error, message); + } invalidPlugins.Add(plugin.Metadata.GUID); continue; diff --git a/BepInEx.Core/Contract/Attributes.cs b/BepInEx.Core/Contract/Attributes.cs index 0de9e93..87ae233 100644 --- a/BepInEx.Core/Contract/Attributes.cs +++ b/BepInEx.Core/Contract/Attributes.cs @@ -120,8 +120,18 @@ public class BepInDependency : Attribute, ICacheable /// not load and an error will be logged instead. /// /// The GUID of the referenced plugin. - /// The version range of the referenced plugin. - /// When version is supplied the dependency is always treated as HardDependency + /// + /// The version requirement of the referenced plugin, parsed as a SemVer range + /// (see ). A plain version such as + /// 1.2.0 requires that exact version; use a range such as >=1.2.0, 1.2.*, + /// ~1.2.0 or ^1.2.0 to accept more than one version. + /// + /// + /// When a version is supplied the dependency is always treated as a hard dependency. + /// Plugins migrating from BepInEx 5 should note a behaviour change: a bare version was previously + /// treated as a minimum (>=), whereas in BepInEx 6 it is an exact match. Use + /// >=1.2.0 to keep the old behaviour. + /// public BepInDependency(string guid, string version) : this(guid) { VersionRange = Range.Parse(version); From f568f0e7b50ed1e7012631eeec836f9b3684f46e Mon Sep 17 00:00:00 2001 From: HetCreep Date: Mon, 8 Jun 2026 19:49:39 +0700 Subject: [PATCH 59/65] Fix misleading error label for Finished handlers (#1329) BaseChainloader.Execute invoked the Finished event inside the same try/catch that reports plugin-loading failures, so an exception thrown by a Finished event listener was logged as "Error occurred loading plugins:" even though it was unrelated to plugin loading. Move Finished?.Invoke() into its own try/catch with an accurate message. Control flow is otherwise unchanged: Finished still runs only after LoadPlugins succeeds, a throwing listener is still caught, and "Chainloader startup complete" is still logged afterwards. Fixes #842 --- BepInEx.Core/Bootstrap/BaseChainloader.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/BepInEx.Core/Bootstrap/BaseChainloader.cs b/BepInEx.Core/Bootstrap/BaseChainloader.cs index bec5b6b..0776bae 100644 --- a/BepInEx.Core/Bootstrap/BaseChainloader.cs +++ b/BepInEx.Core/Bootstrap/BaseChainloader.cs @@ -315,7 +315,15 @@ public abstract class BaseChainloader var plugins = DiscoverPlugins(); Logger.Log(LogLevel.Info, $"{plugins.Count} plugin{(plugins.Count == 1 ? "" : "s")} to load"); LoadPlugins(plugins); - Finished?.Invoke(); + + try + { + Finished?.Invoke(); + } + catch (Exception ex) + { + Logger.Log(LogLevel.Error, $"Error occurred in a Chainloader.Finished event handler: {ex}"); + } } catch (Exception ex) { From 9aedb90fc62533df5f42c1b6c5caa8e7bbd5523e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AF=E3=82=84=E3=81=97=E3=81=86=E3=82=81?= <122680126+HayashiUme@users.noreply.github.com> Date: Tue, 9 Jun 2026 23:07:44 +0800 Subject: [PATCH 60/65] Set the console codepage to ensure correct text display regardless of encoding (#1337) --- .../Console/Windows/ConsoleEncoding/ConsoleEncoding.cs | 8 ++++---- BepInEx.Core/Console/Windows/WindowsConsoleDriver.cs | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/BepInEx.Core/Console/Windows/ConsoleEncoding/ConsoleEncoding.cs b/BepInEx.Core/Console/Windows/ConsoleEncoding/ConsoleEncoding.cs index b70f1e1..b9d7c1c 100644 --- a/BepInEx.Core/Console/Windows/ConsoleEncoding/ConsoleEncoding.cs +++ b/BepInEx.Core/Console/Windows/ConsoleEncoding/ConsoleEncoding.cs @@ -41,7 +41,7 @@ internal partial class ConsoleEncoding : Encoding public override int GetByteCount(char[] chars, int index, int count) { WriteCharBuffer(chars, index, count); - var result = WideCharToMultiByte(_codePage, 0, chars, count, _zeroByte, 0, IntPtr.Zero, IntPtr.Zero); + var result = WideCharToMultiByte(_codePage, 0, _charBuffer, count, _zeroByte, 0, IntPtr.Zero, IntPtr.Zero); return result; } @@ -50,7 +50,7 @@ internal partial class ConsoleEncoding : Encoding var byteCount = GetByteCount(chars, charIndex, charCount); WriteCharBuffer(chars, charIndex, charCount); ExpandByteBuffer(byteCount); - _ = WideCharToMultiByte(_codePage, 0, chars, charCount, _byteBuffer, byteCount, IntPtr.Zero, + _ = WideCharToMultiByte(_codePage, 0, _charBuffer, charCount, _byteBuffer, byteCount, IntPtr.Zero, IntPtr.Zero); var readCount = Math.Min(bytes.Length, byteCount); ReadByteBuffer(bytes, byteIndex, readCount); @@ -60,7 +60,7 @@ internal partial class ConsoleEncoding : Encoding public override int GetCharCount(byte[] bytes, int index, int count) { WriteByteBuffer(bytes, index, count); - var result = MultiByteToWideChar(_codePage, 0, bytes, count, _zeroChar, 0); + var result = MultiByteToWideChar(_codePage, 0, _byteBuffer, count, _zeroChar, 0); return result; } @@ -69,7 +69,7 @@ internal partial class ConsoleEncoding : Encoding var charCount = GetCharCount(bytes, byteIndex, byteCount); WriteByteBuffer(bytes, byteIndex, byteCount); ExpandCharBuffer(charCount); - _ = MultiByteToWideChar(_codePage, 0, bytes, byteCount, _charBuffer, charCount); + _ = MultiByteToWideChar(_codePage, 0, _byteBuffer, byteCount, _charBuffer, charCount); var readCount = Math.Min(chars.Length, charCount); ReadCharBuffer(chars, charIndex, readCount); return readCount; diff --git a/BepInEx.Core/Console/Windows/WindowsConsoleDriver.cs b/BepInEx.Core/Console/Windows/WindowsConsoleDriver.cs index 1edb857..802cd52 100644 --- a/BepInEx.Core/Console/Windows/WindowsConsoleDriver.cs +++ b/BepInEx.Core/Console/Windows/WindowsConsoleDriver.cs @@ -92,8 +92,9 @@ internal class WindowsConsoleDriver : IConsoleDriver // Make sure of ConsoleEncoding helper class because on some Monos // Encoding.GetEncoding throws NotImplementedException on most codepages // NOTE: We don't set Console.OutputEncoding because it resets any existing Console.Out writers - if (!useManagedEncoder) - ConsoleEncoding.ConsoleCodePage = codepage; + // Always set the console output codepage so the Windows console interprets bytes correctly, + // regardless of whether we use a managed encoder or ConsoleEncoding to produce those bytes. + ConsoleEncoding.ConsoleCodePage = codepage; // If stdout exists, write to it, otherwise make it the same as console out // Not sure if this is needed? Does the original Console.Out still work? From a1afbfb169610ffbc1813d83e946c9e2cc0d2d63 Mon Sep 17 00:00:00 2001 From: HetCreep Date: Thu, 11 Jun 2026 16:46:18 +0700 Subject: [PATCH 61/65] Skip missing search directories to fix preloader DirectoryNotFoundException (#1331) --- BepInEx.Core/Utility.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/BepInEx.Core/Utility.cs b/BepInEx.Core/Utility.cs index a6b93bd..a346ada 100644 --- a/BepInEx.Core/Utility.cs +++ b/BepInEx.Core/Utility.cs @@ -6,6 +6,7 @@ using System.Reflection; using System.Reflection.Emit; using System.Security.Cryptography; using System.Text; +using BepInEx.Logging; using Mono.Cecil; namespace BepInEx; @@ -416,11 +417,21 @@ public static class Utility { var result = new Dictionary(StringComparer.InvariantCultureIgnoreCase); foreach (var directory in directories) - foreach (var file in Directory.GetFiles(directory, pattern)) { - var fileName = Path.GetFileName(file); - if (!result.ContainsKey(fileName)) - result[fileName] = file; + // A configured search directory (e.g. a missing unstripped_corlib) may not exist; warn and skip it + // instead of letting Directory.GetFiles throw DirectoryNotFoundException. + if (!Directory.Exists(directory)) + { + Logger.Log(LogLevel.Warning, $"Skipping search directory that does not exist: {directory}"); + continue; + } + + foreach (var file in Directory.GetFiles(directory, pattern)) + { + var fileName = Path.GetFileName(file); + if (!result.ContainsKey(fileName)) + result[fileName] = file; + } } return result.Values; From a06688e85bbac09023c213fd9a5efee1a4fa4d07 Mon Sep 17 00:00:00 2001 From: HetCreep Date: Thu, 11 Jun 2026 16:54:22 +0700 Subject: [PATCH 62/65] Degrade instead of crashing when the IL2CPP interop assemblies are missing (#1338) * Degrade instead of crashing when the interop assemblies are missing OnInvokeMethod references interop types (UnityEngine.Application/LogType) in its body. The CLR JIT-compiles the whole method before running any of it, so when the interop assemblies cannot be resolved the method throws FileNotFoundException at JIT time -- before its own try is entered -- and the exception escapes into the native runtime-invoke detour and crashes the process with nothing logged. Move the interop-referencing call into its own SetupUnityLogging method called inside the try. OnInvokeMethod then JITs without the interop assemblies present, the missing-interop failure is caught, and the loader degrades (logs and continues) so the game runs unmodded. Set unhook at the top of the try so the detour fires exactly once even on failure. * Condense the interop-degrade comments and reword the fatal log Per review: trim the JIT-bypass comments to one line each and change the fatal message to "Unable to execute IL2CPP chainloader, no plugins will be loaded". --- .../BepInEx.Unity.IL2CPP/IL2CPPChainloader.cs | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/Runtimes/Unity/BepInEx.Unity.IL2CPP/IL2CPPChainloader.cs b/Runtimes/Unity/BepInEx.Unity.IL2CPP/IL2CPPChainloader.cs index 1ccfb9e..5fcdb35 100644 --- a/Runtimes/Unity/BepInEx.Unity.IL2CPP/IL2CPPChainloader.cs +++ b/Runtimes/Unity/BepInEx.Unity.IL2CPP/IL2CPPChainloader.cs @@ -83,23 +83,19 @@ public class IL2CPPChainloader : BaseChainloader if (methodName == "Internal_ActiveSceneChanged") try { - if (ConfigUnityLogging.Value) - { - Logger.Sources.Add(new IL2CPPUnityLogSource()); - - Application.CallLogCallback("Test call after applying unity logging hook", "", LogType.Assert, - true); - } - + // Unhook up front so the detour fires once even if setup below throws. unhook = true; + // Isolated so a missing-interop JIT failure happens inside the try (caught), not in OnInvokeMethod itself. + SetupUnityLogging(); + Il2CppInteropManager.PreloadInteropAssemblies(); Instance.Execute(); } catch (Exception ex) { - Logger.Log(LogLevel.Fatal, "Unable to execute IL2CPP chainloader"); + Logger.Log(LogLevel.Fatal, "Unable to execute IL2CPP chainloader, no plugins will be loaded"); Logger.Log(LogLevel.Error, ex); } @@ -115,6 +111,17 @@ public class IL2CPPChainloader : BaseChainloader return result; } + // JIT-compiled only when called here, so OnInvokeMethod needs no interop assemblies present to JIT. + private static void SetupUnityLogging() + { + if (!ConfigUnityLogging.Value) + return; + + Logger.Sources.Add(new IL2CPPUnityLogSource()); + + Application.CallLogCallback("Test call after applying unity logging hook", "", LogType.Assert, true); + } + protected override void InitializeLoggers() { base.InitializeLoggers(); From bd467c9af6d456547c79e7018db828c4b2941194 Mon Sep 17 00:00:00 2001 From: js6pak Date: Thu, 11 Jun 2026 12:25:33 +0200 Subject: [PATCH 63/65] Expose a DisplayBepInExVersion (#556) --- BepInEx.Core/Paths.cs | 6 ++++++ BepInEx.Preloader.Core/Logging/ChainloaderLogHelper.cs | 9 +++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/BepInEx.Core/Paths.cs b/BepInEx.Core/Paths.cs index 6a37342..a1a44eb 100644 --- a/BepInEx.Core/Paths.cs +++ b/BepInEx.Core/Paths.cs @@ -19,6 +19,12 @@ public static class Paths Version.Parse(MetadataHelper.GetAttributes(typeof(Paths).Assembly)[0] .InformationalVersion); + /// + /// BepInEx version without the build suffix. + /// + public static Version DisplayBepInExVersion { get; } = + new(BepInExVersion.Major, BepInExVersion.Minor, BepInExVersion.Patch, BepInExVersion.PreRelease); + /// /// The path to the Managed folder that contains the main managed assemblies. /// diff --git a/BepInEx.Preloader.Core/Logging/ChainloaderLogHelper.cs b/BepInEx.Preloader.Core/Logging/ChainloaderLogHelper.cs index 43e48e0..73bdc9f 100644 --- a/BepInEx.Preloader.Core/Logging/ChainloaderLogHelper.cs +++ b/BepInEx.Preloader.Core/Logging/ChainloaderLogHelper.cs @@ -35,17 +35,14 @@ public static class ChainloaderLogHelper public static void PrintLogInfo(ManualLogSource log) { - var bepinVersion = Paths.BepInExVersion; - var versionMini = new SemanticVersioning.Version(bepinVersion.Major, bepinVersion.Minor, bepinVersion.Patch, - bepinVersion.PreRelease); - var consoleTitle = $"BepInEx {versionMini} - {Paths.ProcessName}"; + var consoleTitle = $"BepInEx {Paths.DisplayBepInExVersion} - {Paths.ProcessName}"; log.Log(LogLevel.Message, $"{consoleTitle} ({File.GetLastWriteTime(Paths.ExecutablePath)})"); if (ConsoleManager.ConsoleActive) ConsoleManager.SetConsoleTitle(consoleTitle); - if (!string.IsNullOrEmpty(bepinVersion.Build)) - log.Log(LogLevel.Message, $"Built from commit {bepinVersion.Build}"); + if (!string.IsNullOrEmpty(Paths.BepInExVersion.Build)) + log.Log(LogLevel.Message, $"Built from commit {Paths.BepInExVersion.Build}"); Logger.Log(LogLevel.Info, $"System platform: {GetPlatformString()}"); Logger.Log(LogLevel.Info, From da64b220eb38a7937ccab3e1a7f372ca66851ac9 Mon Sep 17 00:00:00 2001 From: HetCreep Date: Thu, 11 Jun 2026 19:49:59 +0700 Subject: [PATCH 64/65] Do not abort startup when the config file cannot be written (#1340) * Don't abort startup when the config file can't be written ConfigFile.Save opens the file for writing with no exception handling and is invoked automatically via SaveOnConfigSet while settings are bound during startup, before logging is initialized. A read-only or otherwise unwritable config file (a read-only install directory, a file locked by another process or antivirus) therefore throws UnauthorizedAccessException out of the early startup path and shows the Failed to start BepInEx dialog. --- BepInEx.Core/Configuration/ConfigFile.cs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/BepInEx.Core/Configuration/ConfigFile.cs b/BepInEx.Core/Configuration/ConfigFile.cs index e81b085..8f67ef7 100644 --- a/BepInEx.Core/Configuration/ConfigFile.cs +++ b/BepInEx.Core/Configuration/ConfigFile.cs @@ -34,7 +34,7 @@ public class ConfigFile : IDictionary if (File.Exists(ConfigFilePath)) Reload(); - else if (saveOnInit) Save(); + else if (saveOnInit) TrySave(); } public static ConfigFile CoreConfig { get; } = new(Paths.BepInExConfigPath, true); @@ -352,6 +352,24 @@ public class ConfigFile : IDictionary } } + /// + /// Writes the config to disk like , but logs and swallows I/O failures (e.g. a read-only + /// or locked config file) instead of throwing. Used only for the creation-time saves (initial file write and + /// new-entry binds), which can run before logging is up; explicit saves and setting changes still throw. + /// + private void TrySave() + { + try + { + Save(); + } + catch (Exception e) when (e is IOException or UnauthorizedAccessException) + { + Logger.Log(LogLevel.Warning, + $"Could not write config file {ConfigFilePath}: {e.Message}. Continuing with the in-memory configuration."); + } + } + #endregion #region Wraps @@ -452,7 +470,7 @@ public class ConfigFile : IDictionary } if (SaveOnConfigSet) - Save(); + TrySave(); return entry; } From 5f39645992ab8b944cad394b63470e4920f8b16d Mon Sep 17 00:00:00 2001 From: HetCreep Date: Thu, 11 Jun 2026 19:52:43 +0700 Subject: [PATCH 65/65] Allow multiple [TargetType] and [TargetAssembly] attributes (#1343) Patch-method discovery already reads both with the plural MetadataHelper.GetAttributes(), and the docs say additional target attributes can be specified, but the attributes lacked AllowMultiple = true, so the compiler rejected a second one (CS0579). Set AllowMultiple = true on both. --- BepInEx.Preloader.Core/Patching/Attributes.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BepInEx.Preloader.Core/Patching/Attributes.cs b/BepInEx.Preloader.Core/Patching/Attributes.cs index 95b21c4..c8c76c1 100644 --- a/BepInEx.Preloader.Core/Patching/Attributes.cs +++ b/BepInEx.Preloader.Core/Patching/Attributes.cs @@ -82,7 +82,7 @@ public class PatcherPluginInfoAttribute : Attribute /// /// Defines an assembly that a patch method will target. /// -[AttributeUsage(AttributeTargets.Method)] +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public class TargetAssemblyAttribute : Attribute { /// @@ -108,7 +108,7 @@ public class TargetAssemblyAttribute : Attribute /// /// Defines a type that a patch method will target. /// -[AttributeUsage(AttributeTargets.Method)] +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public class TargetTypeAttribute : Attribute { /// The short filename of the assembly of which belongs to.