From 325e0df14b47bc63f276743c359f60658537af42 Mon Sep 17 00:00:00 2001 From: ghorsington Date: Tue, 21 Dec 2021 16:09:39 +0200 Subject: [PATCH] Use C# 10, reformat project --- .editorconfig | 4 +- BepInEx.Core/BepInEx.Core.csproj | 2 +- BepInEx.Core/Bootstrap/BaseChainloader.cs | 695 ++++++----- BepInEx.Core/Bootstrap/TypeLoader.cs | 495 ++++---- .../Configuration/AcceptableValueBase.cs | 59 +- .../Configuration/AcceptableValueList.cs | 67 +- .../Configuration/AcceptableValueRange.cs | 91 +- .../Configuration/ConfigDefinition.cs | 193 ++-- .../Configuration/ConfigDescription.cs | 75 +- BepInEx.Core/Configuration/ConfigEntryBase.cs | 293 +++-- BepInEx.Core/Configuration/ConfigFile.cs | 1027 ++++++++--------- BepInEx.Core/Configuration/ConfigWrapper.cs | 87 +- .../Configuration/SettingChangedEventArgs.cs | 33 +- .../Configuration/TomlTypeConverter.cs | 529 +++++---- BepInEx.Core/Configuration/TypeConverter.cs | 29 +- BepInEx.Core/Console/ConsoleManager.cs | 279 +++-- BepInEx.Core/Console/IConsoleDriver.cs | 31 +- BepInEx.Core/Console/SafeConsole.cs | 185 ++- BepInEx.Core/Console/Unix/ConsoleWriter.cs | 81 +- .../Console/Unix/LinuxConsoleDriver.cs | 179 ++- BepInEx.Core/Console/Unix/TtyHandler.cs | 289 +++-- BepInEx.Core/Console/Unix/UnixStream.cs | 93 +- BepInEx.Core/Console/Unix/UnixStreamHelper.cs | 105 +- .../ConsoleEncoding.Buffers.cs | 90 +- .../ConsoleEncoding.PInvoke.cs | 71 +- .../ConsoleEncoding/ConsoleEncoding.cs | 145 ++- BepInEx.Core/Console/Windows/ConsoleWindow.cs | 309 +++-- .../Console/Windows/WindowsConsoleDriver.cs | 225 ++-- BepInEx.Core/Contract/Attributes.cs | 611 +++++----- BepInEx.Core/Contract/PluginInfo.cs | 201 ++-- BepInEx.Core/Logging/ConsoleLogListener.cs | 43 +- BepInEx.Core/Logging/DiskLogListener.cs | 173 ++- BepInEx.Core/Logging/HarmonyLogSource.cs | 57 +- BepInEx.Core/Logging/ILogListener.cs | 21 +- BepInEx.Core/Logging/ILogSource.cs | 25 +- BepInEx.Core/Logging/LogEventArgs.cs | 77 +- BepInEx.Core/Logging/LogLevel.cs | 155 ++- BepInEx.Core/Logging/Logger.cs | 153 ++- BepInEx.Core/Logging/ManualLogSource.cs | 125 +- BepInEx.Core/Logging/TraceLogSource.cs | 141 ++- BepInEx.Core/Paths.cs | 201 ++-- BepInEx.Core/Utility.cs | 683 ++++++----- BepInEx.IL2CPP/BasePlugin.cs | 47 +- BepInEx.IL2CPP/BepInEx.IL2CPP.csproj | 2 +- BepInEx.IL2CPP/DoorstopEntrypoint.cs | 181 ++- .../Hook/Allocator/LinuxPageAllocator.cs | 37 +- .../Hook/Allocator/MacOsPageAllocator.cs | 147 ++- .../Hook/Allocator/PageAllocator.cs | 233 ++-- .../Hook/Allocator/UnixPageAllocator.cs | 225 ++-- .../Hook/Allocator/WindowsPageAllocator.cs | 203 ++-- BepInEx.IL2CPP/Hook/DetourGenerator.cs | 385 +++--- BepInEx.IL2CPP/Hook/FastNativeDetour.cs | 275 +++-- .../Hook/IL2CPPDetourMethodPatcher.cs | 767 ++++++------ .../Hook/UnhollowerDetourHandler.cs | 13 +- BepInEx.IL2CPP/IL2CPPChainloader.cs | 277 +++-- BepInEx.IL2CPP/Logging/IL2CPPLogSource.cs | 35 +- .../Logging/IL2CPPUnityLogSource.cs | 57 +- BepInEx.IL2CPP/MonoExtensions.cs | 25 +- BepInEx.IL2CPP/Preloader.cs | 231 ++-- BepInEx.IL2CPP/ProxyAssemblyGenerator.cs | 621 +++++----- BepInEx.IL2CPP/UnityEngine/Input.cs | 675 ++++++----- .../Utils/Collections/CollectionExtensions.cs | 31 +- .../Collections/Il2CppManagedEnumerable.cs | 43 +- .../Collections/Il2CppManagedEnumerator.cs | 123 +- .../Collections/ManagedIl2CppEnumerable.cs | 19 +- .../Collections/ManagedIl2CppEnumerator.cs | 43 +- BepInEx.IL2CPP/Utils/Il2CppUtils.cs | 25 +- .../Utils/MonoBehaviourExtensions.cs | 13 +- BepInEx.IL2CPP/app.config | 18 +- BepInEx.NetLauncher/App.config | 6 +- BepInEx.NetLauncher/BasePlugin.cs | 37 +- .../BepInEx.NetLauncher.csproj | 2 +- BepInEx.NetLauncher/NetChainloader.cs | 27 +- BepInEx.NetLauncher/NetPreloader.cs | 260 ++--- BepInEx.NetLauncher/Program.cs | 221 ++-- .../RuntimeFixes/AssemblyFixes.cs | 119 +- BepInEx.NetLauncher/Utility.cs | 93 +- .../BepInEx.Preloader.Core.csproj | 2 +- BepInEx.Preloader.Core/EnvVars.cs | 67 +- .../InternalPreloaderLogger.cs | 9 +- .../Logging/ChainloaderLogHelper.cs | 63 +- .../Logging/PreloaderConsoleListener.cs | 45 +- .../Patching/AssemblyPatcher.cs | 929 +++++++-------- BepInEx.Preloader.Core/Patching/Attributes.cs | 215 ++-- .../Patching/BasePatcher.cs | 79 +- .../Patching/PatcherContext.cs | 167 ++- .../Patching/PatcherPluginMetadata.cs | 25 +- BepInEx.Preloader.Core/PlatformUtils.cs | 211 ++-- .../RuntimeFixes/ConsoleSetOutFix.cs | 65 +- .../BepInEx.Preloader.Unity.csproj | 2 +- BepInEx.Preloader.Unity/DoorstopEntrypoint.cs | 283 +++-- BepInEx.Preloader.Unity/EntrypointPatcher.cs | 200 ++-- .../RuntimeFixes/TraceFix.cs | 107 +- .../RuntimeFixes/UnityPatches.cs | 55 +- .../RuntimeFixes/XTermFix.cs | 191 ++- BepInEx.Preloader.Unity/UnityPreloader.cs | 333 +++--- BepInEx.Shared/BepInEx.Shared.shproj | 10 +- BepInEx.Shared/BuildInfoAttribute.cs | 29 +- BepInEx.Unity/BaseUnityPlugin.cs | 79 +- BepInEx.Unity/BepInEx.Unity.csproj | 2 +- BepInEx.Unity/BepInExInstance.cs | 9 +- BepInEx.Unity/Bootstrap/UnityChainloader.cs | 161 ++- .../Configuration/KeyboardShortcut.cs | 333 +++--- BepInEx.Unity/Logging/UnityLogSource.cs | 171 ++- BepInEx.Unity/ThreadingHelper.cs | 521 +++++---- BepInEx.Unity/UnityTomlTypeConverters.cs | 53 +- BepInExTests/AssemblyInit.cs | 33 +- BepInExTests/Configuration/ConfigFileTests.cs | 585 +++++----- BepInExTests/TrampolineTests.cs | 127 +- README.md | 7 +- 110 files changed, 9502 insertions(+), 9609 deletions(-) diff --git a/.editorconfig b/.editorconfig index 2c97ae5..918200f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -92,7 +92,7 @@ dotnet_naming_style.all_upper_style.capitalization = all_upper dotnet_naming_style.all_upper_style.word_separator = _ dotnet_naming_style.lower_camel_case_style.capitalization = camel_case dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case -dotnet_naming_symbols.constants_symbols.applicable_accessibilities = public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.constants_symbols.applicable_accessibilities = public, internal, protected, protected_internal, private_protected dotnet_naming_symbols.constants_symbols.applicable_kinds = field dotnet_naming_symbols.constants_symbols.required_modifiers = const dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private @@ -105,7 +105,7 @@ dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field -dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static,readonly +dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static, readonly dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:none dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none diff --git a/BepInEx.Core/BepInEx.Core.csproj b/BepInEx.Core/BepInEx.Core.csproj index ebb624c..7bdbe29 100644 --- a/BepInEx.Core/BepInEx.Core.csproj +++ b/BepInEx.Core/BepInEx.Core.csproj @@ -5,7 +5,7 @@ true BepInEx BepInEx - 9 + 10 ..\bin\Core\ $(OutputPath)\$(TargetFramework)\BepInEx.Core.xml Unity plugin injection framework diff --git a/BepInEx.Core/Bootstrap/BaseChainloader.cs b/BepInEx.Core/Bootstrap/BaseChainloader.cs index 459ed07..37c2fa6 100644 --- a/BepInEx.Core/Bootstrap/BaseChainloader.cs +++ b/BepInEx.Core/Bootstrap/BaseChainloader.cs @@ -10,403 +10,402 @@ using BepInEx.Configuration; using BepInEx.Logging; using Mono.Cecil; -namespace BepInEx.Bootstrap +namespace BepInEx.Bootstrap; + +public abstract class BaseChainloader { - public abstract class BaseChainloader + protected static readonly string CurrentAssemblyName = Assembly.GetExecutingAssembly().GetName().Name; + protected static readonly Version CurrentAssemblyVersion = Assembly.GetExecutingAssembly().GetName().Version; + + private static Regex allowedGuidRegex { get; } = new(@"^[a-zA-Z0-9\._\-]+$"); + + /// + /// Analyzes the given type definition and attempts to convert it to a valid + /// + /// 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) { - protected static readonly string CurrentAssemblyName = Assembly.GetExecutingAssembly().GetName().Name; - protected static readonly Version CurrentAssemblyVersion = Assembly.GetExecutingAssembly().GetName().Version; + if (type.IsInterface || type.IsAbstract) + return null; - private static Regex allowedGuidRegex { get; } = new(@"^[a-zA-Z0-9\._\-]+$"); - - /// - /// Analyzes the given type definition and attempts to convert it to a valid - /// - /// 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) + try { - if (type.IsInterface || type.IsAbstract) + if (!type.IsSubtypeOf(typeof(TPlugin))) return null; - - try - { - if (!type.IsSubtypeOf(typeof(TPlugin))) - return null; - } - catch (AssemblyResolutionException) - { - // Can happen if this type inherits a type from an assembly that can't be found. Safe to assume it's not a plugin. - return null; - } - - var metadata = BepInPlugin.FromCecilType(type); - - // Perform checks that will prevent the plugin from being loaded in ALL cases - if (metadata == null) - { - Logger.LogWarning($"Skipping over type [{type.FullName}] as no metadata attribute is specified"); - return null; - } - - if (string.IsNullOrEmpty(metadata.GUID) || !allowedGuidRegex.IsMatch(metadata.GUID)) - { - Logger.LogWarning($"Skipping type [{type.FullName}] because its GUID [{metadata.GUID}] is of an illegal format."); - return null; - } - - if (metadata.Version == null) - { - Logger.LogWarning($"Skipping type [{type.FullName}] because its version is invalid."); - return null; - } - - if (metadata.Name == null) - { - Logger.LogWarning($"Skipping type [{type.FullName}] because its name is null."); - return null; - } - - var filters = BepInProcess.FromCecilType(type); - var dependencies = BepInDependency.FromCecilType(type); - var incompatibilities = BepInIncompatibility.FromCecilType(type); - - var bepinVersion = - type.Module.AssemblyReferences.FirstOrDefault(reference => reference.Name == "BepInEx.Core")?.Version ?? - new Version(); - - return new PluginInfo - { - Metadata = metadata, - Processes = filters, - Dependencies = dependencies, - Incompatibilities = incompatibilities, - TypeName = type.FullName, - TargettedBepInExVersion = bepinVersion, - Location = assemblyLocation - }; + } + catch (AssemblyResolutionException) + { + // Can happen if this type inherits a type from an assembly that can't be found. Safe to assume it's not a plugin. + return null; } - 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(BepInPlugin).FullName)) - return false; + var metadata = BepInPlugin.FromCecilType(type); - return true; + // Perform checks that will prevent the plugin from being loaded in ALL cases + if (metadata == null) + { + Logger.LogWarning($"Skipping over type [{type.FullName}] as no metadata attribute is specified"); + return null; } - protected static bool PluginTargetsWrongBepin(PluginInfo pluginInfo) + if (string.IsNullOrEmpty(metadata.GUID) || !allowedGuidRegex.IsMatch(metadata.GUID)) { - var pluginTarget = pluginInfo.TargettedBepInExVersion; - // X.X.X.x - compare normally. x.x.x.X - nightly build number, ignore - if (pluginTarget.Major != CurrentAssemblyVersion.Major) return true; - if (pluginTarget.Minor > CurrentAssemblyVersion.Minor) return true; - if (pluginTarget.Minor < CurrentAssemblyVersion.Minor) return false; - return pluginTarget.Build > CurrentAssemblyVersion.Build; + Logger.LogWarning($"Skipping type [{type.FullName}] because its GUID [{metadata.GUID}] is of an illegal format."); + return null; } - #region Contract - - protected virtual string ConsoleTitle => $"BepInEx {Paths.BepInExVersion} - {Paths.ProcessName}"; - - private bool _initialized; - - /// - /// List of all instances loaded via the chainloader. - /// - public Dictionary Plugins { 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(); - - public virtual void Initialize(string gameExePath = null) + if (metadata.Version == null) { - if (_initialized) - throw new InvalidOperationException("Chainloader cannot be initialized multiple times"); - - // Set vitals - if (gameExePath != null) - // Checking for null allows a more advanced initialization workflow, where the Paths class has been initialized before calling Chainloader.Initialize - // This is used by Preloader to use environment variables, for example - Paths.SetExecutablePath(gameExePath); - - InitializeLoggers(); - - if (!Directory.Exists(Paths.PluginPath)) - Directory.CreateDirectory(Paths.PluginPath); - - if (!Directory.Exists(Paths.PatcherPluginPath)) - Directory.CreateDirectory(Paths.PatcherPluginPath); - - _initialized = true; - - Logger.LogMessage("Chainloader initialized"); + Logger.LogWarning($"Skipping type [{type.FullName}] because its version is invalid."); + return null; } - protected virtual void InitializeLoggers() + if (metadata.Name == null) { - if (ConsoleManager.ConsoleEnabled && !ConsoleManager.ConsoleActive) - ConsoleManager.CreateConsole(); + Logger.LogWarning($"Skipping type [{type.FullName}] because its name is null."); + return null; + } - if (ConsoleManager.ConsoleActive) + var filters = BepInProcess.FromCecilType(type); + var dependencies = BepInDependency.FromCecilType(type); + var incompatibilities = BepInIncompatibility.FromCecilType(type); + + var bepinVersion = + type.Module.AssemblyReferences.FirstOrDefault(reference => reference.Name == "BepInEx.Core")?.Version ?? + new Version(); + + return new PluginInfo + { + Metadata = metadata, + Processes = filters, + Dependencies = dependencies, + Incompatibilities = incompatibilities, + TypeName = type.FullName, + TargettedBepInExVersion = bepinVersion, + Location = assemblyLocation + }; + } + + 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(BepInPlugin).FullName)) + return false; + + return true; + } + + protected static bool PluginTargetsWrongBepin(PluginInfo pluginInfo) + { + var pluginTarget = pluginInfo.TargettedBepInExVersion; + // X.X.X.x - compare normally. x.x.x.X - nightly build number, ignore + if (pluginTarget.Major != CurrentAssemblyVersion.Major) return true; + if (pluginTarget.Minor > CurrentAssemblyVersion.Minor) return true; + if (pluginTarget.Minor < CurrentAssemblyVersion.Minor) return false; + return pluginTarget.Build > CurrentAssemblyVersion.Build; + } + + #region Contract + + protected virtual string ConsoleTitle => $"BepInEx {Paths.BepInExVersion} - {Paths.ProcessName}"; + + private bool _initialized; + + /// + /// List of all instances loaded via the chainloader. + /// + public Dictionary Plugins { 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(); + + public virtual void Initialize(string gameExePath = null) + { + if (_initialized) + throw new InvalidOperationException("Chainloader cannot be initialized multiple times"); + + // Set vitals + if (gameExePath != null) + // Checking for null allows a more advanced initialization workflow, where the Paths class has been initialized before calling Chainloader.Initialize + // This is used by Preloader to use environment variables, for example + Paths.SetExecutablePath(gameExePath); + + InitializeLoggers(); + + if (!Directory.Exists(Paths.PluginPath)) + Directory.CreateDirectory(Paths.PluginPath); + + if (!Directory.Exists(Paths.PatcherPluginPath)) + Directory.CreateDirectory(Paths.PatcherPluginPath); + + _initialized = true; + + Logger.LogMessage("Chainloader initialized"); + } + + protected virtual void InitializeLoggers() + { + if (ConsoleManager.ConsoleEnabled && !ConsoleManager.ConsoleActive) + ConsoleManager.CreateConsole(); + + if (ConsoleManager.ConsoleActive) + { + if (!Logger.Listeners.Any(x => x is ConsoleLogListener)) + Logger.Listeners.Add(new ConsoleLogListener()); + + ConsoleManager.SetConsoleTitle(ConsoleTitle); + } + + if (ConfigDiskLogging.Value) + Logger.Listeners.Add(new DiskLogListener("LogOutput.log", ConfigDiskLoggingDisplayedLevel.Value, + ConfigDiskAppend.Value, ConfigDiskLoggingInstantFlushing.Value, + ConfigDiskLoggingFileLimit.Value)); + + if (!TraceLogSource.IsListening) + Logger.Sources.Add(TraceLogSource.CreateSource()); + + if (!Logger.Sources.Any(x => x is HarmonyLogSource)) + Logger.Sources.Add(new HarmonyLogSource()); + } + + protected virtual IList DiscoverPlugins() + { + var pluginsToLoad = + TypeLoader.FindPluginTypes(Paths.PluginPath, ToPluginInfo, HasBepinPlugins, "chainloader"); + + return pluginsToLoad.SelectMany(p => p.Value).ToList(); + } + + protected virtual IList ModifyLoadOrder(IList plugins) + { + // We use a sorted dictionary to ensure consistent load order + var dependencyDict = + new SortedDictionary>(StringComparer.InvariantCultureIgnoreCase); + var pluginsByGuid = new Dictionary(); + + foreach (var pluginInfoGroup in plugins.GroupBy(info => info.Metadata.GUID)) + { + PluginInfo loadedVersion = null; + foreach (var pluginInfo in pluginInfoGroup.OrderByDescending(x => x.Metadata.Version)) { - if (!Logger.Listeners.Any(x => x is ConsoleLogListener)) - Logger.Listeners.Add(new ConsoleLogListener()); - - ConsoleManager.SetConsoleTitle(ConsoleTitle); - } - - if (ConfigDiskLogging.Value) - Logger.Listeners.Add(new DiskLogListener("LogOutput.log", ConfigDiskLoggingDisplayedLevel.Value, - ConfigDiskAppend.Value, ConfigDiskLoggingInstantFlushing.Value, - ConfigDiskLoggingFileLimit.Value)); - - if (!TraceLogSource.IsListening) - Logger.Sources.Add(TraceLogSource.CreateSource()); - - if (!Logger.Sources.Any(x => x is HarmonyLogSource)) - Logger.Sources.Add(new HarmonyLogSource()); - } - - protected virtual IList DiscoverPlugins() - { - var pluginsToLoad = - TypeLoader.FindPluginTypes(Paths.PluginPath, ToPluginInfo, HasBepinPlugins, "chainloader"); - - return pluginsToLoad.SelectMany(p => p.Value).ToList(); - } - - protected virtual IList ModifyLoadOrder(IList plugins) - { - // We use a sorted dictionary to ensure consistent load order - var dependencyDict = - new SortedDictionary>(StringComparer.InvariantCultureIgnoreCase); - var pluginsByGuid = new Dictionary(); - - foreach (var pluginInfoGroup in plugins.GroupBy(info => info.Metadata.GUID)) - { - PluginInfo loadedVersion = null; - foreach (var pluginInfo in pluginInfoGroup.OrderByDescending(x => x.Metadata.Version)) + if (loadedVersion != null) { - if (loadedVersion != null) + Logger.LogWarning($"Skipping [{pluginInfo}] because a newer version exists ({loadedVersion})"); + continue; + } + + // Perform checks that will prevent loading plugins in this run + var filters = pluginInfo.Processes.ToList(); + var invalidProcessName = filters.Count != 0 && + filters.All(x => !string.Equals(x.ProcessName.Replace(".exe", ""), + Paths.ProcessName, + StringComparison + .InvariantCultureIgnoreCase)); + + if (invalidProcessName) + { + Logger.LogWarning($"Skipping [{pluginInfo}] because of process filters ({string.Join(", ", pluginInfo.Processes.Select(p => p.ProcessName).ToArray())})"); + continue; + } + + loadedVersion = pluginInfo; + dependencyDict[pluginInfo.Metadata.GUID] = pluginInfo.Dependencies.Select(d => d.DependencyGUID); + pluginsByGuid[pluginInfo.Metadata.GUID] = pluginInfo; + } + } + + foreach (var pluginInfo in pluginsByGuid.Values.ToList()) + if (pluginInfo.Incompatibilities.Any(incompatibility => + pluginsByGuid.ContainsKey(incompatibility.IncompatibilityGUID)) + ) + { + pluginsByGuid.Remove(pluginInfo.Metadata.GUID); + dependencyDict.Remove(pluginInfo.Metadata.GUID); + + var incompatiblePlugins = pluginInfo.Incompatibilities.Select(x => x.IncompatibilityGUID) + .Where(x => pluginsByGuid.ContainsKey(x)).ToArray(); + var message = + $@"Could not load [{pluginInfo}] because it is incompatible with: {string.Join(", ", incompatiblePlugins)}"; + DependencyErrors.Add(message); + Logger.LogError(message); + } + else if (PluginTargetsWrongBepin(pluginInfo)) + { + var message = + $@"Plugin [{pluginInfo}] targets a wrong version of BepInEx ({pluginInfo.TargettedBepInExVersion}) and might not work until you update"; + DependencyErrors.Add(message); + Logger.LogWarning(message); + } + + var emptyDependencies = new string[0]; + + // Sort plugins by their dependencies. + // Give missing dependencies no dependencies of its own, which will cause missing plugins to be first in the resulting list. + var sortedPlugins = Utility.TopologicalSort(dependencyDict.Keys, + x => + dependencyDict.TryGetValue(x, out var deps) + ? deps + : emptyDependencies).ToList(); + + return sortedPlugins.Where(pluginsByGuid.ContainsKey).Select(x => pluginsByGuid[x]).ToList(); + } + + public virtual void Execute() + { + try + { + var plugins = DiscoverPlugins(); + + Logger.LogInfo($"{plugins.Count} plugin{(plugins.Count == 1 ? "" : "s")} to load"); + + var sortedPlugins = ModifyLoadOrder(plugins); + + var invalidPlugins = new HashSet(); + var processedPlugins = new Dictionary(); + var loadedAssemblies = new Dictionary(); + + foreach (var plugin in sortedPlugins) + { + 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); + if (!dependencyExists || dependency.VersionRange != null && + !dependency.VersionRange.IsSatisfied(pluginVersion)) { - Logger.LogWarning($"Skipping [{pluginInfo}] because a newer version exists ({loadedVersion})"); + // If the dependency is hard, collect it into a list to show + if (IsHardDependency(dependency)) + missingDependencies.Add(dependency); continue; } - // Perform checks that will prevent loading plugins in this run - var filters = pluginInfo.Processes.ToList(); - var invalidProcessName = filters.Count != 0 && - filters.All(x => !string.Equals(x.ProcessName.Replace(".exe", ""), - Paths.ProcessName, - StringComparison - .InvariantCultureIgnoreCase)); - - if (invalidProcessName) + // 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)) { - Logger.LogWarning($"Skipping [{pluginInfo}] because of process filters ({string.Join(", ", pluginInfo.Processes.Select(p => p.ProcessName).ToArray())})"); - continue; + dependsOnInvalidPlugin = true; + break; } - - loadedVersion = pluginInfo; - dependencyDict[pluginInfo.Metadata.GUID] = pluginInfo.Dependencies.Select(d => d.DependencyGUID); - pluginsByGuid[pluginInfo.Metadata.GUID] = pluginInfo; } - } - foreach (var pluginInfo in pluginsByGuid.Values.ToList()) - if (pluginInfo.Incompatibilities.Any(incompatibility => - pluginsByGuid.ContainsKey(incompatibility.IncompatibilityGUID)) - ) - { - pluginsByGuid.Remove(pluginInfo.Metadata.GUID); - dependencyDict.Remove(pluginInfo.Metadata.GUID); + processedPlugins.Add(plugin.Metadata.GUID, plugin.Metadata.Version); - var incompatiblePlugins = pluginInfo.Incompatibilities.Select(x => x.IncompatibilityGUID) - .Where(x => pluginsByGuid.ContainsKey(x)).ToArray(); - var message = - $@"Could not load [{pluginInfo}] because it is incompatible with: {string.Join(", ", incompatiblePlugins)}"; - DependencyErrors.Add(message); - Logger.LogError(message); - } - else if (PluginTargetsWrongBepin(pluginInfo)) + if (dependsOnInvalidPlugin) { var message = - $@"Plugin [{pluginInfo}] targets a wrong version of BepInEx ({pluginInfo.TargettedBepInExVersion}) and might not work until you update"; + $"Skipping [{plugin}] because it has a dependency that was not loaded. See previous errors for details."; DependencyErrors.Add(message); Logger.LogWarning(message); + continue; } - var emptyDependencies = new string[0]; - - // Sort plugins by their dependencies. - // Give missing dependencies no dependencies of its own, which will cause missing plugins to be first in the resulting list. - var sortedPlugins = Utility.TopologicalSort(dependencyDict.Keys, - x => - dependencyDict.TryGetValue(x, out var deps) - ? deps - : emptyDependencies).ToList(); - - return sortedPlugins.Where(pluginsByGuid.ContainsKey).Select(x => pluginsByGuid[x]).ToList(); - } - - public virtual void Execute() - { - try - { - var plugins = DiscoverPlugins(); - - Logger.LogInfo($"{plugins.Count} plugin{(plugins.Count == 1 ? "" : "s")} to load"); - - var sortedPlugins = ModifyLoadOrder(plugins); - - var invalidPlugins = new HashSet(); - var processedPlugins = new Dictionary(); - var loadedAssemblies = new Dictionary(); - - foreach (var plugin in sortedPlugins) + if (missingDependencies.Count != 0) { - var dependsOnInvalidPlugin = false; - var missingDependencies = new List(); - foreach (var dependency in plugin.Dependencies) - { - static bool IsHardDependency(BepInDependency dep) => - (dep.Flags & BepInDependency.DependencyFlags.HardDependency) != 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.LogError(message); - // If the dependency wasn't already processed, it's missing altogether - var dependencyExists = - processedPlugins.TryGetValue(dependency.DependencyGUID, out var pluginVersion); - 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.LogWarning(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.LogError(message); - - invalidPlugins.Add(plugin.Metadata.GUID); - continue; - } - - try - { - Logger.LogInfo($"Loading [{plugin}]"); - - if (!loadedAssemblies.TryGetValue(plugin.Location, out var ass)) - loadedAssemblies[plugin.Location] = ass = Assembly.LoadFile(plugin.Location); - - Plugins[plugin.Metadata.GUID] = plugin; - TryRunModuleCtor(plugin, ass); - plugin.Instance = LoadPlugin(plugin, ass); - - //_plugins.Add((TPlugin)plugin.Instance); - } - catch (Exception ex) - { - invalidPlugins.Add(plugin.Metadata.GUID); - Plugins.Remove(plugin.Metadata.GUID); - - Logger.LogError($"Error loading [{plugin}]: {(ex is ReflectionTypeLoadException re ? TypeLoader.TypeLoadExceptionToString(re) : ex)}"); - } + invalidPlugins.Add(plugin.Metadata.GUID); + continue; } - } - catch (Exception ex) - { + try { - ConsoleManager.CreateConsole(); + Logger.LogInfo($"Loading [{plugin}]"); + + if (!loadedAssemblies.TryGetValue(plugin.Location, out var ass)) + loadedAssemblies[plugin.Location] = ass = Assembly.LoadFile(plugin.Location); + + Plugins[plugin.Metadata.GUID] = plugin; + TryRunModuleCtor(plugin, ass); + plugin.Instance = LoadPlugin(plugin, ass); + + //_plugins.Add((TPlugin)plugin.Instance); } - catch { } + catch (Exception ex) + { + invalidPlugins.Add(plugin.Metadata.GUID); + Plugins.Remove(plugin.Metadata.GUID); - Logger.LogError("Error occurred starting the game: " + ex); + Logger.LogError($"Error loading [{plugin}]: {(ex is ReflectionTypeLoadException re ? TypeLoader.TypeLoadExceptionToString(re) : ex)}"); + } } - - Logger.LogMessage("Chainloader startup complete"); } - - private static void TryRunModuleCtor(PluginInfo plugin, Assembly assembly) + catch (Exception ex) { try { - RuntimeHelpers.RunModuleConstructor(assembly.GetType(plugin.TypeName).Module.ModuleHandle); - } - catch (Exception e) - { - Logger.LogWarning($"Couldn't run Module constructor for {assembly.FullName}::{plugin.TypeName}: {e}"); + ConsoleManager.CreateConsole(); } + catch { } + + Logger.LogError("Error occurred starting the game: " + ex); } - public abstract TPlugin LoadPlugin(PluginInfo pluginInfo, Assembly pluginAssembly); - - #endregion - - #region Config - - private static readonly ConfigEntry ConfigDiskAppend = ConfigFile.CoreConfig.Bind( - "Logging.Disk", "AppendLog", - false, - "Appends to the log file instead of overwriting, on game startup."); - - private static readonly ConfigEntry ConfigDiskLogging = ConfigFile.CoreConfig.Bind( - "Logging.Disk", "Enabled", - true, - "Enables writing log messages to disk."); - - private static readonly ConfigEntry ConfigDiskLoggingDisplayedLevel = ConfigFile.CoreConfig.Bind( - "Logging.Disk", "LogLevels", - LogLevel.Fatal | LogLevel.Error | LogLevel.Warning | LogLevel.Message | LogLevel.Info, - "Only displays the specified log levels in the disk log output."); - - private static readonly ConfigEntry ConfigDiskLoggingInstantFlushing = ConfigFile.CoreConfig.Bind( - "Logging.Disk", "InstantFlushing", - false, - new StringBuilder() - .AppendLine("If true, instantly writes any received log entries to disk.") - .AppendLine("This incurs a major performance hit if a lot of log messages are being written, however it is really useful for debugging crashes.") - .ToString()); - - private static readonly ConfigEntry ConfigDiskLoggingFileLimit = ConfigFile.CoreConfig.Bind( - "Logging.Disk", "ConcurrentFileLimit", - 5, - new StringBuilder() - .AppendLine("The maximum amount of concurrent log files that will be written to disk.") - .AppendLine("As one log file is used per open game instance, you may find it necessary to increase this limit when debugging multiple instances at the same time.") - .ToString()); - - #endregion + Logger.LogMessage("Chainloader startup complete"); } + + private static void TryRunModuleCtor(PluginInfo plugin, Assembly assembly) + { + try + { + RuntimeHelpers.RunModuleConstructor(assembly.GetType(plugin.TypeName).Module.ModuleHandle); + } + catch (Exception e) + { + Logger.LogWarning($"Couldn't run Module constructor for {assembly.FullName}::{plugin.TypeName}: {e}"); + } + } + + public abstract TPlugin LoadPlugin(PluginInfo pluginInfo, Assembly pluginAssembly); + + #endregion + + #region Config + + private static readonly ConfigEntry ConfigDiskAppend = ConfigFile.CoreConfig.Bind( + "Logging.Disk", "AppendLog", + false, + "Appends to the log file instead of overwriting, on game startup."); + + private static readonly ConfigEntry ConfigDiskLogging = ConfigFile.CoreConfig.Bind( + "Logging.Disk", "Enabled", + true, + "Enables writing log messages to disk."); + + private static readonly ConfigEntry ConfigDiskLoggingDisplayedLevel = ConfigFile.CoreConfig.Bind( + "Logging.Disk", "LogLevels", + LogLevel.Fatal | LogLevel.Error | LogLevel.Warning | LogLevel.Message | LogLevel.Info, + "Only displays the specified log levels in the disk log output."); + + private static readonly ConfigEntry ConfigDiskLoggingInstantFlushing = ConfigFile.CoreConfig.Bind( + "Logging.Disk", "InstantFlushing", + false, + new StringBuilder() + .AppendLine("If true, instantly writes any received log entries to disk.") + .AppendLine("This incurs a major performance hit if a lot of log messages are being written, however it is really useful for debugging crashes.") + .ToString()); + + private static readonly ConfigEntry ConfigDiskLoggingFileLimit = ConfigFile.CoreConfig.Bind( + "Logging.Disk", "ConcurrentFileLimit", + 5, + new StringBuilder() + .AppendLine("The maximum amount of concurrent log files that will be written to disk.") + .AppendLine("As one log file is used per open game instance, you may find it necessary to increase this limit when debugging multiple instances at the same time.") + .ToString()); + + #endregion } diff --git a/BepInEx.Core/Bootstrap/TypeLoader.cs b/BepInEx.Core/Bootstrap/TypeLoader.cs index 5baa246..368a5b2 100644 --- a/BepInEx.Core/Bootstrap/TypeLoader.cs +++ b/BepInEx.Core/Bootstrap/TypeLoader.cs @@ -8,305 +8,304 @@ using BepInEx.Configuration; using BepInEx.Logging; using Mono.Cecil; -namespace BepInEx.Bootstrap +namespace BepInEx.Bootstrap; + +/// +/// A cacheable metadata item. Can be used with and +/// to cache plugin metadata. +/// +public interface ICacheable { /// - /// A cacheable metadata item. Can be used with and - /// to cache plugin metadata. + /// Serialize the object into a binary format. /// - public interface ICacheable - { - /// - /// Serialize the object into a binary format. - /// - /// - void Save(BinaryWriter bw); - - /// - /// Loads the object from binary format. - /// - /// - void Load(BinaryReader br); - } + /// + void Save(BinaryWriter bw); /// - /// A cached assembly. + /// Loads the object from binary format. /// - /// - public class CachedAssembly where T : ICacheable - { - /// - /// List of cached items inside the assembly. - /// - public List CacheItems { get; set; } + /// + void Load(BinaryReader br); +} - /// - /// Hash of the assembly. Used to verify that the assembly hasn't been changed. - /// - public string Hash { get; set; } - } +/// +/// A cached assembly. +/// +/// +public class CachedAssembly where T : ICacheable +{ + /// + /// List of cached items inside the assembly. + /// + public List CacheItems { get; set; } /// - /// Provides methods for loading specified types from an assembly. + /// Hash of the assembly. Used to verify that the assembly hasn't been changed. /// - public static class TypeLoader + public string Hash { get; set; } +} + +/// +/// Provides methods for loading specified types from an assembly. +/// +public static class TypeLoader +{ + /// + /// Default assembly resolved used by the + /// + public static readonly DefaultAssemblyResolver CecilResolver; + + /// + /// Default reader parameters used by + /// + public static readonly ReaderParameters ReaderParameters; + + public static HashSet SearchDirectories = new(); + + #region Config + + private static readonly ConfigEntry EnableAssemblyCache = ConfigFile.CoreConfig.Bind( + "Caching", "EnableAssemblyCache", + true, + "Enable/disable assembly metadata cache\nEnabling this will speed up discovery of plugins and patchers by caching the metadata of all types BepInEx discovers."); + + #endregion + + static TypeLoader() { - /// - /// Default assembly resolved used by the - /// - public static readonly DefaultAssemblyResolver CecilResolver; + CecilResolver = new DefaultAssemblyResolver(); + ReaderParameters = new ReaderParameters { AssemblyResolver = CecilResolver }; - /// - /// Default reader parameters used by - /// - public static readonly ReaderParameters ReaderParameters; + CecilResolver.ResolveFailure += CecilResolveOnFailure; + } - public static HashSet SearchDirectories = new(); + public static AssemblyDefinition CecilResolveOnFailure(object sender, AssemblyNameReference reference) + { + if (!Utility.TryParseAssemblyName(reference.FullName, out var name)) + return null; - #region Config - - private static readonly ConfigEntry EnableAssemblyCache = ConfigFile.CoreConfig.Bind( - "Caching", "EnableAssemblyCache", - true, - "Enable/disable assembly metadata cache\nEnabling this will speed up discovery of plugins and patchers by caching the metadata of all types BepInEx discovers."); - - #endregion - - static TypeLoader() + var resolveDirs = new[] { - CecilResolver = new DefaultAssemblyResolver(); - ReaderParameters = new ReaderParameters { AssemblyResolver = CecilResolver }; + Paths.BepInExAssemblyDirectory, + Paths.PluginPath, + Paths.PatcherPluginPath, + Paths.ManagedPath + }.Concat(SearchDirectories); - CecilResolver.ResolveFailure += CecilResolveOnFailure; - } - - public static AssemblyDefinition CecilResolveOnFailure(object sender, AssemblyNameReference reference) + foreach (var dir in resolveDirs) { - if (!Utility.TryParseAssemblyName(reference.FullName, out var name)) - return null; - - var resolveDirs = new[] + if (!Directory.Exists(dir)) { - Paths.BepInExAssemblyDirectory, - Paths.PluginPath, - Paths.PatcherPluginPath, - Paths.ManagedPath, - }.Concat(SearchDirectories); - - foreach (var dir in resolveDirs) - { - if (!Directory.Exists(dir)) - { - Logger.LogDebug($"Unable to resolve cecil search directory '{dir}'"); - continue; - } - - if (Utility.TryResolveDllAssembly(name, dir, ReaderParameters, out var assembly)) - return assembly; + Logger.LogDebug($"Unable to resolve cecil search directory '{dir}'"); + continue; } - return AssemblyResolve?.Invoke(sender, reference); + if (Utility.TryResolveDllAssembly(name, dir, ReaderParameters, out var assembly)) + return assembly; } - /// - /// Event fired when fails to resolve a type during type loading. - /// - public static event AssemblyResolveEventHandler AssemblyResolve; + return AssemblyResolve?.Invoke(sender, reference); + } - /// - /// Looks up assemblies in the given directory and locates all types that can be loaded and collects their metadata. - /// - /// The specific base type to search for. - /// The directory to search for assemblies. - /// 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 Dictionary> FindPluginTypes(string directory, - Func typeSelector, - Func assemblyFilter = - null, - string cacheName = null) - where T : ICacheable, new() - { - var result = new Dictionary>(); - var hashes = new Dictionary(); - Dictionary> cache = null; + /// + /// Event fired when fails to resolve a type during type loading. + /// + public static event AssemblyResolveEventHandler AssemblyResolve; - if (cacheName != null) - cache = LoadAssemblyCache(cacheName); + /// + /// Looks up assemblies in the given directory and locates all types that can be loaded and collects their metadata. + /// + /// The specific base type to search for. + /// The directory to search for assemblies. + /// 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 Dictionary> FindPluginTypes(string directory, + Func typeSelector, + Func assemblyFilter = + null, + string cacheName = null) + where T : ICacheable, new() + { + var result = new Dictionary>(); + var hashes = new Dictionary(); + Dictionary> cache = null; - foreach (var dll in Directory.GetFiles(Path.GetFullPath(directory), "*.dll", SearchOption.AllDirectories)) - try - { - using var dllMs = new MemoryStream(File.ReadAllBytes(dll)); - var hash = Utility.HashStream(dllMs); - hashes[dll] = hash; - dllMs.Position = 0; - if (cache != null && cache.TryGetValue(dll, out var cacheEntry)) - if (hash == cacheEntry.Hash) - { - result[dll] = cacheEntry.CacheItems; - continue; - } + if (cacheName != null) + cache = LoadAssemblyCache(cacheName); - using var ass = AssemblyDefinition.ReadAssembly(dllMs, ReaderParameters); - Logger.LogDebug($"Examining '{dll}'"); - - if (!assemblyFilter?.Invoke(ass) ?? false) + foreach (var dll in Directory.GetFiles(Path.GetFullPath(directory), "*.dll", SearchOption.AllDirectories)) + try + { + using var dllMs = new MemoryStream(File.ReadAllBytes(dll)); + var hash = Utility.HashStream(dllMs); + hashes[dll] = hash; + dllMs.Position = 0; + if (cache != null && cache.TryGetValue(dll, out var cacheEntry)) + if (hash == cacheEntry.Hash) { - result[dll] = new List(); + result[dll] = cacheEntry.CacheItems; continue; } - var matches = ass.MainModule.Types - .Select(t => typeSelector(t, dll)) - .Where(t => t != null).ToList(); - result[dll] = matches; - } - catch (BadImageFormatException e) + using var ass = AssemblyDefinition.ReadAssembly(dllMs, ReaderParameters); + Logger.LogDebug($"Examining '{dll}'"); + + if (!assemblyFilter?.Invoke(ass) ?? false) { - Logger.LogDebug($"Skipping loading {dll} because it's not a valid .NET assembly. Full error: {e.Message}"); - } - catch (Exception e) - { - Logger.LogError(e.ToString()); + result[dll] = new List(); + continue; } - if (cacheName != null) - SaveAssemblyCache(cacheName, result, hashes); + var matches = ass.MainModule.Types + .Select(t => typeSelector(t, dll)) + .Where(t => t != null).ToList(); + result[dll] = matches; + } + catch (BadImageFormatException e) + { + Logger.LogDebug($"Skipping loading {dll} because it's not a valid .NET assembly. Full error: {e.Message}"); + } + catch (Exception e) + { + Logger.LogError(e.ToString()); + } - return result; - } + if (cacheName != null) + SaveAssemblyCache(cacheName, result, hashes); - /// - /// Loads an index of type metadatas from a cache. - /// - /// Name of the cache - /// Cacheable item - /// - /// Cached type metadatas indexed by the path of the assembly that defines the type. If no cache is defined, - /// return null. - /// - public static Dictionary> LoadAssemblyCache(string cacheName) - where T : ICacheable, new() + return result; + } + + /// + /// Loads an index of type metadatas from a cache. + /// + /// Name of the cache + /// Cacheable item + /// + /// Cached type metadatas indexed by the path of the assembly that defines the type. If no cache is defined, + /// return null. + /// + public static Dictionary> LoadAssemblyCache(string cacheName) + where T : ICacheable, new() + { + if (!EnableAssemblyCache.Value) + return null; + + var result = new Dictionary>(); + try { - if (!EnableAssemblyCache.Value) + var path = Path.Combine(Paths.CachePath, $"{cacheName}_typeloader.dat"); + if (!File.Exists(path)) return null; - var result = new Dictionary>(); - try + using (var br = new BinaryReader(File.OpenRead(path))) { - var path = Path.Combine(Paths.CachePath, $"{cacheName}_typeloader.dat"); - if (!File.Exists(path)) - return null; + var entriesCount = br.ReadInt32(); - using (var br = new BinaryReader(File.OpenRead(path))) + for (var i = 0; i < entriesCount; i++) { - var entriesCount = br.ReadInt32(); + var entryIdentifier = br.ReadString(); + var hash = br.ReadString(); + var itemsCount = br.ReadInt32(); + var items = new List(); - for (var i = 0; i < entriesCount; i++) + for (var j = 0; j < itemsCount; j++) { - var entryIdentifier = br.ReadString(); - var hash = br.ReadString(); - var itemsCount = br.ReadInt32(); - var items = new List(); - - for (var j = 0; j < itemsCount; j++) - { - var entry = new T(); - entry.Load(br); - items.Add(entry); - } - - result[entryIdentifier] = new CachedAssembly { Hash = hash, CacheItems = items }; + var entry = new T(); + entry.Load(br); + items.Add(entry); } + + result[entryIdentifier] = new CachedAssembly { Hash = hash, CacheItems = items }; } } - catch (Exception e) - { - Logger.LogWarning($"Failed to load cache \"{cacheName}\"; skipping loading cache. Reason: {e.Message}."); - } - - return result; - } - - /// - /// Saves indexed type metadata into a cache. - /// - /// Name of the cache - /// List of plugin metadatas indexed by the path to the assembly that contains the types - /// Hash values that can be used for checking similarity between cached and live assembly - /// Cacheable item - public static void SaveAssemblyCache(string cacheName, - Dictionary> entries, - Dictionary hashes) - where T : ICacheable - { - if (!EnableAssemblyCache.Value) - return; - - try - { - if (!Directory.Exists(Paths.CachePath)) - Directory.CreateDirectory(Paths.CachePath); - - var path = Path.Combine(Paths.CachePath, $"{cacheName}_typeloader.dat"); - - using var bw = new BinaryWriter(File.OpenWrite(path)); - bw.Write(entries.Count); - - foreach (var kv in entries) - { - bw.Write(kv.Key); - bw.Write(hashes.TryGetValue(kv.Key, out var hash) ? hash : ""); - bw.Write(kv.Value.Count); - - foreach (var item in kv.Value) - item.Save(bw); - } - } - catch (Exception e) - { - Logger.LogWarning($"Failed to save cache \"{cacheName}\"; skipping saving cache. Reason: {e.Message}."); - } } - - /// - /// Converts TypeLoadException to a readable string. - /// - /// TypeLoadException - /// Readable representation of the exception - public static string TypeLoadExceptionToString(ReflectionTypeLoadException ex) + catch (Exception e) { - var sb = new StringBuilder(); - foreach (var exSub in ex.LoaderExceptions) + Logger.LogWarning($"Failed to load cache \"{cacheName}\"; skipping loading cache. Reason: {e.Message}."); + } + + return result; + } + + /// + /// Saves indexed type metadata into a cache. + /// + /// Name of the cache + /// List of plugin metadatas indexed by the path to the assembly that contains the types + /// Hash values that can be used for checking similarity between cached and live assembly + /// Cacheable item + public static void SaveAssemblyCache(string cacheName, + Dictionary> entries, + Dictionary hashes) + where T : ICacheable + { + if (!EnableAssemblyCache.Value) + return; + + try + { + if (!Directory.Exists(Paths.CachePath)) + Directory.CreateDirectory(Paths.CachePath); + + var path = Path.Combine(Paths.CachePath, $"{cacheName}_typeloader.dat"); + + using var bw = new BinaryWriter(File.OpenWrite(path)); + bw.Write(entries.Count); + + foreach (var kv in entries) { - sb.AppendLine(exSub.Message); - if (exSub is FileNotFoundException exFileNotFound) - { - if (!string.IsNullOrEmpty(exFileNotFound.FusionLog)) - { - sb.AppendLine("Fusion Log:"); - sb.AppendLine(exFileNotFound.FusionLog); - } - } - else if (exSub is FileLoadException exLoad) - { - if (!string.IsNullOrEmpty(exLoad.FusionLog)) - { - sb.AppendLine("Fusion Log:"); - sb.AppendLine(exLoad.FusionLog); - } - } + bw.Write(kv.Key); + bw.Write(hashes.TryGetValue(kv.Key, out var hash) ? hash : ""); + bw.Write(kv.Value.Count); - sb.AppendLine(); + foreach (var item in kv.Value) + item.Save(bw); } - - return sb.ToString(); + } + catch (Exception e) + { + Logger.LogWarning($"Failed to save cache \"{cacheName}\"; skipping saving cache. Reason: {e.Message}."); } } + + /// + /// Converts TypeLoadException to a readable string. + /// + /// TypeLoadException + /// Readable representation of the exception + public static string TypeLoadExceptionToString(ReflectionTypeLoadException ex) + { + var sb = new StringBuilder(); + foreach (var exSub in ex.LoaderExceptions) + { + sb.AppendLine(exSub.Message); + if (exSub is FileNotFoundException exFileNotFound) + { + if (!string.IsNullOrEmpty(exFileNotFound.FusionLog)) + { + sb.AppendLine("Fusion Log:"); + sb.AppendLine(exFileNotFound.FusionLog); + } + } + else if (exSub is FileLoadException exLoad) + { + if (!string.IsNullOrEmpty(exLoad.FusionLog)) + { + sb.AppendLine("Fusion Log:"); + sb.AppendLine(exLoad.FusionLog); + } + } + + sb.AppendLine(); + } + + return sb.ToString(); + } } diff --git a/BepInEx.Core/Configuration/AcceptableValueBase.cs b/BepInEx.Core/Configuration/AcceptableValueBase.cs index 810d892..e48354a 100644 --- a/BepInEx.Core/Configuration/AcceptableValueBase.cs +++ b/BepInEx.Core/Configuration/AcceptableValueBase.cs @@ -1,36 +1,35 @@ using System; -namespace BepInEx.Configuration +namespace BepInEx.Configuration; + +/// +/// Base type of all classes representing and enforcing acceptable values of config settings. +/// +public abstract class AcceptableValueBase { - /// - /// Base type of all classes representing and enforcing acceptable values of config settings. - /// - public abstract class AcceptableValueBase + /// Type of values that this class can Clamp. + protected AcceptableValueBase(Type valueType) { - /// Type of values that this class can Clamp. - protected AcceptableValueBase(Type valueType) - { - ValueType = valueType; - } - - /// - /// Type of the supported values. - /// - public Type ValueType { get; } - - /// - /// Change the value to be acceptable, if it's not already. - /// - public abstract object Clamp(object value); - - /// - /// Check if the value is an acceptable value. - /// - public abstract bool IsValid(object value); - - /// - /// Get the string for use in config files. - /// - public abstract string ToDescriptionString(); + ValueType = valueType; } + + /// + /// Type of the supported values. + /// + public Type ValueType { get; } + + /// + /// Change the value to be acceptable, if it's not already. + /// + public abstract object Clamp(object value); + + /// + /// Check if the value is an acceptable value. + /// + public abstract bool IsValid(object value); + + /// + /// Get the string for use in config files. + /// + public abstract string ToDescriptionString(); } diff --git a/BepInEx.Core/Configuration/AcceptableValueList.cs b/BepInEx.Core/Configuration/AcceptableValueList.cs index 488a2b9..5770a98 100644 --- a/BepInEx.Core/Configuration/AcceptableValueList.cs +++ b/BepInEx.Core/Configuration/AcceptableValueList.cs @@ -1,45 +1,44 @@ using System; using System.Linq; -namespace BepInEx.Configuration +namespace BepInEx.Configuration; + +/// +/// Specify the list of acceptable values for a setting. +/// +public class AcceptableValueList : AcceptableValueBase where T : IEquatable { /// /// Specify the list of acceptable values for a setting. + /// If the setting does not equal any of the values, it will be set to the first one. /// - public class AcceptableValueList : AcceptableValueBase where T : IEquatable + public AcceptableValueList(params T[] acceptableValues) : base(typeof(T)) { - /// - /// Specify the list of acceptable values for a setting. - /// If the setting does not equal any of the values, it will be set to the first one. - /// - public AcceptableValueList(params T[] acceptableValues) : base(typeof(T)) - { - if (acceptableValues == null) throw new ArgumentNullException(nameof(acceptableValues)); - if (acceptableValues.Length == 0) - throw new ArgumentException("At least one acceptable value is needed", nameof(acceptableValues)); + if (acceptableValues == null) throw new ArgumentNullException(nameof(acceptableValues)); + if (acceptableValues.Length == 0) + throw new ArgumentException("At least one acceptable value is needed", nameof(acceptableValues)); - AcceptableValues = acceptableValues; - } - - /// - /// List of values that a setting can take. - /// - public virtual T[] AcceptableValues { get; } - - /// - public override object Clamp(object value) - { - if (IsValid(value)) - return value; - - return AcceptableValues[0]; - } - - /// - public override bool IsValid(object value) => value is T v && AcceptableValues.Any(x => x.Equals(v)); - - /// - public override string ToDescriptionString() => - "# Acceptable values: " + string.Join(", ", AcceptableValues.Select(x => x.ToString()).ToArray()); + AcceptableValues = acceptableValues; } + + /// + /// List of values that a setting can take. + /// + public virtual T[] AcceptableValues { get; } + + /// + public override object Clamp(object value) + { + if (IsValid(value)) + return value; + + return AcceptableValues[0]; + } + + /// + public override bool IsValid(object value) => value is T v && AcceptableValues.Any(x => x.Equals(v)); + + /// + public override string ToDescriptionString() => + "# Acceptable values: " + string.Join(", ", AcceptableValues.Select(x => x.ToString()).ToArray()); } diff --git a/BepInEx.Core/Configuration/AcceptableValueRange.cs b/BepInEx.Core/Configuration/AcceptableValueRange.cs index b25a218..2a3ddf4 100644 --- a/BepInEx.Core/Configuration/AcceptableValueRange.cs +++ b/BepInEx.Core/Configuration/AcceptableValueRange.cs @@ -1,53 +1,52 @@ using System; -namespace BepInEx.Configuration +namespace BepInEx.Configuration; + +/// +/// Specify the range of acceptable values for a setting. +/// +public class AcceptableValueRange : AcceptableValueBase where T : IComparable { - /// - /// Specify the range of acceptable values for a setting. - /// - public class AcceptableValueRange : AcceptableValueBase where T : IComparable + /// Lowest acceptable value + /// Highest acceptable value + public AcceptableValueRange(T minValue, T maxValue) : base(typeof(T)) { - /// Lowest acceptable value - /// Highest acceptable value - public AcceptableValueRange(T minValue, T maxValue) : base(typeof(T)) - { - if (maxValue == null) - throw new ArgumentNullException(nameof(maxValue)); - if (minValue == null) - throw new ArgumentNullException(nameof(minValue)); - if (minValue.CompareTo(maxValue) >= 0) - throw new ArgumentException($"{nameof(minValue)} has to be lower than {nameof(maxValue)}"); + if (maxValue == null) + throw new ArgumentNullException(nameof(maxValue)); + if (minValue == null) + throw new ArgumentNullException(nameof(minValue)); + if (minValue.CompareTo(maxValue) >= 0) + throw new ArgumentException($"{nameof(minValue)} has to be lower than {nameof(maxValue)}"); - MinValue = minValue; - MaxValue = maxValue; - } - - /// - /// Lowest acceptable value - /// - public virtual T MinValue { get; } - - /// - /// Highest acceptable value - /// - public virtual T MaxValue { get; } - - /// - public override object Clamp(object value) - { - if (MinValue.CompareTo(value) > 0) - return MinValue; - - if (MaxValue.CompareTo(value) < 0) - return MaxValue; - - return value; - } - - /// - public override bool IsValid(object value) => MinValue.CompareTo(value) <= 0 && MaxValue.CompareTo(value) >= 0; - - /// - public override string ToDescriptionString() => $"# Acceptable value range: From {MinValue} to {MaxValue}"; + MinValue = minValue; + MaxValue = maxValue; } + + /// + /// Lowest acceptable value + /// + public virtual T MinValue { get; } + + /// + /// Highest acceptable value + /// + public virtual T MaxValue { get; } + + /// + public override object Clamp(object value) + { + if (MinValue.CompareTo(value) > 0) + return MinValue; + + if (MaxValue.CompareTo(value) < 0) + return MaxValue; + + return value; + } + + /// + public override bool IsValid(object value) => MinValue.CompareTo(value) <= 0 && MaxValue.CompareTo(value) >= 0; + + /// + public override string ToDescriptionString() => $"# Acceptable value range: From {MinValue} to {MaxValue}"; } diff --git a/BepInEx.Core/Configuration/ConfigDefinition.cs b/BepInEx.Core/Configuration/ConfigDefinition.cs index 1168c1e..d963c19 100644 --- a/BepInEx.Core/Configuration/ConfigDefinition.cs +++ b/BepInEx.Core/Configuration/ConfigDefinition.cs @@ -1,107 +1,106 @@ using System; using System.Linq; -namespace BepInEx.Configuration +namespace BepInEx.Configuration; + +/// +/// Section and key of a setting. Used as a unique key for identification within a +/// . +/// The same definition can be used in multiple config files, it will point to different settings then. +/// +/// +public class ConfigDefinition : IEquatable { + private static readonly char[] _invalidConfigChars = { '=', '\n', '\t', '\\', '"', '\'', '[', ']' }; + /// - /// Section and key of a setting. Used as a unique key for identification within a - /// . - /// The same definition can be used in multiple config files, it will point to different settings then. + /// Create a new definition. Definitions with same section and key are equal. + /// + /// Group of the setting, case sensitive. + /// Name of the setting, case sensitive. + public ConfigDefinition(string section, string key) + { + CheckInvalidConfigChars(section, nameof(section)); + CheckInvalidConfigChars(key, nameof(key)); + Key = key; + Section = section; + } + + /// + [Obsolete("description argument is no longer used, put it in a ConfigDescription instead")] + public ConfigDefinition(string section, string key, string description) + { + Key = key ?? ""; + Section = section ?? ""; + } + + /// + /// Group of the setting. All settings within a config file are grouped by this. + /// + public string Section { get; } + + /// + /// Name of the setting. + /// + public string Key { get; } + + /// + /// Check if the definitions are the same. /// /// - public class ConfigDefinition : IEquatable + public bool Equals(ConfigDefinition other) { - private static readonly char[] _invalidConfigChars = { '=', '\n', '\t', '\\', '"', '\'', '[', ']' }; - - /// - /// Create a new definition. Definitions with same section and key are equal. - /// - /// Group of the setting, case sensitive. - /// Name of the setting, case sensitive. - public ConfigDefinition(string section, string key) - { - CheckInvalidConfigChars(section, nameof(section)); - CheckInvalidConfigChars(key, nameof(key)); - Key = key; - Section = section; - } - - /// - [Obsolete("description argument is no longer used, put it in a ConfigDescription instead")] - public ConfigDefinition(string section, string key, string description) - { - Key = key ?? ""; - Section = section ?? ""; - } - - /// - /// Group of the setting. All settings within a config file are grouped by this. - /// - public string Section { get; } - - /// - /// Name of the setting. - /// - public string Key { get; } - - /// - /// Check if the definitions are the same. - /// - /// - public bool Equals(ConfigDefinition other) - { - if (other == null) return false; - return string.Equals(Key, other.Key) - && string.Equals(Section, other.Section); - } - - private static void CheckInvalidConfigChars(string val, string name) - { - if (val == null) throw new ArgumentNullException(name); - if (val != val.Trim()) - throw new ArgumentException("Cannot use whitespace characters at start or end of section and key names", - name); - if (val.Any(c => _invalidConfigChars.Contains(c))) - throw new - ArgumentException(@"Cannot use any of the following characters in section and key names: = \n \t \ "" ' [ ]", - name); - } - - /// - /// Check if the definitions are the same. - /// - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) - return false; - if (ReferenceEquals(this, obj)) - return true; - - return Equals(obj as ConfigDefinition); - } - - /// - public override int GetHashCode() - { - unchecked - { - var hashCode = Key != null ? Key.GetHashCode() : 0; - hashCode = (hashCode * 397) ^ (Section != null ? Section.GetHashCode() : 0); - return hashCode; - } - } - - /// - /// Check if the definitions are the same. - /// - public static bool operator ==(ConfigDefinition left, ConfigDefinition right) => Equals(left, right); - - /// - /// Check if the definitions are the same. - /// - public static bool operator !=(ConfigDefinition left, ConfigDefinition right) => !Equals(left, right); - - /// - public override string ToString() => Section + "." + Key; + if (other == null) return false; + return string.Equals(Key, other.Key) + && string.Equals(Section, other.Section); } + + private static void CheckInvalidConfigChars(string val, string name) + { + if (val == null) throw new ArgumentNullException(name); + if (val != val.Trim()) + throw new ArgumentException("Cannot use whitespace characters at start or end of section and key names", + name); + if (val.Any(c => _invalidConfigChars.Contains(c))) + throw new + ArgumentException(@"Cannot use any of the following characters in section and key names: = \n \t \ "" ' [ ]", + name); + } + + /// + /// Check if the definitions are the same. + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + return false; + if (ReferenceEquals(this, obj)) + return true; + + return Equals(obj as ConfigDefinition); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashCode = Key != null ? Key.GetHashCode() : 0; + hashCode = (hashCode * 397) ^ (Section != null ? Section.GetHashCode() : 0); + return hashCode; + } + } + + /// + /// Check if the definitions are the same. + /// + public static bool operator ==(ConfigDefinition left, ConfigDefinition right) => Equals(left, right); + + /// + /// Check if the definitions are the same. + /// + public static bool operator !=(ConfigDefinition left, ConfigDefinition right) => !Equals(left, right); + + /// + public override string ToString() => Section + "." + Key; } diff --git a/BepInEx.Core/Configuration/ConfigDescription.cs b/BepInEx.Core/Configuration/ConfigDescription.cs index 0cc4fdd..ce4d9f9 100644 --- a/BepInEx.Core/Configuration/ConfigDescription.cs +++ b/BepInEx.Core/Configuration/ConfigDescription.cs @@ -1,46 +1,45 @@ using System; -namespace BepInEx.Configuration +namespace BepInEx.Configuration; + +/// +/// Metadata of a . +/// +public class ConfigDescription { /// - /// Metadata of a . + /// Create a new description. /// - public class ConfigDescription + /// Text describing the function of the setting and any notes or warnings. + /// + /// Range of values that this setting can take. The setting's value will be automatically + /// clamped. + /// + /// Objects that can be used by user-made classes to add functionality. + public ConfigDescription(string description, AcceptableValueBase acceptableValues = null, params object[] tags) { - /// - /// Create a new description. - /// - /// Text describing the function of the setting and any notes or warnings. - /// - /// Range of values that this setting can take. The setting's value will be automatically - /// clamped. - /// - /// Objects that can be used by user-made classes to add functionality. - public ConfigDescription(string description, AcceptableValueBase acceptableValues = null, params object[] tags) - { - AcceptableValues = acceptableValues; - Tags = tags; - Description = description ?? throw new ArgumentNullException(nameof(description)); - } - - /// - /// Text describing the function of the setting and any notes or warnings. - /// - public string Description { get; } - - /// - /// Range of acceptable values for a setting. - /// - public AcceptableValueBase AcceptableValues { get; } - - /// - /// Objects that can be used by user-made classes to add functionality. - /// - public object[] Tags { get; } - - /// - /// An empty description. - /// - public static ConfigDescription Empty { get; } = new(""); + AcceptableValues = acceptableValues; + Tags = tags; + Description = description ?? throw new ArgumentNullException(nameof(description)); } + + /// + /// Text describing the function of the setting and any notes or warnings. + /// + public string Description { get; } + + /// + /// Range of acceptable values for a setting. + /// + public AcceptableValueBase AcceptableValues { get; } + + /// + /// Objects that can be used by user-made classes to add functionality. + /// + public object[] Tags { get; } + + /// + /// An empty description. + /// + public static ConfigDescription Empty { get; } = new(""); } diff --git a/BepInEx.Core/Configuration/ConfigEntryBase.cs b/BepInEx.Core/Configuration/ConfigEntryBase.cs index a5b822f..2f1bc5f 100644 --- a/BepInEx.Core/Configuration/ConfigEntryBase.cs +++ b/BepInEx.Core/Configuration/ConfigEntryBase.cs @@ -3,180 +3,179 @@ using System.IO; using System.Linq; using BepInEx.Logging; -namespace BepInEx.Configuration +namespace BepInEx.Configuration; + +/// +/// Provides access to a single setting inside of a . +/// +/// Type of the setting. +public sealed class ConfigEntry : ConfigEntryBase { - /// - /// Provides access to a single setting inside of a . - /// - /// Type of the setting. - public sealed class ConfigEntry : ConfigEntryBase + private T _typedValue; + + internal ConfigEntry(ConfigFile configFile, + ConfigDefinition definition, + T defaultValue, + ConfigDescription configDescription) : base(configFile, definition, typeof(T), + defaultValue, configDescription) { - private T _typedValue; - - internal ConfigEntry(ConfigFile configFile, - ConfigDefinition definition, - T defaultValue, - ConfigDescription configDescription) : base(configFile, definition, typeof(T), - defaultValue, configDescription) + configFile.SettingChanged += (sender, args) => { - configFile.SettingChanged += (sender, args) => - { - if (args.ChangedSetting == this) SettingChanged?.Invoke(sender, args); - }; - } - - /// - /// Value of this setting. - /// - public T Value - { - get => _typedValue; - set - { - value = ClampValue(value); - if (Equals(_typedValue, value)) - return; - - _typedValue = value; - OnSettingChanged(this); - } - } - - /// - public override object BoxedValue - { - get => Value; - set => Value = (T) value; - } - - /// - /// Fired when the setting is changed. Does not detect changes made outside from this object. - /// - public event EventHandler SettingChanged; + if (args.ChangedSetting == this) SettingChanged?.Invoke(sender, args); + }; } /// - /// Container for a single setting of a . - /// Each config entry is linked to one config file. + /// Value of this setting. /// - public abstract class ConfigEntryBase + public T Value { - /// - /// Types of defaultValue and definition.AcceptableValues have to be the same as settingType. - /// - internal ConfigEntryBase(ConfigFile configFile, - ConfigDefinition definition, - Type settingType, - object defaultValue, - ConfigDescription configDescription) + get => _typedValue; + set { - ConfigFile = configFile ?? throw new ArgumentNullException(nameof(configFile)); - Definition = definition ?? throw new ArgumentNullException(nameof(definition)); - SettingType = settingType ?? throw new ArgumentNullException(nameof(settingType)); + value = ClampValue(value); + if (Equals(_typedValue, value)) + return; - Description = configDescription ?? ConfigDescription.Empty; - if (Description.AcceptableValues != null && - !SettingType.IsAssignableFrom(Description.AcceptableValues.ValueType)) - throw new - ArgumentException("configDescription.AcceptableValues is for a different type than the type of this setting"); - - DefaultValue = defaultValue; - - // Free type check and automatically calls ClampValue in case AcceptableValues were provided - BoxedValue = defaultValue; + _typedValue = value; + OnSettingChanged(this); } + } - /// - /// Config file this entry is a part of. - /// - public ConfigFile ConfigFile { get; } + /// + public override object BoxedValue + { + get => Value; + set => Value = (T) value; + } - /// - /// Category and name of this setting. Used as a unique key for identification within a - /// . - /// - public ConfigDefinition Definition { get; } + /// + /// Fired when the setting is changed. Does not detect changes made outside from this object. + /// + public event EventHandler SettingChanged; +} - /// - /// Description / metadata of this setting. - /// - public ConfigDescription Description { get; } +/// +/// Container for a single setting of a . +/// Each config entry is linked to one config file. +/// +public abstract class ConfigEntryBase +{ + /// + /// Types of defaultValue and definition.AcceptableValues have to be the same as settingType. + /// + internal ConfigEntryBase(ConfigFile configFile, + ConfigDefinition definition, + Type settingType, + object defaultValue, + ConfigDescription configDescription) + { + ConfigFile = configFile ?? throw new ArgumentNullException(nameof(configFile)); + Definition = definition ?? throw new ArgumentNullException(nameof(definition)); + SettingType = settingType ?? throw new ArgumentNullException(nameof(settingType)); - /// - /// Type of the that this setting holds. - /// - public Type SettingType { get; } + Description = configDescription ?? ConfigDescription.Empty; + if (Description.AcceptableValues != null && + !SettingType.IsAssignableFrom(Description.AcceptableValues.ValueType)) + throw new + ArgumentException("configDescription.AcceptableValues is for a different type than the type of this setting"); - /// - /// Default value of this setting (set only if the setting was not changed before). - /// - public object DefaultValue { get; } + DefaultValue = defaultValue; - /// - /// Get or set the value of the setting. - /// - public abstract object BoxedValue { get; set; } + // Free type check and automatically calls ClampValue in case AcceptableValues were provided + BoxedValue = defaultValue; + } - /// - /// Get the serialized representation of the value. - /// - public string GetSerializedValue() => TomlTypeConverter.ConvertToString(BoxedValue, SettingType); + /// + /// Config file this entry is a part of. + /// + public ConfigFile ConfigFile { get; } - /// - /// Set the value by using its serialized form. - /// - public void SetSerializedValue(string value) + /// + /// Category and name of this setting. Used as a unique key for identification within a + /// . + /// + public ConfigDefinition Definition { get; } + + /// + /// Description / metadata of this setting. + /// + public ConfigDescription Description { get; } + + /// + /// Type of the that this setting holds. + /// + public Type SettingType { get; } + + /// + /// Default value of this setting (set only if the setting was not changed before). + /// + public object DefaultValue { get; } + + /// + /// Get or set the value of the setting. + /// + public abstract object BoxedValue { get; set; } + + /// + /// Get the serialized representation of the value. + /// + public string GetSerializedValue() => TomlTypeConverter.ConvertToString(BoxedValue, SettingType); + + /// + /// Set the value by using its serialized form. + /// + public void SetSerializedValue(string value) + { + try { - try - { - var newValue = TomlTypeConverter.ConvertToValue(value, SettingType); - BoxedValue = newValue; - } - catch (Exception e) - { - Logger.Log(LogLevel.Warning, - $"Config value of setting \"{Definition}\" could not be parsed and will be ignored. Reason: {e.Message}; Value: {value}"); - } + var newValue = TomlTypeConverter.ConvertToValue(value, SettingType); + BoxedValue = newValue; } - - /// - /// If necessary, clamp the value to acceptable value range. T has to be equal to settingType. - /// - protected T ClampValue(T value) + catch (Exception e) { - if (Description.AcceptableValues != null) - return (T) Description.AcceptableValues.Clamp(value); - return value; + Logger.Log(LogLevel.Warning, + $"Config value of setting \"{Definition}\" could not be parsed and will be ignored. Reason: {e.Message}; Value: {value}"); } + } - /// - /// Trigger setting changed event. - /// - protected void OnSettingChanged(object sender) => ConfigFile.OnSettingChanged(sender, this); + /// + /// If necessary, clamp the value to acceptable value range. T has to be equal to settingType. + /// + protected T ClampValue(T value) + { + if (Description.AcceptableValues != null) + return (T) Description.AcceptableValues.Clamp(value); + return value; + } - /// - /// Write a description of this setting using all available metadata. - /// - public void WriteDescription(StreamWriter writer) + /// + /// Trigger setting changed event. + /// + protected void OnSettingChanged(object sender) => ConfigFile.OnSettingChanged(sender, this); + + /// + /// Write a description of this setting using all available metadata. + /// + public void WriteDescription(StreamWriter writer) + { + if (!string.IsNullOrEmpty(Description.Description)) + writer.WriteLine($"## {Description.Description.Replace("\n", "\n## ")}"); + + writer.WriteLine($"# Setting type: {SettingType.Name}"); + + writer.WriteLine($"# Default value: {TomlTypeConverter.ConvertToString(DefaultValue, SettingType)}"); + + if (Description.AcceptableValues != null) { - if (!string.IsNullOrEmpty(Description.Description)) - writer.WriteLine($"## {Description.Description.Replace("\n", "\n## ")}"); + writer.WriteLine(Description.AcceptableValues.ToDescriptionString()); + } + else if (SettingType.IsEnum) + { + writer.WriteLine($"# Acceptable values: {string.Join(", ", Enum.GetNames(SettingType))}"); - writer.WriteLine("# Setting type: " + SettingType.Name); - - writer.WriteLine("# Default value: " + TomlTypeConverter.ConvertToString(DefaultValue, SettingType)); - - if (Description.AcceptableValues != null) - { - writer.WriteLine(Description.AcceptableValues.ToDescriptionString()); - } - else if (SettingType.IsEnum) - { - writer.WriteLine("# Acceptable values: " + string.Join(", ", Enum.GetNames(SettingType))); - - if (SettingType.GetCustomAttributes(typeof(FlagsAttribute), true).Any()) - writer.WriteLine("# Multiple values can be set at the same time by separating them with , (e.g. Debug, Warning)"); - } + if (SettingType.GetCustomAttributes(typeof(FlagsAttribute), true).Any()) + writer.WriteLine("# Multiple values can be set at the same time by separating them with , (e.g. Debug, Warning)"); } } } diff --git a/BepInEx.Core/Configuration/ConfigFile.cs b/BepInEx.Core/Configuration/ConfigFile.cs index 5f1a711..714fdbf 100644 --- a/BepInEx.Core/Configuration/ConfigFile.cs +++ b/BepInEx.Core/Configuration/ConfigFile.cs @@ -7,606 +7,605 @@ using System.Linq; using System.Text; using BepInEx.Logging; -namespace BepInEx.Configuration +namespace BepInEx.Configuration; + +/// +/// A helper class to handle persistent data. All public methods are thread-safe. +/// +public class ConfigFile : IDictionary { + private readonly BepInPlugin _ownerMetadata; + + /// + public ConfigFile(string configPath, bool saveOnInit) : this(configPath, saveOnInit, null) { } + /// - /// A helper class to handle persistent data. All public methods are thread-safe. + /// Create a new config file at the specified config path. /// - public class ConfigFile : IDictionary + /// Full path to a file that contains settings. The file will be created as needed. + /// If the config file/directory doesn't exist, create it immediately. + /// Information about the plugin that owns this setting file. + public ConfigFile(string configPath, bool saveOnInit, BepInPlugin ownerMetadata) { - private readonly BepInPlugin _ownerMetadata; + _ownerMetadata = ownerMetadata; - /// - public ConfigFile(string configPath, bool saveOnInit) : this(configPath, saveOnInit, null) { } + if (configPath == null) throw new ArgumentNullException(nameof(configPath)); + configPath = Path.GetFullPath(configPath); + ConfigFilePath = configPath; - /// - /// Create a new config file at the specified config path. - /// - /// Full path to a file that contains settings. The file will be created as needed. - /// If the config file/directory doesn't exist, create it immediately. - /// Information about the plugin that owns this setting file. - public ConfigFile(string configPath, bool saveOnInit, BepInPlugin ownerMetadata) - { - _ownerMetadata = ownerMetadata; + if (File.Exists(ConfigFilePath)) + Reload(); + else if (saveOnInit) Save(); + } - if (configPath == null) throw new ArgumentNullException(nameof(configPath)); - configPath = Path.GetFullPath(configPath); - ConfigFilePath = configPath; + public static ConfigFile CoreConfig { get; } = new(Paths.BepInExConfigPath, true); - if (File.Exists(ConfigFilePath)) - Reload(); - else if (saveOnInit) Save(); - } + /// + /// All config entries inside + /// + protected Dictionary Entries { get; } = new(); - public static ConfigFile CoreConfig { get; } = new(Paths.BepInExConfigPath, true); + private Dictionary OrphanedEntries { get; } = new(); - /// - /// All config entries inside - /// - protected Dictionary Entries { get; } = new(); - - private Dictionary OrphanedEntries { get; } = new(); - - /// - /// Create a list with all config entries inside of this config file. - /// - [Obsolete("Use Keys instead")] - public ReadOnlyCollection ConfigDefinitions - { - get - { - lock (_ioLock) - { - return Entries.Keys.ToList().AsReadOnly(); - } - } - } - - /// - /// Full path to the config file. The file might not exist until a setting is added and changed, or - /// is called. - /// - public string ConfigFilePath { get; } - - /// - /// If enabled, writes the config to disk every time a value is set. - /// If disabled, you have to manually use or the changes will be lost! - /// - public bool SaveOnConfigSet { get; set; } = true; - - /// - public ConfigEntryBase this[ConfigDefinition key] - { - get - { - lock (_ioLock) - { - return Entries[key]; - } - } - } - - /// - /// - /// - /// - public ConfigEntryBase this[string section, string key] => this[new ConfigDefinition(section, key)]; - - /// - public IEnumerator> GetEnumerator() => - // We can't really do a read lock for this - Entries.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - void ICollection>.Add( - KeyValuePair item) + /// + /// Create a list with all config entries inside of this config file. + /// + [Obsolete("Use Keys instead")] + public ReadOnlyCollection ConfigDefinitions + { + get { lock (_ioLock) { - Entries.Add(item.Key, item.Value); + return Entries.Keys.ToList().AsReadOnly(); } } + } - /// - public bool Contains(KeyValuePair item) + /// + /// Full path to the config file. The file might not exist until a setting is added and changed, or + /// is called. + /// + public string ConfigFilePath { get; } + + /// + /// If enabled, writes the config to disk every time a value is set. + /// If disabled, you have to manually use or the changes will be lost! + /// + public bool SaveOnConfigSet { get; set; } = true; + + /// + public ConfigEntryBase this[ConfigDefinition key] + { + get { lock (_ioLock) { - return ((ICollection>) Entries).Contains(item); + return Entries[key]; } } + } - void ICollection>.CopyTo( - KeyValuePair[] array, - int arrayIndex) + /// + /// + /// + /// + public ConfigEntryBase this[string section, string key] => this[new ConfigDefinition(section, key)]; + + /// + public IEnumerator> GetEnumerator() => + // We can't really do a read lock for this + Entries.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + void ICollection>.Add( + KeyValuePair item) + { + lock (_ioLock) + { + Entries.Add(item.Key, item.Value); + } + } + + /// + public bool Contains(KeyValuePair item) + { + lock (_ioLock) + { + return ((ICollection>) Entries).Contains(item); + } + } + + void ICollection>.CopyTo( + KeyValuePair[] array, + int arrayIndex) + { + lock (_ioLock) + { + ((ICollection>) Entries).CopyTo(array, arrayIndex); + } + } + + bool ICollection>.Remove( + KeyValuePair item) + { + lock (_ioLock) + { + return Entries.Remove(item.Key); + } + } + + /// + public int Count + { + get { lock (_ioLock) { - ((ICollection>) Entries).CopyTo(array, arrayIndex); + return Entries.Count; } } + } - bool ICollection>.Remove( - KeyValuePair item) + /// + public bool IsReadOnly => false; + + /// + public bool ContainsKey(ConfigDefinition key) + { + lock (_ioLock) + { + return Entries.ContainsKey(key); + } + } + + /// + public void Add(ConfigDefinition key, ConfigEntryBase value) => + throw new InvalidOperationException("Directly adding a config entry is not supported"); + + /// + public bool Remove(ConfigDefinition key) + { + lock (_ioLock) + { + return Entries.Remove(key); + } + } + + /// + public void Clear() + { + lock (_ioLock) + { + Entries.Clear(); + } + } + + bool IDictionary.TryGetValue(ConfigDefinition key, out ConfigEntryBase value) + { + lock (_ioLock) + { + return Entries.TryGetValue(key, out value); + } + } + + /// + ConfigEntryBase IDictionary.this[ConfigDefinition key] + { + get { lock (_ioLock) { - return Entries.Remove(item.Key); + return Entries[key]; } } + set => throw new InvalidOperationException("Directly setting a config entry is not supported"); + } - /// - public int Count - { - get - { - lock (_ioLock) - { - return Entries.Count; - } - } - } - - /// - public bool IsReadOnly => false; - - /// - public bool ContainsKey(ConfigDefinition key) + /// + /// Returns the ConfigDefinitions that the ConfigFile contains. + /// Creates a new array when the property is accessed. Thread-safe. + /// + public ICollection Keys + { + get { lock (_ioLock) { - return Entries.ContainsKey(key); + return Entries.Keys.ToArray(); } } + } - /// - public void Add(ConfigDefinition key, ConfigEntryBase value) => - throw new InvalidOperationException("Directly adding a config entry is not supported"); - - /// - public bool Remove(ConfigDefinition key) - { - lock (_ioLock) - { - return Entries.Remove(key); - } - } - - /// - public void Clear() - { - lock (_ioLock) - { - Entries.Clear(); - } - } - - bool IDictionary.TryGetValue(ConfigDefinition key, out ConfigEntryBase value) - { - lock (_ioLock) - { - return Entries.TryGetValue(key, out value); - } - } - - /// - ConfigEntryBase IDictionary.this[ConfigDefinition key] - { - get - { - lock (_ioLock) - { - return Entries[key]; - } - } - set => throw new InvalidOperationException("Directly setting a config entry is not supported"); - } - - /// - /// Returns the ConfigDefinitions that the ConfigFile contains. - /// Creates a new array when the property is accessed. Thread-safe. - /// - public ICollection Keys - { - get - { - lock (_ioLock) - { - return Entries.Keys.ToArray(); - } - } - } - - /// - /// Returns the ConfigEntryBase values that the ConfigFile contains. - /// Creates a new array when the property is accessed. Thread-safe. - /// - ICollection IDictionary.Values - { - get - { - lock (_ioLock) - { - return Entries.Values.ToArray(); - } - } - } - - /// - /// Create an array with all config entries inside of this config file. Should be only used for metadata purposes. - /// If you want to access and modify an existing setting then use - /// - /// instead with no description. - /// - [Obsolete("Use Values instead")] - public ConfigEntryBase[] GetConfigEntries() + /// + /// Returns the ConfigEntryBase values that the ConfigFile contains. + /// Creates a new array when the property is accessed. Thread-safe. + /// + ICollection IDictionary.Values + { + get { lock (_ioLock) { return Entries.Values.ToArray(); } } + } - #region Save/Load - - private readonly object _ioLock = new(); - - /// - /// Generate user-readable comments for each of the settings in the saved .cfg file. - /// - public bool GenerateSettingDescriptions { get; set; } = true; - - /// - /// Reloads the config from disk. Unsaved changes are lost. - /// - public void Reload() + /// + /// Create an array with all config entries inside of this config file. Should be only used for metadata purposes. + /// If you want to access and modify an existing setting then use + /// + /// instead with no description. + /// + [Obsolete("Use Values instead")] + public ConfigEntryBase[] GetConfigEntries() + { + lock (_ioLock) { - lock (_ioLock) + return Entries.Values.ToArray(); + } + } + + #region Save/Load + + private readonly object _ioLock = new(); + + /// + /// Generate user-readable comments for each of the settings in the saved .cfg file. + /// + public bool GenerateSettingDescriptions { get; set; } = true; + + /// + /// Reloads the config from disk. Unsaved changes are lost. + /// + public void Reload() + { + lock (_ioLock) + { + OrphanedEntries.Clear(); + + var currentSection = string.Empty; + + foreach (var rawLine in File.ReadAllLines(ConfigFilePath)) { - OrphanedEntries.Clear(); + var line = rawLine.Trim(); - var currentSection = string.Empty; + if (line.StartsWith("#")) //comment + continue; - foreach (var rawLine in File.ReadAllLines(ConfigFilePath)) + if (line.StartsWith("[") && line.EndsWith("]")) //section { - var line = rawLine.Trim(); - - if (line.StartsWith("#")) //comment - continue; - - if (line.StartsWith("[") && line.EndsWith("]")) //section - { - currentSection = line.Substring(1, line.Length - 2); - continue; - } - - var split = line.Split(new[] { '=' }, 2); //actual config line - if (split.Length != 2) - continue; //empty/invalid line - - var currentKey = split[0].Trim(); - var currentValue = split[1].Trim(); - - var definition = new ConfigDefinition(currentSection, currentKey); - - Entries.TryGetValue(definition, out var entry); - - if (entry != null) - entry.SetSerializedValue(currentValue); - else - OrphanedEntries[definition] = currentValue; + currentSection = line.Substring(1, line.Length - 2); + continue; } - } - OnConfigReloaded(); + var split = line.Split(new[] { '=' }, 2); //actual config line + if (split.Length != 2) + continue; //empty/invalid line + + var currentKey = split[0].Trim(); + var currentValue = split[1].Trim(); + + var definition = new ConfigDefinition(currentSection, currentKey); + + Entries.TryGetValue(definition, out var entry); + + if (entry != null) + entry.SetSerializedValue(currentValue); + else + OrphanedEntries[definition] = currentValue; + } } - /// - /// Writes the config to disk. - /// - public void Save() + OnConfigReloaded(); + } + + /// + /// Writes the config to disk. + /// + public void Save() + { + lock (_ioLock) { - lock (_ioLock) + var directoryName = Path.GetDirectoryName(ConfigFilePath); + if (directoryName != null) Directory.CreateDirectory(directoryName); + + using (var writer = new StreamWriter(ConfigFilePath, false, Utility.UTF8NoBom)) { - var directoryName = Path.GetDirectoryName(ConfigFilePath); - if (directoryName != null) Directory.CreateDirectory(directoryName); - - using (var writer = new StreamWriter(ConfigFilePath, false, Utility.UTF8NoBom)) + if (_ownerMetadata != null) { - if (_ownerMetadata != null) + writer.WriteLine($"## Settings file was created by plugin {_ownerMetadata.Name} v{_ownerMetadata.Version}"); + writer.WriteLine($"## Plugin GUID: {_ownerMetadata.GUID}"); + writer.WriteLine(); + } + + var allConfigEntries = Entries + .Select(x => new + { + x.Key, entry = x.Value, value = x.Value.GetSerializedValue() + }) + .Concat(OrphanedEntries.Select(x => new + { + x.Key, entry = (ConfigEntryBase) null, value = x.Value + })); + + foreach (var sectionKv in allConfigEntries.GroupBy(x => x.Key.Section).OrderBy(x => x.Key)) + { + // Section heading + writer.WriteLine($"[{sectionKv.Key}]"); + + foreach (var configEntry in sectionKv) { - writer.WriteLine($"## Settings file was created by plugin {_ownerMetadata.Name} v{_ownerMetadata.Version}"); - writer.WriteLine($"## Plugin GUID: {_ownerMetadata.GUID}"); - writer.WriteLine(); - } - - var allConfigEntries = Entries - .Select(x => new - { - x.Key, entry = x.Value, value = x.Value.GetSerializedValue() - }) - .Concat(OrphanedEntries.Select(x => new - { - x.Key, entry = (ConfigEntryBase) null, value = x.Value - })); - - foreach (var sectionKv in allConfigEntries.GroupBy(x => x.Key.Section).OrderBy(x => x.Key)) - { - // Section heading - writer.WriteLine($"[{sectionKv.Key}]"); - - foreach (var configEntry in sectionKv) + if (GenerateSettingDescriptions) { - if (GenerateSettingDescriptions) - { - writer.WriteLine(); - configEntry.entry?.WriteDescription(writer); - } - - writer.WriteLine($"{configEntry.Key.Key} = {configEntry.value}"); + writer.WriteLine(); + configEntry.entry?.WriteDescription(writer); } - writer.WriteLine(); + writer.WriteLine($"{configEntry.Key.Key} = {configEntry.value}"); } + + writer.WriteLine(); } } } + } - #endregion + #endregion - #region Wraps + #region Wraps - /// - /// Access one of the existing settings. If the setting has not been added yet, null is returned. - /// If the setting exists but has a different type than T, an exception is thrown. - /// New settings should be added with . - /// - /// Type of the value contained in this setting. - /// Section and Key of the setting. - [Obsolete("Use ConfigFile[key] or TryGetEntry instead")] - public ConfigEntry GetSetting(ConfigDefinition configDefinition) => - TryGetEntry(configDefinition, out var entry) - ? entry - : null; + /// + /// Access one of the existing settings. If the setting has not been added yet, null is returned. + /// If the setting exists but has a different type than T, an exception is thrown. + /// New settings should be added with . + /// + /// Type of the value contained in this setting. + /// Section and Key of the setting. + [Obsolete("Use ConfigFile[key] or TryGetEntry instead")] + public ConfigEntry GetSetting(ConfigDefinition configDefinition) => + TryGetEntry(configDefinition, out var entry) + ? entry + : null; - /// - /// Access one of the existing settings. If the setting has not been added yet, null is returned. - /// If the setting exists but has a different type than T, an exception is thrown. - /// New settings should be added with . - /// - /// Type of the value contained in this setting. - /// Section/category/group of the setting. Settings are grouped by this. - /// Name of the setting. - [Obsolete("Use ConfigFile[key] or TryGetEntry instead")] - public ConfigEntry GetSetting(string section, string key) => - TryGetEntry(section, key, out var entry) - ? entry - : null; + /// + /// Access one of the existing settings. If the setting has not been added yet, null is returned. + /// If the setting exists but has a different type than T, an exception is thrown. + /// New settings should be added with . + /// + /// Type of the value contained in this setting. + /// Section/category/group of the setting. Settings are grouped by this. + /// Name of the setting. + [Obsolete("Use ConfigFile[key] or TryGetEntry instead")] + public ConfigEntry GetSetting(string section, string key) => + TryGetEntry(section, key, out var entry) + ? entry + : null; - /// - /// Access one of the existing settings. If the setting has not been added yet, false is returned. Otherwise, true. - /// If the setting exists but has a different type than T, an exception is thrown. - /// New settings should be added with - /// . - /// - /// Type of the value contained in this setting. - /// Section and Key of the setting. - /// The ConfigEntry value to return. - public bool TryGetEntry(ConfigDefinition configDefinition, out ConfigEntry entry) + /// + /// Access one of the existing settings. If the setting has not been added yet, false is returned. Otherwise, true. + /// If the setting exists but has a different type than T, an exception is thrown. + /// New settings should be added with + /// . + /// + /// Type of the value contained in this setting. + /// Section and Key of the setting. + /// The ConfigEntry value to return. + public bool TryGetEntry(ConfigDefinition configDefinition, out ConfigEntry entry) + { + lock (_ioLock) { - lock (_ioLock) + if (Entries.TryGetValue(configDefinition, out var rawEntry)) { - if (Entries.TryGetValue(configDefinition, out var rawEntry)) - { - entry = (ConfigEntry) rawEntry; - return true; - } - - entry = null; - return false; + entry = (ConfigEntry) rawEntry; + return true; } + + entry = null; + return false; } + } - /// - /// Access one of the existing settings. If the setting has not been added yet, null is returned. - /// If the setting exists but has a different type than T, an exception is thrown. - /// New settings should be added with - /// . - /// - /// Type of the value contained in this setting. - /// Section/category/group of the setting. Settings are grouped by this. - /// Name of the setting. - /// The ConfigEntry value to return. - public bool TryGetEntry(string section, string key, out ConfigEntry entry) => - TryGetEntry(new ConfigDefinition(section, key), out entry); + /// + /// Access one of the existing settings. If the setting has not been added yet, null is returned. + /// If the setting exists but has a different type than T, an exception is thrown. + /// New settings should be added with + /// . + /// + /// Type of the value contained in this setting. + /// Section/category/group of the setting. Settings are grouped by this. + /// Name of the setting. + /// The ConfigEntry value to return. + public bool TryGetEntry(string section, string key, out ConfigEntry entry) => + TryGetEntry(new ConfigDefinition(section, key), out entry); - /// - /// Create a new setting. The setting is saved to drive and loaded automatically. - /// Each definition can be used to add only one setting, trying to add a second setting will throw an exception. - /// - /// Type of the value contained in this setting. - /// Section and Key of the setting. - /// Value of the setting if the setting was not created yet. - /// Description of the setting shown to the user and other metadata. - public ConfigEntry Bind(ConfigDefinition configDefinition, - T defaultValue, - ConfigDescription configDescription = null) + /// + /// Create a new setting. The setting is saved to drive and loaded automatically. + /// Each definition can be used to add only one setting, trying to add a second setting will throw an exception. + /// + /// Type of the value contained in this setting. + /// Section and Key of the setting. + /// Value of the setting if the setting was not created yet. + /// Description of the setting shown to the user and other metadata. + public ConfigEntry Bind(ConfigDefinition configDefinition, + T defaultValue, + ConfigDescription configDescription = null) + { + if (!TomlTypeConverter.CanConvert(typeof(T))) + throw new + ArgumentException($"Type {typeof(T)} is not supported by the config system. Supported types: {string.Join(", ", TomlTypeConverter.GetSupportedTypes().Select(x => x.Name).ToArray())}"); + + lock (_ioLock) { - if (!TomlTypeConverter.CanConvert(typeof(T))) - throw new - ArgumentException($"Type {typeof(T)} is not supported by the config system. Supported types: {string.Join(", ", TomlTypeConverter.GetSupportedTypes().Select(x => x.Name).ToArray())}"); + if (Entries.TryGetValue(configDefinition, out var rawEntry)) + return (ConfigEntry) rawEntry; - lock (_ioLock) + var entry = new ConfigEntry(this, configDefinition, defaultValue, configDescription); + + Entries[configDefinition] = entry; + + if (OrphanedEntries.TryGetValue(configDefinition, out var homelessValue)) { - if (Entries.TryGetValue(configDefinition, out var rawEntry)) - return (ConfigEntry) rawEntry; - - var entry = new ConfigEntry(this, configDefinition, defaultValue, configDescription); - - Entries[configDefinition] = entry; - - if (OrphanedEntries.TryGetValue(configDefinition, out var homelessValue)) - { - entry.SetSerializedValue(homelessValue); - OrphanedEntries.Remove(configDefinition); - } - - if (SaveOnConfigSet) - Save(); - - return entry; + entry.SetSerializedValue(homelessValue); + OrphanedEntries.Remove(configDefinition); } - } - - /// - /// Create a new setting. The setting is saved to drive and loaded automatically. - /// Each section and key pair can be used to add only one setting, trying to add a second setting will throw an - /// exception. - /// - /// Type of the value contained in this setting. - /// Section/category/group of the setting. Settings are grouped by this. - /// Name of the setting. - /// Value of the setting if the setting was not created yet. - /// Description of the setting shown to the user and other metadata. - public ConfigEntry Bind(string section, - string key, - T defaultValue, - ConfigDescription configDescription = null) => - Bind(new ConfigDefinition(section, key), defaultValue, configDescription); - - /// - /// Create a new setting. The setting is saved to drive and loaded automatically. - /// Each section and key pair can be used to add only one setting, trying to add a second setting will throw an - /// exception. - /// - /// Type of the value contained in this setting. - /// Section/category/group of the setting. Settings are grouped by this. - /// Name of the setting. - /// Value of the setting if the setting was not created yet. - /// Simple description of the setting shown to the user. - public ConfigEntry Bind(string section, string key, T defaultValue, string description) => - Bind(new ConfigDefinition(section, key), defaultValue, new ConfigDescription(description)); - - /// - /// Create a new setting. The setting is saved to drive and loaded automatically. - /// Each definition can be used to add only one setting, trying to add a second setting will throw an exception. - /// - /// Type of the value contained in this setting. - /// Section and Key of the setting. - /// Value of the setting if the setting was not created yet. - /// Description of the setting shown to the user and other metadata. - [Obsolete("Use Bind instead")] - public ConfigEntry AddSetting(ConfigDefinition configDefinition, - T defaultValue, - ConfigDescription configDescription = null) => - Bind(configDefinition, defaultValue, configDescription); - - /// - /// Create a new setting. The setting is saved to drive and loaded automatically. - /// Each section and key pair can be used to add only one setting, trying to add a second setting will throw an - /// exception. - /// - /// Type of the value contained in this setting. - /// Section/category/group of the setting. Settings are grouped by this. - /// Name of the setting. - /// Value of the setting if the setting was not created yet. - /// Description of the setting shown to the user and other metadata. - [Obsolete("Use Bind instead")] - public ConfigEntry AddSetting(string section, - string key, - T defaultValue, - ConfigDescription configDescription = null) => - Bind(new ConfigDefinition(section, key), defaultValue, configDescription); - - /// - /// Create a new setting. The setting is saved to drive and loaded automatically. - /// Each section and key pair can be used to add only one setting, trying to add a second setting will throw an - /// exception. - /// - /// Type of the value contained in this setting. - /// Section/category/group of the setting. Settings are grouped by this. - /// Name of the setting. - /// Value of the setting if the setting was not created yet. - /// Simple description of the setting shown to the user. - [Obsolete("Use Bind instead")] - public ConfigEntry AddSetting(string section, string key, T defaultValue, string description) => - Bind(new ConfigDefinition(section, key), defaultValue, new ConfigDescription(description)); - - /// - /// Access a setting. Use Bind instead. - /// - [Obsolete("Use Bind instead")] - public ConfigWrapper Wrap(string section, string key, string description = null, T defaultValue = default) - { - lock (_ioLock) - { - var definition = new ConfigDefinition(section, key, description); - var setting = Bind(definition, defaultValue, - string.IsNullOrEmpty(description) ? null : new ConfigDescription(description)); - return new ConfigWrapper(setting); - } - } - - /// - /// Access a setting. Use Bind instead. - /// - [Obsolete("Use Bind instead")] - public ConfigWrapper Wrap(ConfigDefinition configDefinition, T defaultValue = default) => - Wrap(configDefinition.Section, configDefinition.Key, null, defaultValue); - - #endregion - - #region Events - - /// - /// An event that is fired every time the config is reloaded. - /// - public event EventHandler ConfigReloaded; - - /// - /// Fired when one of the settings is changed. - /// - public event EventHandler SettingChanged; - - internal void OnSettingChanged(object sender, ConfigEntryBase changedEntryBase) - { - if (changedEntryBase == null) throw new ArgumentNullException(nameof(changedEntryBase)); if (SaveOnConfigSet) Save(); - var settingChanged = SettingChanged; - if (settingChanged == null) return; - - var args = new SettingChangedEventArgs(changedEntryBase); - foreach (EventHandler callback in settingChanged.GetInvocationList()) - try - { - callback(sender, args); - } - catch (Exception e) - { - Logger.Log(LogLevel.Error, e); - } + return entry; } - - private void OnConfigReloaded() - { - var configReloaded = ConfigReloaded; - if (configReloaded == null) return; - - foreach (EventHandler callback in configReloaded.GetInvocationList()) - try - { - callback(this, EventArgs.Empty); - } - catch (Exception e) - { - Logger.Log(LogLevel.Error, e); - } - } - - #endregion } + + /// + /// Create a new setting. The setting is saved to drive and loaded automatically. + /// Each section and key pair can be used to add only one setting, trying to add a second setting will throw an + /// exception. + /// + /// Type of the value contained in this setting. + /// Section/category/group of the setting. Settings are grouped by this. + /// Name of the setting. + /// Value of the setting if the setting was not created yet. + /// Description of the setting shown to the user and other metadata. + public ConfigEntry Bind(string section, + string key, + T defaultValue, + ConfigDescription configDescription = null) => + Bind(new ConfigDefinition(section, key), defaultValue, configDescription); + + /// + /// Create a new setting. The setting is saved to drive and loaded automatically. + /// Each section and key pair can be used to add only one setting, trying to add a second setting will throw an + /// exception. + /// + /// Type of the value contained in this setting. + /// Section/category/group of the setting. Settings are grouped by this. + /// Name of the setting. + /// Value of the setting if the setting was not created yet. + /// Simple description of the setting shown to the user. + public ConfigEntry Bind(string section, string key, T defaultValue, string description) => + Bind(new ConfigDefinition(section, key), defaultValue, new ConfigDescription(description)); + + /// + /// Create a new setting. The setting is saved to drive and loaded automatically. + /// Each definition can be used to add only one setting, trying to add a second setting will throw an exception. + /// + /// Type of the value contained in this setting. + /// Section and Key of the setting. + /// Value of the setting if the setting was not created yet. + /// Description of the setting shown to the user and other metadata. + [Obsolete("Use Bind instead")] + public ConfigEntry AddSetting(ConfigDefinition configDefinition, + T defaultValue, + ConfigDescription configDescription = null) => + Bind(configDefinition, defaultValue, configDescription); + + /// + /// Create a new setting. The setting is saved to drive and loaded automatically. + /// Each section and key pair can be used to add only one setting, trying to add a second setting will throw an + /// exception. + /// + /// Type of the value contained in this setting. + /// Section/category/group of the setting. Settings are grouped by this. + /// Name of the setting. + /// Value of the setting if the setting was not created yet. + /// Description of the setting shown to the user and other metadata. + [Obsolete("Use Bind instead")] + public ConfigEntry AddSetting(string section, + string key, + T defaultValue, + ConfigDescription configDescription = null) => + Bind(new ConfigDefinition(section, key), defaultValue, configDescription); + + /// + /// Create a new setting. The setting is saved to drive and loaded automatically. + /// Each section and key pair can be used to add only one setting, trying to add a second setting will throw an + /// exception. + /// + /// Type of the value contained in this setting. + /// Section/category/group of the setting. Settings are grouped by this. + /// Name of the setting. + /// Value of the setting if the setting was not created yet. + /// Simple description of the setting shown to the user. + [Obsolete("Use Bind instead")] + public ConfigEntry AddSetting(string section, string key, T defaultValue, string description) => + Bind(new ConfigDefinition(section, key), defaultValue, new ConfigDescription(description)); + + /// + /// Access a setting. Use Bind instead. + /// + [Obsolete("Use Bind instead")] + public ConfigWrapper Wrap(string section, string key, string description = null, T defaultValue = default) + { + lock (_ioLock) + { + var definition = new ConfigDefinition(section, key, description); + var setting = Bind(definition, defaultValue, + string.IsNullOrEmpty(description) ? null : new ConfigDescription(description)); + return new ConfigWrapper(setting); + } + } + + /// + /// Access a setting. Use Bind instead. + /// + [Obsolete("Use Bind instead")] + public ConfigWrapper Wrap(ConfigDefinition configDefinition, T defaultValue = default) => + Wrap(configDefinition.Section, configDefinition.Key, null, defaultValue); + + #endregion + + #region Events + + /// + /// An event that is fired every time the config is reloaded. + /// + public event EventHandler ConfigReloaded; + + /// + /// Fired when one of the settings is changed. + /// + public event EventHandler SettingChanged; + + internal void OnSettingChanged(object sender, ConfigEntryBase changedEntryBase) + { + if (changedEntryBase == null) throw new ArgumentNullException(nameof(changedEntryBase)); + + if (SaveOnConfigSet) + Save(); + + var settingChanged = SettingChanged; + if (settingChanged == null) return; + + var args = new SettingChangedEventArgs(changedEntryBase); + foreach (EventHandler callback in settingChanged.GetInvocationList()) + try + { + callback(sender, args); + } + catch (Exception e) + { + Logger.Log(LogLevel.Error, e); + } + } + + private void OnConfigReloaded() + { + var configReloaded = ConfigReloaded; + if (configReloaded == null) return; + + foreach (EventHandler callback in configReloaded.GetInvocationList()) + try + { + callback(this, EventArgs.Empty); + } + catch (Exception e) + { + Logger.Log(LogLevel.Error, e); + } + } + + #endregion } diff --git a/BepInEx.Core/Configuration/ConfigWrapper.cs b/BepInEx.Core/Configuration/ConfigWrapper.cs index 792c765..6ba0166 100644 --- a/BepInEx.Core/Configuration/ConfigWrapper.cs +++ b/BepInEx.Core/Configuration/ConfigWrapper.cs @@ -1,51 +1,50 @@ using System; -namespace BepInEx.Configuration +namespace BepInEx.Configuration; + +/// +/// Provides access to a single setting inside of a . +/// +/// Type of the setting. +[Obsolete("Use ConfigFile from new Bind overloads instead")] +public sealed class ConfigWrapper { - /// - /// Provides access to a single setting inside of a . - /// - /// Type of the setting. - [Obsolete("Use ConfigFile from new Bind overloads instead")] - public sealed class ConfigWrapper + internal ConfigWrapper(ConfigEntry configEntry) { - internal ConfigWrapper(ConfigEntry configEntry) + ConfigEntry = configEntry ?? throw new ArgumentNullException(nameof(configEntry)); + + configEntry.ConfigFile.SettingChanged += (sender, args) => { - ConfigEntry = configEntry ?? throw new ArgumentNullException(nameof(configEntry)); - - configEntry.ConfigFile.SettingChanged += (sender, args) => - { - if (args.ChangedSetting == configEntry) SettingChanged?.Invoke(sender, args); - }; - } - - /// - /// Entry of this setting in the . - /// - public ConfigEntry ConfigEntry { get; } - - /// - /// Unique definition of this setting. - /// - public ConfigDefinition Definition => ConfigEntry.Definition; - - /// - /// Config file this setting is inside of. - /// - public ConfigFile ConfigFile => ConfigEntry.ConfigFile; - - /// - /// Value of this setting. - /// - public T Value - { - get => ConfigEntry.Value; - set => ConfigEntry.Value = value; - } - - /// - /// Fired when the setting is changed. Does not detect changes made outside from this object. - /// - public event EventHandler SettingChanged; + if (args.ChangedSetting == configEntry) SettingChanged?.Invoke(sender, args); + }; } + + /// + /// Entry of this setting in the . + /// + public ConfigEntry ConfigEntry { get; } + + /// + /// Unique definition of this setting. + /// + public ConfigDefinition Definition => ConfigEntry.Definition; + + /// + /// Config file this setting is inside of. + /// + public ConfigFile ConfigFile => ConfigEntry.ConfigFile; + + /// + /// Value of this setting. + /// + public T Value + { + get => ConfigEntry.Value; + set => ConfigEntry.Value = value; + } + + /// + /// Fired when the setting is changed. Does not detect changes made outside from this object. + /// + public event EventHandler SettingChanged; } diff --git a/BepInEx.Core/Configuration/SettingChangedEventArgs.cs b/BepInEx.Core/Configuration/SettingChangedEventArgs.cs index 9beffa0..7e167ec 100644 --- a/BepInEx.Core/Configuration/SettingChangedEventArgs.cs +++ b/BepInEx.Core/Configuration/SettingChangedEventArgs.cs @@ -1,22 +1,21 @@ using System; -namespace BepInEx.Configuration -{ - /// - /// Arguments for events concerning a change of a setting. - /// - /// - public sealed class SettingChangedEventArgs : EventArgs - { - /// - public SettingChangedEventArgs(ConfigEntryBase changedSetting) - { - ChangedSetting = changedSetting; - } +namespace BepInEx.Configuration; - /// - /// Setting that was changed - /// - public ConfigEntryBase ChangedSetting { get; } +/// +/// Arguments for events concerning a change of a setting. +/// +/// +public sealed class SettingChangedEventArgs : EventArgs +{ + /// + public SettingChangedEventArgs(ConfigEntryBase changedSetting) + { + ChangedSetting = changedSetting; } + + /// + /// Setting that was changed + /// + public ConfigEntryBase ChangedSetting { get; } } diff --git a/BepInEx.Core/Configuration/TomlTypeConverter.cs b/BepInEx.Core/Configuration/TomlTypeConverter.cs index eaf8315..779cb67 100644 --- a/BepInEx.Core/Configuration/TomlTypeConverter.cs +++ b/BepInEx.Core/Configuration/TomlTypeConverter.cs @@ -5,288 +5,287 @@ using System.Text; using System.Text.RegularExpressions; using BepInEx.Logging; -namespace BepInEx.Configuration +namespace BepInEx.Configuration; + +/// +/// Serializer/deserializer used by the config system. +/// +public static class TomlTypeConverter { - /// - /// Serializer/deserializer used by the config system. - /// - public static class TomlTypeConverter + // Don't put anything from UnityEngine here or it will break preloader, use LazyTomlConverterLoader instead + private static Dictionary TypeConverters { get; } = new() { - // Don't put anything from UnityEngine here or it will break preloader, use LazyTomlConverterLoader instead - private static Dictionary TypeConverters { get; } = new() + [typeof(string)] = new TypeConverter { - [typeof(string)] = new TypeConverter + ConvertToString = (obj, type) => Escape((string) obj), + ConvertToObject = (str, type) => { - ConvertToString = (obj, type) => Escape((string) obj), - ConvertToObject = (str, type) => - { - // Check if the string is a file path with unescaped \ path separators (e.g. D:\test and not D:\\test) - if (Regex.IsMatch(str, @"^""?\w:\\(?!\\)(?!.+\\\\)")) - return str; - return Unescape(str); - } - }, - [typeof(bool)] = new TypeConverter - { - ConvertToString = (obj, type) => obj.ToString().ToLowerInvariant(), - ConvertToObject = (str, type) => bool.Parse(str) - }, - [typeof(byte)] = new TypeConverter - { - ConvertToString = (obj, type) => obj.ToString(), - ConvertToObject = (str, type) => byte.Parse(str) - }, - - //integral types - - [typeof(sbyte)] = new TypeConverter - { - ConvertToString = (obj, type) => obj.ToString(), - ConvertToObject = (str, type) => sbyte.Parse(str) - }, - [typeof(byte)] = new TypeConverter - { - ConvertToString = (obj, type) => obj.ToString(), - ConvertToObject = (str, type) => byte.Parse(str) - }, - [typeof(short)] = new TypeConverter - { - ConvertToString = (obj, type) => obj.ToString(), - ConvertToObject = (str, type) => short.Parse(str) - }, - [typeof(ushort)] = new TypeConverter - { - ConvertToString = (obj, type) => obj.ToString(), - ConvertToObject = (str, type) => ushort.Parse(str) - }, - [typeof(int)] = new TypeConverter - { - ConvertToString = (obj, type) => obj.ToString(), - ConvertToObject = (str, type) => int.Parse(str) - }, - [typeof(uint)] = new TypeConverter - { - ConvertToString = (obj, type) => obj.ToString(), - ConvertToObject = (str, type) => uint.Parse(str) - }, - [typeof(long)] = new TypeConverter - { - ConvertToString = (obj, type) => obj.ToString(), - ConvertToObject = (str, type) => long.Parse(str) - }, - [typeof(ulong)] = new TypeConverter - { - ConvertToString = (obj, type) => obj.ToString(), - ConvertToObject = (str, type) => ulong.Parse(str) - }, - - //floating point types - - [typeof(float)] = new TypeConverter - { - ConvertToString = (obj, type) => ((float) obj).ToString(NumberFormatInfo.InvariantInfo), - ConvertToObject = (str, type) => float.Parse(str, NumberFormatInfo.InvariantInfo) - }, - [typeof(double)] = new TypeConverter - { - ConvertToString = (obj, type) => ((double) obj).ToString(NumberFormatInfo.InvariantInfo), - ConvertToObject = (str, type) => double.Parse(str, NumberFormatInfo.InvariantInfo) - }, - [typeof(decimal)] = new TypeConverter - { - ConvertToString = (obj, type) => ((decimal) obj).ToString(NumberFormatInfo.InvariantInfo), - ConvertToObject = (str, type) => decimal.Parse(str, NumberFormatInfo.InvariantInfo) - }, - - //enums are special - - [typeof(Enum)] = new TypeConverter - { - ConvertToString = (obj, type) => obj.ToString(), - ConvertToObject = (str, type) => Enum.Parse(type, str, true) + // Check if the string is a file path with unescaped \ path separators (e.g. D:\test and not D:\\test) + if (Regex.IsMatch(str, @"^""?\w:\\(?!\\)(?!.+\\\\)")) + return str; + return Unescape(str); } - }; - - /// - /// Convert object of a given type to a string using available converters. - /// - public static string ConvertToString(object value, Type valueType) + }, + [typeof(bool)] = new TypeConverter { - var conv = GetConverter(valueType); - if (conv == null) - throw new InvalidOperationException($"Cannot convert from type {valueType}"); + ConvertToString = (obj, type) => obj.ToString().ToLowerInvariant(), + ConvertToObject = (str, type) => bool.Parse(str) + }, + [typeof(byte)] = new TypeConverter + { + ConvertToString = (obj, type) => obj.ToString(), + ConvertToObject = (str, type) => byte.Parse(str) + }, - return conv.ConvertToString(value, valueType); + //integral types + + [typeof(sbyte)] = new TypeConverter + { + ConvertToString = (obj, type) => obj.ToString(), + ConvertToObject = (str, type) => sbyte.Parse(str) + }, + [typeof(byte)] = new TypeConverter + { + ConvertToString = (obj, type) => obj.ToString(), + ConvertToObject = (str, type) => byte.Parse(str) + }, + [typeof(short)] = new TypeConverter + { + ConvertToString = (obj, type) => obj.ToString(), + ConvertToObject = (str, type) => short.Parse(str) + }, + [typeof(ushort)] = new TypeConverter + { + ConvertToString = (obj, type) => obj.ToString(), + ConvertToObject = (str, type) => ushort.Parse(str) + }, + [typeof(int)] = new TypeConverter + { + ConvertToString = (obj, type) => obj.ToString(), + ConvertToObject = (str, type) => int.Parse(str) + }, + [typeof(uint)] = new TypeConverter + { + ConvertToString = (obj, type) => obj.ToString(), + ConvertToObject = (str, type) => uint.Parse(str) + }, + [typeof(long)] = new TypeConverter + { + ConvertToString = (obj, type) => obj.ToString(), + ConvertToObject = (str, type) => long.Parse(str) + }, + [typeof(ulong)] = new TypeConverter + { + ConvertToString = (obj, type) => obj.ToString(), + ConvertToObject = (str, type) => ulong.Parse(str) + }, + + //floating point types + + [typeof(float)] = new TypeConverter + { + ConvertToString = (obj, type) => ((float) obj).ToString(NumberFormatInfo.InvariantInfo), + ConvertToObject = (str, type) => float.Parse(str, NumberFormatInfo.InvariantInfo) + }, + [typeof(double)] = new TypeConverter + { + ConvertToString = (obj, type) => ((double) obj).ToString(NumberFormatInfo.InvariantInfo), + ConvertToObject = (str, type) => double.Parse(str, NumberFormatInfo.InvariantInfo) + }, + [typeof(decimal)] = new TypeConverter + { + ConvertToString = (obj, type) => ((decimal) obj).ToString(NumberFormatInfo.InvariantInfo), + ConvertToObject = (str, type) => decimal.Parse(str, NumberFormatInfo.InvariantInfo) + }, + + //enums are special + + [typeof(Enum)] = new TypeConverter + { + ConvertToString = (obj, type) => obj.ToString(), + ConvertToObject = (str, type) => Enum.Parse(type, str, true) + } + }; + + /// + /// Convert object of a given type to a string using available converters. + /// + public static string ConvertToString(object value, Type valueType) + { + var conv = GetConverter(valueType); + if (conv == null) + throw new InvalidOperationException($"Cannot convert from type {valueType}"); + + return conv.ConvertToString(value, valueType); + } + + /// + /// Convert string to an object of a given type using available converters. + /// + public static T ConvertToValue(string value) => (T) ConvertToValue(value, typeof(T)); + + /// + /// Convert string to an object of a given type using available converters. + /// + public static object ConvertToValue(string value, Type valueType) + { + var conv = GetConverter(valueType); + if (conv == null) + throw new InvalidOperationException($"Cannot convert to type {valueType.Name}"); + + return conv.ConvertToObject(value, valueType); + } + + /// + /// Get a converter for a given type if there is any. + /// + public static TypeConverter GetConverter(Type valueType) + { + if (valueType == null) + throw new ArgumentNullException(nameof(valueType)); + + if (valueType.IsEnum) + return TypeConverters[typeof(Enum)]; + + TypeConverters.TryGetValue(valueType, out var result); + + return result; + } + + /// + /// Add a new type converter for a given type. + /// If a different converter is already added, this call is ignored and false is returned. + /// + public static bool AddConverter(Type type, TypeConverter converter) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + if (converter == null) throw new ArgumentNullException(nameof(converter)); + if (CanConvert(type)) + { + Logger.LogWarning("Tried to add a TomlConverter when one already exists for type " + type.FullName); + return false; } - /// - /// Convert string to an object of a given type using available converters. - /// - public static T ConvertToValue(string value) => (T) ConvertToValue(value, typeof(T)); + TypeConverters.Add(type, converter); + return true; + } - /// - /// Convert string to an object of a given type using available converters. - /// - public static object ConvertToValue(string value, Type valueType) - { - var conv = GetConverter(valueType); - if (conv == null) - throw new InvalidOperationException($"Cannot convert to type {valueType.Name}"); + /// + /// Check if a given type can be converted to and from string. + /// + public static bool CanConvert(Type type) => GetConverter(type) != null; - return conv.ConvertToObject(value, valueType); - } + /// + /// Give a list of types with registered converters. + /// + public static IEnumerable GetSupportedTypes() => TypeConverters.Keys; - /// - /// Get a converter for a given type if there is any. - /// - public static TypeConverter GetConverter(Type valueType) - { - if (valueType == null) - throw new ArgumentNullException(nameof(valueType)); + private static string Escape(this string txt) + { + if (string.IsNullOrEmpty(txt)) return string.Empty; - if (valueType.IsEnum) - return TypeConverters[typeof(Enum)]; - - TypeConverters.TryGetValue(valueType, out var result); - - return result; - } - - /// - /// Add a new type converter for a given type. - /// If a different converter is already added, this call is ignored and false is returned. - /// - public static bool AddConverter(Type type, TypeConverter converter) - { - if (type == null) throw new ArgumentNullException(nameof(type)); - if (converter == null) throw new ArgumentNullException(nameof(converter)); - if (CanConvert(type)) + var stringBuilder = new StringBuilder(txt.Length + 2); + foreach (var c in txt) + switch (c) { - Logger.LogWarning("Tried to add a TomlConverter when one already exists for type " + type.FullName); - return false; - } - - TypeConverters.Add(type, converter); - return true; - } - - /// - /// Check if a given type can be converted to and from string. - /// - public static bool CanConvert(Type type) => GetConverter(type) != null; - - /// - /// Give a list of types with registered converters. - /// - public static IEnumerable GetSupportedTypes() => TypeConverters.Keys; - - private static string Escape(this string txt) - { - if (string.IsNullOrEmpty(txt)) return string.Empty; - - var stringBuilder = new StringBuilder(txt.Length + 2); - foreach (var c in txt) - switch (c) - { - case '\0': - stringBuilder.Append(@"\0"); - break; - case '\a': - stringBuilder.Append(@"\a"); - break; - case '\b': - stringBuilder.Append(@"\b"); - break; - case '\t': - stringBuilder.Append(@"\t"); - break; - case '\n': - stringBuilder.Append(@"\n"); - break; - case '\v': - stringBuilder.Append(@"\v"); - break; - case '\f': - stringBuilder.Append(@"\f"); - break; - case '\r': - stringBuilder.Append(@"\r"); - break; - case '\'': - stringBuilder.Append(@"\'"); - break; - case '\\': - stringBuilder.Append(@"\"); - break; - case '\"': - stringBuilder.Append(@"\"""); - break; - default: - stringBuilder.Append(c); - break; - } - - return stringBuilder.ToString(); - } - - private static string Unescape(this string txt) - { - if (string.IsNullOrEmpty(txt)) - return txt; - var stringBuilder = new StringBuilder(txt.Length); - for (var i = 0; i < txt.Length;) - { - var num = txt.IndexOf('\\', i); - if (num < 0 || num == txt.Length - 1) - num = txt.Length; - stringBuilder.Append(txt, i, num - i); - if (num >= txt.Length) + case '\0': + stringBuilder.Append(@"\0"); + break; + case '\a': + stringBuilder.Append(@"\a"); + break; + case '\b': + stringBuilder.Append(@"\b"); + break; + case '\t': + stringBuilder.Append(@"\t"); + break; + case '\n': + stringBuilder.Append(@"\n"); + break; + case '\v': + stringBuilder.Append(@"\v"); + break; + case '\f': + stringBuilder.Append(@"\f"); + break; + case '\r': + stringBuilder.Append(@"\r"); + break; + case '\'': + stringBuilder.Append(@"\'"); + break; + case '\\': + stringBuilder.Append(@"\"); + break; + case '\"': + stringBuilder.Append(@"\"""); + break; + default: + stringBuilder.Append(c); break; - var c = txt[num + 1]; - switch (c) - { - case '0': - stringBuilder.Append('\0'); - break; - case 'a': - stringBuilder.Append('\a'); - break; - case 'b': - stringBuilder.Append('\b'); - break; - case 't': - stringBuilder.Append('\t'); - break; - case 'n': - stringBuilder.Append('\n'); - break; - case 'v': - stringBuilder.Append('\v'); - break; - case 'f': - stringBuilder.Append('\f'); - break; - case 'r': - stringBuilder.Append('\r'); - break; - case '\'': - stringBuilder.Append('\''); - break; - case '\"': - stringBuilder.Append('\"'); - break; - case '\\': - stringBuilder.Append('\\'); - break; - default: - stringBuilder.Append('\\').Append(c); - break; - } - - i = num + 2; } - return stringBuilder.ToString(); + return stringBuilder.ToString(); + } + + private static string Unescape(this string txt) + { + if (string.IsNullOrEmpty(txt)) + return txt; + var stringBuilder = new StringBuilder(txt.Length); + for (var i = 0; i < txt.Length;) + { + var num = txt.IndexOf('\\', i); + if (num < 0 || num == txt.Length - 1) + num = txt.Length; + stringBuilder.Append(txt, i, num - i); + if (num >= txt.Length) + break; + var c = txt[num + 1]; + switch (c) + { + case '0': + stringBuilder.Append('\0'); + break; + case 'a': + stringBuilder.Append('\a'); + break; + case 'b': + stringBuilder.Append('\b'); + break; + case 't': + stringBuilder.Append('\t'); + break; + case 'n': + stringBuilder.Append('\n'); + break; + case 'v': + stringBuilder.Append('\v'); + break; + case 'f': + stringBuilder.Append('\f'); + break; + case 'r': + stringBuilder.Append('\r'); + break; + case '\'': + stringBuilder.Append('\''); + break; + case '\"': + stringBuilder.Append('\"'); + break; + case '\\': + stringBuilder.Append('\\'); + break; + default: + stringBuilder.Append('\\').Append(c); + break; + } + + i = num + 2; } + + return stringBuilder.ToString(); } } diff --git a/BepInEx.Core/Configuration/TypeConverter.cs b/BepInEx.Core/Configuration/TypeConverter.cs index 833f401..71a92c8 100644 --- a/BepInEx.Core/Configuration/TypeConverter.cs +++ b/BepInEx.Core/Configuration/TypeConverter.cs @@ -1,22 +1,21 @@ using System; -namespace BepInEx.Configuration +namespace BepInEx.Configuration; + +/// +/// A serializer/deserializer combo for some type(s). Used by the config system. +/// +public class TypeConverter { /// - /// A serializer/deserializer combo for some type(s). Used by the config system. + /// Used to serialize the type into a (hopefully) human-readable string. + /// Object is the instance to serialize, Type is the object's type. /// - public class TypeConverter - { - /// - /// Used to serialize the type into a (hopefully) human-readable string. - /// Object is the instance to serialize, Type is the object's type. - /// - public Func ConvertToString { get; set; } + public Func ConvertToString { get; set; } - /// - /// Used to deserialize the type from a string. - /// String is the data to deserialize, Type is the object's type, should return instance to an object of Type. - /// - public Func ConvertToObject { get; set; } - } + /// + /// Used to deserialize the type from a string. + /// String is the data to deserialize, Type is the object's type, should return instance to an object of Type. + /// + public Func ConvertToObject { get; set; } } diff --git a/BepInEx.Core/Console/ConsoleManager.cs b/BepInEx.Core/Console/ConsoleManager.cs index c054448..9470aec 100644 --- a/BepInEx.Core/Console/ConsoleManager.cs +++ b/BepInEx.Core/Console/ConsoleManager.cs @@ -6,153 +6,152 @@ using BepInEx.Configuration; using BepInEx.Unix; using MonoMod.Utils; -namespace BepInEx +namespace BepInEx; + +public static class ConsoleManager { - public static class ConsoleManager + public enum ConsoleOutRedirectType { - public enum ConsoleOutRedirectType + [Description("Auto")] + Auto = 0, + + [Description("Console Out")] + ConsoleOut, + + [Description("Standard Out")] + StandardOut + } + + private const uint SHIFT_JIS_CP = 932; + + private const string ENABLE_CONSOLE_ARG = "--enable-console"; + + public static readonly ConfigEntry ConfigConsoleEnabled = ConfigFile.CoreConfig.Bind( + "Logging.Console", "Enabled", + false, + "Enables showing a console for log output."); + + public static readonly ConfigEntry ConfigPreventClose = ConfigFile.CoreConfig.Bind( + "Logging.Console", "PreventClose", + false, + "If enabled, will prevent closing the console (either by deleting the close button or in other platform-specific way)."); + + public static readonly ConfigEntry ConfigConsoleShiftJis = ConfigFile.CoreConfig.Bind( + "Logging.Console", "ShiftJisEncoding", + false, + "If true, console is set to the Shift-JIS encoding, otherwise UTF-8 encoding."); + + public static readonly ConfigEntry ConfigConsoleOutRedirectType = + ConfigFile.CoreConfig.Bind( + "Logging.Console", "StandardOutType", + ConsoleOutRedirectType.Auto, + new StringBuilder() + .AppendLine("Hints console manager on what handle to assign as StandardOut. Possible values:") + .AppendLine("Auto - lets BepInEx decide how to redirect console output") + .AppendLine("ConsoleOut - prefer redirecting to console output; if possible, closes original standard output") + .AppendLine("StandardOut - prefer redirecting to standard output; if possible, closes console out") + .ToString() + ); + + private static readonly bool? EnableConsoleArgOverride; + + static ConsoleManager() + { + // Ensure GetCommandLineArgs failing (e.g. on unix) does not kill bepin + try { - [Description("Auto")] - Auto = 0, - - [Description("Console Out")] - ConsoleOut, - - [Description("Standard Out")] - StandardOut - } - - private const uint SHIFT_JIS_CP = 932; - - private const string ENABLE_CONSOLE_ARG = "--enable-console"; - - public static readonly ConfigEntry ConfigConsoleEnabled = ConfigFile.CoreConfig.Bind( - "Logging.Console", "Enabled", - false, - "Enables showing a console for log output."); - - public static readonly ConfigEntry ConfigPreventClose = ConfigFile.CoreConfig.Bind( - "Logging.Console", "PreventClose", - false, - "If enabled, will prevent closing the console (either by deleting the close button or in other platform-specific way)."); - - public static readonly ConfigEntry ConfigConsoleShiftJis = ConfigFile.CoreConfig.Bind( - "Logging.Console", "ShiftJisEncoding", - false, - "If true, console is set to the Shift-JIS encoding, otherwise UTF-8 encoding."); - - public static readonly ConfigEntry ConfigConsoleOutRedirectType = - ConfigFile.CoreConfig.Bind( - "Logging.Console", "StandardOutType", - ConsoleOutRedirectType.Auto, - new StringBuilder() - .AppendLine("Hints console manager on what handle to assign as StandardOut. Possible values:") - .AppendLine("Auto - lets BepInEx decide how to redirect console output") - .AppendLine("ConsoleOut - prefer redirecting to console output; if possible, closes original standard output") - .AppendLine("StandardOut - prefer redirecting to standard output; if possible, closes console out") - .ToString() - ); - - private static readonly bool? EnableConsoleArgOverride; - - static ConsoleManager() - { - // Ensure GetCommandLineArgs failing (e.g. on unix) does not kill bepin - try + var args = Environment.GetCommandLineArgs(); + for (var i = 0; i < args.Length; i++) { - var args = Environment.GetCommandLineArgs(); - for (var i = 0; i < args.Length; i++) - { - var res = args[i]; - if (res == ENABLE_CONSOLE_ARG && i + 1 < args.Length && bool.TryParse(args[i + 1], out var enable)) - EnableConsoleArgOverride = enable; - } - } - catch (Exception) - { - // Skip + var res = args[i]; + if (res == ENABLE_CONSOLE_ARG && i + 1 < args.Length && bool.TryParse(args[i + 1], out var enable)) + EnableConsoleArgOverride = enable; } } - - public static bool ConsoleEnabled => EnableConsoleArgOverride ?? ConfigConsoleEnabled.Value; - - internal static IConsoleDriver Driver { get; set; } - - /// - /// True if an external console has been started, false otherwise. - /// - public static bool ConsoleActive => Driver?.ConsoleActive ?? false; - - /// - /// The stream that writes to the standard out stream of the process. Should never be null. - /// - public static TextWriter StandardOutStream => Driver?.StandardOut; - - /// - /// The stream that writes to an external console. Null if no such console exists - /// - public static TextWriter ConsoleStream => Driver?.ConsoleOut; - - - public static void Initialize(bool alreadyActive) + catch (Exception) { - if (PlatformHelper.Is(Platform.Unix)) - Driver = new LinuxConsoleDriver(); - else if (PlatformHelper.Is(Platform.Windows)) - Driver = new WindowsConsoleDriver(); - else - throw new PlatformNotSupportedException("Was unable to determine console driver for platform " + - PlatformHelper.Current); - - Driver.Initialize(alreadyActive); - } - - private static void DriverCheck() - { - if (Driver == null) - throw new InvalidOperationException("Driver has not been initialized"); - } - - public static void CreateConsole() - { - if (ConsoleActive) - return; - - DriverCheck(); - - // Apparently some versions of Mono throw a "Encoding name 'xxx' not supported" - // if you use Encoding.GetEncoding - // That's why we use of codepages directly and handle then in console drivers separately - var codepage = ConfigConsoleShiftJis.Value ? SHIFT_JIS_CP : (uint)Encoding.UTF8.CodePage; - - Driver.CreateConsole(codepage); - - if (ConfigPreventClose.Value) - Driver.PreventClose(); - } - - public static void DetachConsole() - { - if (!ConsoleActive) - return; - - DriverCheck(); - - Driver.DetachConsole(); - } - - public static void SetConsoleTitle(string title) - { - DriverCheck(); - - Driver.SetConsoleTitle(title); - } - - public static void SetConsoleColor(ConsoleColor color) - { - DriverCheck(); - - Driver.SetConsoleColor(color); + // Skip } } + + public static bool ConsoleEnabled => EnableConsoleArgOverride ?? ConfigConsoleEnabled.Value; + + internal static IConsoleDriver Driver { get; set; } + + /// + /// True if an external console has been started, false otherwise. + /// + public static bool ConsoleActive => Driver?.ConsoleActive ?? false; + + /// + /// The stream that writes to the standard out stream of the process. Should never be null. + /// + public static TextWriter StandardOutStream => Driver?.StandardOut; + + /// + /// The stream that writes to an external console. Null if no such console exists + /// + public static TextWriter ConsoleStream => Driver?.ConsoleOut; + + + public static void Initialize(bool alreadyActive) + { + if (PlatformHelper.Is(Platform.Unix)) + Driver = new LinuxConsoleDriver(); + else if (PlatformHelper.Is(Platform.Windows)) + Driver = new WindowsConsoleDriver(); + else + throw new PlatformNotSupportedException("Was unable to determine console driver for platform " + + PlatformHelper.Current); + + Driver.Initialize(alreadyActive); + } + + private static void DriverCheck() + { + if (Driver == null) + throw new InvalidOperationException("Driver has not been initialized"); + } + + public static void CreateConsole() + { + if (ConsoleActive) + return; + + DriverCheck(); + + // Apparently some versions of Mono throw a "Encoding name 'xxx' not supported" + // if you use Encoding.GetEncoding + // That's why we use of codepages directly and handle then in console drivers separately + var codepage = ConfigConsoleShiftJis.Value ? SHIFT_JIS_CP : (uint) Encoding.UTF8.CodePage; + + Driver.CreateConsole(codepage); + + if (ConfigPreventClose.Value) + Driver.PreventClose(); + } + + public static void DetachConsole() + { + if (!ConsoleActive) + return; + + DriverCheck(); + + Driver.DetachConsole(); + } + + public static void SetConsoleTitle(string title) + { + DriverCheck(); + + Driver.SetConsoleTitle(title); + } + + public static void SetConsoleColor(ConsoleColor color) + { + DriverCheck(); + + Driver.SetConsoleColor(color); + } } diff --git a/BepInEx.Core/Console/IConsoleDriver.cs b/BepInEx.Core/Console/IConsoleDriver.cs index a9561d5..0015610 100644 --- a/BepInEx.Core/Console/IConsoleDriver.cs +++ b/BepInEx.Core/Console/IConsoleDriver.cs @@ -1,27 +1,26 @@ using System; using System.IO; -namespace BepInEx +namespace BepInEx; + +internal interface IConsoleDriver { - internal interface IConsoleDriver - { - TextWriter StandardOut { get; } - TextWriter ConsoleOut { get; } + TextWriter StandardOut { get; } + TextWriter ConsoleOut { get; } - bool ConsoleActive { get; } - bool ConsoleIsExternal { get; } + bool ConsoleActive { get; } + bool ConsoleIsExternal { get; } - void PreventClose(); + void PreventClose(); - void Initialize(bool alreadyActive); + void Initialize(bool alreadyActive); - // Apparently Windows code-pages work in Mono. - // https://stackoverflow.com/a/33456543 - void CreateConsole(uint codepage); - void DetachConsole(); + // Apparently Windows code-pages work in Mono. + // https://stackoverflow.com/a/33456543 + void CreateConsole(uint codepage); + void DetachConsole(); - void SetConsoleColor(ConsoleColor color); + void SetConsoleColor(ConsoleColor color); - void SetConsoleTitle(string title); - } + void SetConsoleTitle(string title); } diff --git a/BepInEx.Core/Console/SafeConsole.cs b/BepInEx.Core/Console/SafeConsole.cs index 279080c..9f50b5b 100644 --- a/BepInEx.Core/Console/SafeConsole.cs +++ b/BepInEx.Core/Console/SafeConsole.cs @@ -6,100 +6,99 @@ using System; using System.Reflection; -namespace UnityInjector.ConsoleUtil +namespace UnityInjector.ConsoleUtil; + +/// +/// Console class with safe handlers for Unity 4.x, which does not have a proper Console implementation +/// +internal static class SafeConsole { - /// - /// Console class with safe handlers for Unity 4.x, which does not have a proper Console implementation - /// - internal static class SafeConsole + private static GetColorDelegate _getBackgroundColor; + private static SetColorDelegate _setBackgroundColor; + + private static GetColorDelegate _getForegroundColor; + private static SetColorDelegate _setForegroundColor; + + private static GetStringDelegate _getTitle; + private static SetStringDelegate _setTitle; + + static SafeConsole() { - private static GetColorDelegate _getBackgroundColor; - private static SetColorDelegate _setBackgroundColor; - - private static GetColorDelegate _getForegroundColor; - private static SetColorDelegate _setForegroundColor; - - private static GetStringDelegate _getTitle; - private static SetStringDelegate _setTitle; - - static SafeConsole() - { - var tConsole = typeof(Console); - InitColors(tConsole); - } - - public static bool BackgroundColorExists { get; private set; } - - public static ConsoleColor BackgroundColor - { - get => _getBackgroundColor(); - set => _setBackgroundColor(value); - } - - public static bool ForegroundColorExists { get; private set; } - - public static ConsoleColor ForegroundColor - { - get => _getForegroundColor(); - set => _setForegroundColor(value); - } - - public static bool TitleExists { get; private set; } - - public static string Title - { - get => _getTitle(); - set => _setTitle(value); - } - - private static void InitColors(Type tConsole) - { - const BindingFlags BINDING_FLAGS = BindingFlags.Public | BindingFlags.Static; - - var gfc = tConsole.GetMethod("get_ForegroundColor", BINDING_FLAGS); - var sfc = tConsole.GetMethod("set_ForegroundColor", BINDING_FLAGS); - - var gbc = tConsole.GetMethod("get_BackgroundColor", BINDING_FLAGS); - var sbc = tConsole.GetMethod("set_BackgroundColor", BINDING_FLAGS); - - var gtt = tConsole.GetMethod("get_Title", BINDING_FLAGS); - var stt = tConsole.GetMethod("set_Title", BINDING_FLAGS); - - _setForegroundColor = sfc != null - ? (SetColorDelegate) Delegate.CreateDelegate(typeof(SetColorDelegate), sfc) - : value => { }; - - _setBackgroundColor = sbc != null - ? (SetColorDelegate) Delegate.CreateDelegate(typeof(SetColorDelegate), sbc) - : value => { }; - - _getForegroundColor = gfc != null - ? (GetColorDelegate) Delegate.CreateDelegate(typeof(GetColorDelegate), gfc) - : () => ConsoleColor.Gray; - - _getBackgroundColor = gbc != null - ? (GetColorDelegate) Delegate.CreateDelegate(typeof(GetColorDelegate), gbc) - : () => ConsoleColor.Black; - - _getTitle = gtt != null - ? (GetStringDelegate) Delegate.CreateDelegate(typeof(GetStringDelegate), gtt) - : () => string.Empty; - - _setTitle = stt != null - ? (SetStringDelegate) Delegate.CreateDelegate(typeof(SetStringDelegate), stt) - : value => { }; - - BackgroundColorExists = _setBackgroundColor != null && _getBackgroundColor != null; - ForegroundColorExists = _setForegroundColor != null && _getForegroundColor != null; - TitleExists = _setTitle != null && _getTitle != null; - } - - private delegate ConsoleColor GetColorDelegate(); - - private delegate void SetColorDelegate(ConsoleColor value); - - private delegate string GetStringDelegate(); - - private delegate void SetStringDelegate(string value); + var tConsole = typeof(Console); + InitColors(tConsole); } + + public static bool BackgroundColorExists { get; private set; } + + public static ConsoleColor BackgroundColor + { + get => _getBackgroundColor(); + set => _setBackgroundColor(value); + } + + public static bool ForegroundColorExists { get; private set; } + + public static ConsoleColor ForegroundColor + { + get => _getForegroundColor(); + set => _setForegroundColor(value); + } + + public static bool TitleExists { get; private set; } + + public static string Title + { + get => _getTitle(); + set => _setTitle(value); + } + + private static void InitColors(Type tConsole) + { + const BindingFlags BINDING_FLAGS = BindingFlags.Public | BindingFlags.Static; + + var gfc = tConsole.GetMethod("get_ForegroundColor", BINDING_FLAGS); + var sfc = tConsole.GetMethod("set_ForegroundColor", BINDING_FLAGS); + + var gbc = tConsole.GetMethod("get_BackgroundColor", BINDING_FLAGS); + var sbc = tConsole.GetMethod("set_BackgroundColor", BINDING_FLAGS); + + var gtt = tConsole.GetMethod("get_Title", BINDING_FLAGS); + var stt = tConsole.GetMethod("set_Title", BINDING_FLAGS); + + _setForegroundColor = sfc != null + ? (SetColorDelegate) Delegate.CreateDelegate(typeof(SetColorDelegate), sfc) + : value => { }; + + _setBackgroundColor = sbc != null + ? (SetColorDelegate) Delegate.CreateDelegate(typeof(SetColorDelegate), sbc) + : value => { }; + + _getForegroundColor = gfc != null + ? (GetColorDelegate) Delegate.CreateDelegate(typeof(GetColorDelegate), gfc) + : () => ConsoleColor.Gray; + + _getBackgroundColor = gbc != null + ? (GetColorDelegate) Delegate.CreateDelegate(typeof(GetColorDelegate), gbc) + : () => ConsoleColor.Black; + + _getTitle = gtt != null + ? (GetStringDelegate) Delegate.CreateDelegate(typeof(GetStringDelegate), gtt) + : () => string.Empty; + + _setTitle = stt != null + ? (SetStringDelegate) Delegate.CreateDelegate(typeof(SetStringDelegate), stt) + : value => { }; + + BackgroundColorExists = _setBackgroundColor != null && _getBackgroundColor != null; + ForegroundColorExists = _setForegroundColor != null && _getForegroundColor != null; + TitleExists = _setTitle != null && _getTitle != null; + } + + private delegate ConsoleColor GetColorDelegate(); + + private delegate void SetColorDelegate(ConsoleColor value); + + private delegate string GetStringDelegate(); + + private delegate void SetStringDelegate(string value); } diff --git a/BepInEx.Core/Console/Unix/ConsoleWriter.cs b/BepInEx.Core/Console/Unix/ConsoleWriter.cs index 4e03679..c3e98c2 100644 --- a/BepInEx.Core/Console/Unix/ConsoleWriter.cs +++ b/BepInEx.Core/Console/Unix/ConsoleWriter.cs @@ -5,52 +5,51 @@ using System.Reflection; using System.Text; using HarmonyLib; -namespace BepInEx.Unix +namespace BepInEx.Unix; + +internal static class ConsoleWriter { - internal static class ConsoleWriter + private static Func cStreamWriterConstructor; + + private static Func CStreamWriterConstructor { - private static Func cStreamWriterConstructor; - - private static Func CStreamWriterConstructor + get { - get - { - if (cStreamWriterConstructor != null) - return cStreamWriterConstructor; - - var cStreamWriter = AccessTools.TypeByName("System.IO.CStreamWriter"); - - Func GetCtor(int[] perm) - { - var parameters = new[] { typeof(Stream), typeof(Encoding), typeof(bool) }; - var ctor = AccessTools.Constructor(cStreamWriter, perm.Select(i => parameters[i]).ToArray()); - if (ctor != null) - return (stream, encoding, l) => - { - var vals = new object[] { stream, encoding, l }; - return (StreamWriter) ctor.Invoke(perm.Select(i => vals[i]).ToArray()); - }; - return null; - } - - var ctorParams = new[] - { - new[] { 0, 1, 2 }, // Unity 5.x and up - new[] { 0, 1 } // Unity 4.7 and older - }; - - cStreamWriterConstructor = ctorParams.Select(GetCtor).FirstOrDefault(f => f != null); - if (cStreamWriterConstructor == null) - throw new AmbiguousMatchException("Failed to find suitable constructor for CStreamWriter"); + if (cStreamWriterConstructor != null) return cStreamWriterConstructor; - } - } - public static TextWriter CreateConsoleStreamWriter(Stream stream, Encoding encoding, bool leaveOpen) - { - var writer = CStreamWriterConstructor(stream, encoding, leaveOpen); - writer.AutoFlush = true; - return writer; + var cStreamWriter = AccessTools.TypeByName("System.IO.CStreamWriter"); + + Func GetCtor(int[] perm) + { + var parameters = new[] { typeof(Stream), typeof(Encoding), typeof(bool) }; + var ctor = AccessTools.Constructor(cStreamWriter, perm.Select(i => parameters[i]).ToArray()); + if (ctor != null) + return (stream, encoding, l) => + { + var vals = new object[] { stream, encoding, l }; + return (StreamWriter) ctor.Invoke(perm.Select(i => vals[i]).ToArray()); + }; + return null; + } + + var ctorParams = new[] + { + new[] { 0, 1, 2 }, // Unity 5.x and up + new[] { 0, 1 } // Unity 4.7 and older + }; + + cStreamWriterConstructor = ctorParams.Select(GetCtor).FirstOrDefault(f => f != null); + if (cStreamWriterConstructor == null) + throw new AmbiguousMatchException("Failed to find suitable constructor for CStreamWriter"); + return cStreamWriterConstructor; } } + + public static TextWriter CreateConsoleStreamWriter(Stream stream, Encoding encoding, bool leaveOpen) + { + var writer = CStreamWriterConstructor(stream, encoding, leaveOpen); + writer.AutoFlush = true; + return writer; + } } diff --git a/BepInEx.Core/Console/Unix/LinuxConsoleDriver.cs b/BepInEx.Core/Console/Unix/LinuxConsoleDriver.cs index 1ab4a87..ef4f2b4 100644 --- a/BepInEx.Core/Console/Unix/LinuxConsoleDriver.cs +++ b/BepInEx.Core/Console/Unix/LinuxConsoleDriver.cs @@ -5,110 +5,109 @@ using BepInEx.Logging; using HarmonyLib; using UnityInjector.ConsoleUtil; -namespace BepInEx.Unix +namespace BepInEx.Unix; + +internal class LinuxConsoleDriver : IConsoleDriver { - internal class LinuxConsoleDriver : IConsoleDriver + private static readonly ConfigEntry ForceCustomTtyDriverConfig = + ConfigFile.CoreConfig.Bind( + "Logging.Console", + "ForceBepInExTTYDriver", + false, + "If enabled, forces to use custom BepInEx TTY driver for handling terminal output on unix."); + + static LinuxConsoleDriver() { - private static readonly ConfigEntry ForceCustomTtyDriverConfig = - ConfigFile.CoreConfig.Bind( - "Logging.Console", - "ForceBepInExTTYDriver", - false, - "If enabled, forces to use custom BepInEx TTY driver for handling terminal output on unix."); + UseMonoTtyDriver = false; - static LinuxConsoleDriver() + if (ForceCustomTtyDriverConfig.Value) + return; + + var consoleDriverType = typeof(Console).Assembly.GetType("System.ConsoleDriver"); + + if (consoleDriverType != null) + UseMonoTtyDriver = typeof(Console).Assembly.GetType("System.ParameterizedStrings") != null; + } + + public static bool UseMonoTtyDriver { get; } + + public bool StdoutRedirected { get; private set; } + + public TtyInfo TtyInfo { get; private set; } + + public TextWriter StandardOut { get; private set; } + public TextWriter ConsoleOut { get; private set; } + + public bool ConsoleActive { get; private set; } + public bool ConsoleIsExternal => false; + + public void PreventClose() + { + // Not supported by all distros + } + + public void Initialize(bool alreadyActive) + { + // Console is always considered active on Unix + ConsoleActive = true; + + StdoutRedirected = UnixStreamHelper.isatty(1) != 1; + + var duplicateStream = UnixStreamHelper.CreateDuplicateStream(1); + + if (UseMonoTtyDriver && !StdoutRedirected) { - UseMonoTtyDriver = false; + // Mono implementation handles xterm for us - if (ForceCustomTtyDriverConfig.Value) - return; + var writer = ConsoleWriter.CreateConsoleStreamWriter(duplicateStream, Console.Out.Encoding, true); - var consoleDriverType = typeof(Console).Assembly.GetType("System.ConsoleDriver"); + StandardOut = TextWriter.Synchronized(writer); - if (consoleDriverType != null) - UseMonoTtyDriver = typeof(Console).Assembly.GetType("System.ParameterizedStrings") != null; + var driver = AccessTools.Field(AccessTools.TypeByName("System.ConsoleDriver"), "driver").GetValue(null); + AccessTools.Field(AccessTools.TypeByName("System.TermInfoDriver"), "stdout").SetValue(driver, writer); + } + else + { + // Handle TTY ourselves + + var writer = new StreamWriter(duplicateStream, Console.Out.Encoding); + + writer.AutoFlush = true; + + StandardOut = TextWriter.Synchronized(writer); + + TtyInfo = TtyHandler.GetTtyInfo(); } - public static bool UseMonoTtyDriver { get; } + ConsoleOut = StandardOut; + } - public bool StdoutRedirected { get; private set; } + public void CreateConsole(uint codepage) => + Logger.LogWarning("An external console currently cannot be spawned on a Unix platform."); - public TtyInfo TtyInfo { get; private set; } + public void DetachConsole() => + throw new PlatformNotSupportedException("Cannot detach console on a Unix platform"); - public TextWriter StandardOut { get; private set; } - public TextWriter ConsoleOut { get; private set; } + public void SetConsoleColor(ConsoleColor color) + { + if (StdoutRedirected) + return; - public bool ConsoleActive { get; private set; } - public bool ConsoleIsExternal => false; + if (UseMonoTtyDriver) + // Use mono's inbuilt terminfo driver to set the foreground color for us + SafeConsole.ForegroundColor = color; + else + ConsoleOut.Write(TtyInfo.GetAnsiCode(color)); + } - public void PreventClose() - { - // Not supported by all distros - } + public void SetConsoleTitle(string title) + { + if (StdoutRedirected) + return; - public void Initialize(bool alreadyActive) - { - // Console is always considered active on Unix - ConsoleActive = true; - - StdoutRedirected = UnixStreamHelper.isatty(1) != 1; - - var duplicateStream = UnixStreamHelper.CreateDuplicateStream(1); - - if (UseMonoTtyDriver && !StdoutRedirected) - { - // Mono implementation handles xterm for us - - var writer = ConsoleWriter.CreateConsoleStreamWriter(duplicateStream, Console.Out.Encoding, true); - - StandardOut = TextWriter.Synchronized(writer); - - var driver = AccessTools.Field(AccessTools.TypeByName("System.ConsoleDriver"), "driver").GetValue(null); - AccessTools.Field(AccessTools.TypeByName("System.TermInfoDriver"), "stdout").SetValue(driver, writer); - } - else - { - // Handle TTY ourselves - - var writer = new StreamWriter(duplicateStream, Console.Out.Encoding); - - writer.AutoFlush = true; - - StandardOut = TextWriter.Synchronized(writer); - - TtyInfo = TtyHandler.GetTtyInfo(); - } - - ConsoleOut = StandardOut; - } - - public void CreateConsole(uint codepage) => - Logger.LogWarning("An external console currently cannot be spawned on a Unix platform."); - - public void DetachConsole() => - throw new PlatformNotSupportedException("Cannot detach console on a Unix platform"); - - public void SetConsoleColor(ConsoleColor color) - { - if (StdoutRedirected) - return; - - if (UseMonoTtyDriver) - // Use mono's inbuilt terminfo driver to set the foreground color for us - SafeConsole.ForegroundColor = color; - else - ConsoleOut.Write(TtyInfo.GetAnsiCode(color)); - } - - public void SetConsoleTitle(string title) - { - if (StdoutRedirected) - return; - - if (UseMonoTtyDriver && SafeConsole.TitleExists) - SafeConsole.Title = title; - else - ConsoleOut.Write($"\u001B]2;{title.Replace("\\", "\\\\")}\u0007"); - } + if (UseMonoTtyDriver && SafeConsole.TitleExists) + SafeConsole.Title = title; + else + ConsoleOut.Write($"\u001B]2;{title.Replace("\\", "\\\\")}\u0007"); } } diff --git a/BepInEx.Core/Console/Unix/TtyHandler.cs b/BepInEx.Core/Console/Unix/TtyHandler.cs index db57b34..32b024d 100644 --- a/BepInEx.Core/Console/Unix/TtyHandler.cs +++ b/BepInEx.Core/Console/Unix/TtyHandler.cs @@ -5,200 +5,199 @@ using System.IO; using System.Linq; using System.Text; -namespace BepInEx.Unix +namespace BepInEx.Unix; + +internal class TtyInfo { - internal class TtyInfo + public string TerminalType { get; set; } = "default"; + + public int MaxColors { get; set; } + + public string[] ForegroundColorStrings { get; set; } + + public static TtyInfo Default { get; } = new() { - public string TerminalType { get; set; } = "default"; + MaxColors = 0 + }; - public int MaxColors { get; set; } - - public string[] ForegroundColorStrings { get; set; } - - public static TtyInfo Default { get; } = new() - { - MaxColors = 0 - }; - - public string GetAnsiCode(ConsoleColor color) - { - if (MaxColors <= 0 || ForegroundColorStrings == null) - return string.Empty; - - var index = (int) color % MaxColors; - return ForegroundColorStrings[index]; - } - } - - internal static class TtyHandler + public string GetAnsiCode(ConsoleColor color) { - private static readonly string[] ncursesLocations = - { - "/usr/share/terminfo", - "/etc/terminfo", - "/usr/lib/terminfo", - "/lib/terminfo" - }; + if (MaxColors <= 0 || ForegroundColorStrings == null) + return string.Empty; - private static string TryTermInfoDir(string dir, string term) - { - var infoFilePath = $"{dir}/{(int) term[0]:x}/{term}"; + var index = (int) color % MaxColors; + return ForegroundColorStrings[index]; + } +} + +internal static class TtyHandler +{ + private static readonly string[] ncursesLocations = + { + "/usr/share/terminfo", + "/etc/terminfo", + "/usr/lib/terminfo", + "/lib/terminfo" + }; + + private static string TryTermInfoDir(string dir, string term) + { + var infoFilePath = $"{dir}/{(int) term[0]:x}/{term}"; + + if (File.Exists(infoFilePath)) + return infoFilePath; - if (File.Exists(infoFilePath)) - return infoFilePath; + infoFilePath = Utility.CombinePaths(dir, term.Substring(0, 1), term); - infoFilePath = Utility.CombinePaths(dir, term.Substring(0, 1), term); + if (File.Exists(infoFilePath)) + return infoFilePath; - if (File.Exists(infoFilePath)) - return infoFilePath; + return null; + } + private static string FindTermInfoPath(string term) + { + if (string.IsNullOrEmpty(term)) return null; - } - private static string FindTermInfoPath(string term) + var termInfoVar = Environment.GetEnvironmentVariable("TERMINFO"); + if (termInfoVar != null && Directory.Exists(termInfoVar)) { - if (string.IsNullOrEmpty(term)) - return null; + var text = TryTermInfoDir(termInfoVar, term); + if (text != null) return text; + } - var termInfoVar = Environment.GetEnvironmentVariable("TERMINFO"); - if (termInfoVar != null && Directory.Exists(termInfoVar)) + foreach (var location in ncursesLocations) + if (Directory.Exists(location)) { - var text = TryTermInfoDir(termInfoVar, term); - if (text != null) return text; + var text = TryTermInfoDir(location, term); + + if (text != null) + return text; } - foreach (var location in ncursesLocations) - if (Directory.Exists(location)) - { - var text = TryTermInfoDir(location, term); + return null; + } - if (text != null) - return text; - } - - return null; - } + public static TtyInfo GetTtyInfo(string terminal = null) + { + terminal = terminal ?? Environment.GetEnvironmentVariable("TERM"); + var path = FindTermInfoPath(terminal); - public static TtyInfo GetTtyInfo(string terminal = null) - { - terminal = terminal ?? Environment.GetEnvironmentVariable("TERM"); - var path = FindTermInfoPath(terminal); + if (path == null) + return TtyInfo.Default; - if (path == null) - return TtyInfo.Default; + var buffer = File.ReadAllBytes(path); - var buffer = File.ReadAllBytes(path); + var info = TtyInfoParser.Parse(buffer); + info.TerminalType = terminal; - var info = TtyInfoParser.Parse(buffer); - info.TerminalType = terminal; - - return info; - } + return info; } +} - internal static class TtyInfoParser +internal static class TtyInfoParser +{ + private static readonly int[] ansiColorMapping = { - private static readonly int[] ansiColorMapping = - { - 0, 4, 2, 6, 1, 5, 3, 7, 8, 12, 10, 14, 9, 13, 11, 15 - }; + 0, 4, 2, 6, 1, 5, 3, 7, 8, 12, 10, 14, 9, 13, 11, 15 + }; - public static TtyInfo Parse(byte[] buffer) - { - int intSize; + public static TtyInfo Parse(byte[] buffer) + { + int intSize; - int magic = GetInt16(buffer, 0); + int magic = GetInt16(buffer, 0); - switch (magic) - { - case 0x11a: - intSize = 2; - break; + switch (magic) + { + case 0x11a: + intSize = 2; + break; - case 0x21E: - intSize = 4; - break; + case 0x21E: + intSize = 4; + break; - default: - // Unknown ttyinfo format - return TtyInfo.Default; - } + default: + // Unknown ttyinfo format + return TtyInfo.Default; + } - int boolFieldLength = GetInt16(buffer, 4); - int intFieldLength = GetInt16(buffer, 6); - int strOffsetFieldLength = GetInt16(buffer, 8); + int boolFieldLength = GetInt16(buffer, 4); + int intFieldLength = GetInt16(buffer, 6); + int strOffsetFieldLength = GetInt16(buffer, 8); - // Normally i'd put a more complete implementation here, but I only need to parse this info to get the max color count - // Feel free to implement the rest of this using these sources: - // https://github.com/mono/mono/blob/master/mcs/class/corlib/System/TermInfoReader.cs - // https://invisible-island.net/ncurses/man/term.5.html - // https://invisible-island.net/ncurses/man/terminfo.5.html + // Normally i'd put a more complete implementation here, but I only need to parse this info to get the max color count + // Feel free to implement the rest of this using these sources: + // https://github.com/mono/mono/blob/master/mcs/class/corlib/System/TermInfoReader.cs + // https://invisible-island.net/ncurses/man/term.5.html + // https://invisible-island.net/ncurses/man/terminfo.5.html - var baseOffset = 12 + GetString(buffer, 12).Length + 1; // Skip the terminal name - baseOffset += boolFieldLength; // Length of bool field section - baseOffset += baseOffset % 2; // Correct for boundary + var baseOffset = 12 + GetString(buffer, 12).Length + 1; // Skip the terminal name + baseOffset += boolFieldLength; // Length of bool field section + baseOffset += baseOffset % 2; // Correct for boundary - var colorOffset = - baseOffset - + intSize * (int) TermInfoNumbers.MaxColors; // Finally the offset for the max color integer + var colorOffset = + baseOffset + + intSize * (int) TermInfoNumbers.MaxColors; // Finally the offset for the max color integer - //int stringOffset = baseOffset + (intSize * intFieldLength); + //int stringOffset = baseOffset + (intSize * intFieldLength); - //int foregoundColorOffset = - // stringOffset - // + (2 * (int)TermInfoStrings.SetAForeground); + //int foregoundColorOffset = + // stringOffset + // + (2 * (int)TermInfoStrings.SetAForeground); - //foregoundColorOffset = stringOffset - // + (2 * strOffsetFieldLength) - // + GetInt16(buffer, foregoundColorOffset); + //foregoundColorOffset = stringOffset + // + (2 * strOffsetFieldLength) + // + GetInt16(buffer, foregoundColorOffset); - var info = new TtyInfo(); + var info = new TtyInfo(); - info.MaxColors = GetInteger(intSize, buffer, colorOffset); + info.MaxColors = GetInteger(intSize, buffer, colorOffset); - //string setForegroundTemplate = GetString(buffer, foregoundColorOffset); + //string setForegroundTemplate = GetString(buffer, foregoundColorOffset); - //info.ForegroundColorStrings = ansiColorMapping.Select(x => setForegroundTemplate.Replace("%p1%", x.ToString())).ToArray(); - info.ForegroundColorStrings = - ansiColorMapping.Select(x => $"\u001B[{(x > 7 ? 82 + x : 30 + x)}m").ToArray(); + //info.ForegroundColorStrings = ansiColorMapping.Select(x => setForegroundTemplate.Replace("%p1%", x.ToString())).ToArray(); + info.ForegroundColorStrings = + ansiColorMapping.Select(x => $"\u001B[{(x > 7 ? 82 + x : 30 + x)}m").ToArray(); - return info; - } + return info; + } - private static int GetInt32(byte[] buffer, int offset) => - buffer[offset] - | (buffer[offset + 1] << 8) - | (buffer[offset + 2] << 16) - | (buffer[offset + 3] << 24); + private static int GetInt32(byte[] buffer, int offset) => + buffer[offset] + | (buffer[offset + 1] << 8) + | (buffer[offset + 2] << 16) + | (buffer[offset + 3] << 24); - private static short GetInt16(byte[] buffer, int offset) => - (short) (buffer[offset] - | (buffer[offset + 1] << 8)); + private static short GetInt16(byte[] buffer, int offset) => + (short) (buffer[offset] + | (buffer[offset + 1] << 8)); - private static int GetInteger(int intSize, byte[] buffer, int offset) => - intSize == 2 - ? GetInt16(buffer, offset) - : GetInt32(buffer, offset); + private static int GetInteger(int intSize, byte[] buffer, int offset) => + intSize == 2 + ? GetInt16(buffer, offset) + : GetInt32(buffer, offset); - private static string GetString(byte[] buffer, int offset) - { - var length = 0; + private static string GetString(byte[] buffer, int offset) + { + var length = 0; - while (buffer[offset + length] != 0x00) - length++; + while (buffer[offset + length] != 0x00) + length++; - return Encoding.ASCII.GetString(buffer, offset, length); - } + return Encoding.ASCII.GetString(buffer, offset, length); + } - internal enum TermInfoNumbers - { - MaxColors = 13 - } + internal enum TermInfoNumbers + { + MaxColors = 13 + } - internal enum TermInfoStrings - { - SetAForeground = 359 - } + internal enum TermInfoStrings + { + SetAForeground = 359 } } diff --git a/BepInEx.Core/Console/Unix/UnixStream.cs b/BepInEx.Core/Console/Unix/UnixStream.cs index 8de1aff..f1ee8de 100644 --- a/BepInEx.Core/Console/Unix/UnixStream.cs +++ b/BepInEx.Core/Console/Unix/UnixStream.cs @@ -2,74 +2,73 @@ using System.IO; using System.Runtime.InteropServices; -namespace BepInEx.Unix +namespace BepInEx.Unix; + +internal class UnixStream : Stream { - internal class UnixStream : Stream + public UnixStream(int fileDescriptor, FileAccess access) { - public UnixStream(int fileDescriptor, FileAccess access) - { - Access = access; + Access = access; - var newFd = UnixStreamHelper.dup(fileDescriptor); - FileHandle = UnixStreamHelper.fdopen(newFd, access == FileAccess.Write ? "w" : "r"); - } + var newFd = UnixStreamHelper.dup(fileDescriptor); + FileHandle = UnixStreamHelper.fdopen(newFd, access == FileAccess.Write ? "w" : "r"); + } - public override bool CanRead => Access == FileAccess.Read || Access == FileAccess.ReadWrite; - public override bool CanSeek => false; - public override bool CanWrite => Access == FileAccess.Write || Access == FileAccess.ReadWrite; - public override long Length => throw new InvalidOperationException(); + public override bool CanRead => Access == FileAccess.Read || Access == FileAccess.ReadWrite; + public override bool CanSeek => false; + public override bool CanWrite => Access == FileAccess.Write || Access == FileAccess.ReadWrite; + public override long Length => throw new InvalidOperationException(); - public override long Position - { - get => throw new InvalidOperationException(); - set => throw new InvalidOperationException(); - } + public override long Position + { + get => throw new InvalidOperationException(); + set => throw new InvalidOperationException(); + } - public FileAccess Access { get; } + public FileAccess Access { get; } - public IntPtr FileHandle { get; } + public IntPtr FileHandle { get; } - public override void Flush() => UnixStreamHelper.fflush(FileHandle); + public override void Flush() => UnixStreamHelper.fflush(FileHandle); - public override long Seek(long offset, SeekOrigin origin) => throw new InvalidOperationException(); + public override long Seek(long offset, SeekOrigin origin) => throw new InvalidOperationException(); - public override void SetLength(long value) => throw new InvalidOperationException(); + public override void SetLength(long value) => throw new InvalidOperationException(); - public override int Read(byte[] buffer, int offset, int count) - { - var gcHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned); + public override int Read(byte[] buffer, int offset, int count) + { + var gcHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned); - var read = UnixStreamHelper.fread(new IntPtr(gcHandle.AddrOfPinnedObject().ToInt64() + offset), - (IntPtr) count, (IntPtr) 1, FileHandle); + var read = UnixStreamHelper.fread(new IntPtr(gcHandle.AddrOfPinnedObject().ToInt64() + offset), + (IntPtr) count, (IntPtr) 1, FileHandle); - gcHandle.Free(); + gcHandle.Free(); - return read.ToInt32(); - } + return read.ToInt32(); + } - public override void Write(byte[] buffer, int offset, int count) - { - var gcHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned); + public override void Write(byte[] buffer, int offset, int count) + { + var gcHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned); - UnixStreamHelper.fwrite(new IntPtr(gcHandle.AddrOfPinnedObject().ToInt64() + offset), (IntPtr) count, - (IntPtr) 1, FileHandle); + UnixStreamHelper.fwrite(new IntPtr(gcHandle.AddrOfPinnedObject().ToInt64() + offset), (IntPtr) count, + (IntPtr) 1, FileHandle); - gcHandle.Free(); - } + gcHandle.Free(); + } - private void ReleaseUnmanagedResources() => UnixStreamHelper.fclose(FileHandle); + private void ReleaseUnmanagedResources() => UnixStreamHelper.fclose(FileHandle); - protected override void Dispose(bool disposing) - { - ReleaseUnmanagedResources(); - base.Dispose(disposing); - } + protected override void Dispose(bool disposing) + { + ReleaseUnmanagedResources(); + base.Dispose(disposing); + } - ~UnixStream() - { - Dispose(false); - } + ~UnixStream() + { + Dispose(false); } } diff --git a/BepInEx.Core/Console/Unix/UnixStreamHelper.cs b/BepInEx.Core/Console/Unix/UnixStreamHelper.cs index 18ec5c5..f308fb8 100644 --- a/BepInEx.Core/Console/Unix/UnixStreamHelper.cs +++ b/BepInEx.Core/Console/Unix/UnixStreamHelper.cs @@ -3,65 +3,64 @@ using System.Collections.Generic; using System.IO; using MonoMod.Utils; -namespace BepInEx.Unix +namespace BepInEx.Unix; + +internal static class UnixStreamHelper { - internal static class UnixStreamHelper + public delegate int dupDelegate(int fd); + + public delegate int fcloseDelegate(IntPtr stream); + + public delegate IntPtr fdopenDelegate(int fd, string mode); + + public delegate int fflushDelegate(IntPtr stream); + + public delegate IntPtr freadDelegate(IntPtr ptr, IntPtr size, IntPtr nmemb, IntPtr stream); + + public delegate int fwriteDelegate(IntPtr ptr, IntPtr size, IntPtr nmemb, IntPtr stream); + + public delegate int isattyDelegate(int fd); + + [DynDllImport("libc")] + public static dupDelegate dup; + + [DynDllImport("libc")] + public static fdopenDelegate fdopen; + + [DynDllImport("libc")] + public static freadDelegate fread; + + [DynDllImport("libc")] + public static fwriteDelegate fwrite; + + [DynDllImport("libc")] + public static fcloseDelegate fclose; + + [DynDllImport("libc")] + public static fflushDelegate fflush; + + [DynDllImport("libc")] + public static isattyDelegate isatty; + + static UnixStreamHelper() { - public delegate int dupDelegate(int fd); - - public delegate int fcloseDelegate(IntPtr stream); - - public delegate IntPtr fdopenDelegate(int fd, string mode); - - public delegate int fflushDelegate(IntPtr stream); - - public delegate IntPtr freadDelegate(IntPtr ptr, IntPtr size, IntPtr nmemb, IntPtr stream); - - public delegate int fwriteDelegate(IntPtr ptr, IntPtr size, IntPtr nmemb, IntPtr stream); - - public delegate int isattyDelegate(int fd); - - [DynDllImport("libc")] - public static dupDelegate dup; - - [DynDllImport("libc")] - public static fdopenDelegate fdopen; - - [DynDllImport("libc")] - public static freadDelegate fread; - - [DynDllImport("libc")] - public static fwriteDelegate fwrite; - - [DynDllImport("libc")] - public static fcloseDelegate fclose; - - [DynDllImport("libc")] - public static fflushDelegate fflush; - - [DynDllImport("libc")] - public static isattyDelegate isatty; - - static UnixStreamHelper() + var libcMapping = new Dictionary> { - var libcMapping = new Dictionary> + ["libc"] = new() { - ["libc"] = new() - { - "libc.so.6", // Ubuntu glibc - "libc", // Linux glibc - "/usr/lib/libSystem.dylib" // OSX POSIX - } - }; + "libc.so.6", // Ubuntu glibc + "libc", // Linux glibc + "/usr/lib/libSystem.dylib" // OSX POSIX + } + }; - typeof(UnixStreamHelper).ResolveDynDllImports(libcMapping); - } + typeof(UnixStreamHelper).ResolveDynDllImports(libcMapping); + } - public static Stream CreateDuplicateStream(int fileDescriptor) - { - var newFd = dup(fileDescriptor); + public static Stream CreateDuplicateStream(int fileDescriptor) + { + var newFd = dup(fileDescriptor); - return new UnixStream(newFd, FileAccess.Write); - } + return new UnixStream(newFd, FileAccess.Write); } } diff --git a/BepInEx.Core/Console/Windows/ConsoleEncoding/ConsoleEncoding.Buffers.cs b/BepInEx.Core/Console/Windows/ConsoleEncoding/ConsoleEncoding.Buffers.cs index 69b7c01..b4099e5 100644 --- a/BepInEx.Core/Console/Windows/ConsoleEncoding/ConsoleEncoding.Buffers.cs +++ b/BepInEx.Core/Console/Windows/ConsoleEncoding/ConsoleEncoding.Buffers.cs @@ -3,59 +3,57 @@ // Copyright (c) Usagirei 2015 - 2015 // -------------------------------------------------- -namespace UnityInjector.ConsoleUtil +namespace UnityInjector.ConsoleUtil; +// -------------------------------------------------- +// Code ported from +// https://gist.github.com/asm256/9bfb88336a1433e2328a +// Which in turn was seemingly ported from +// http://jonskeet.uk/csharp/ebcdic/ +// using only safe (managed) code +// -------------------------------------------------- + +internal partial class ConsoleEncoding { - // -------------------------------------------------- - // Code ported from - // https://gist.github.com/asm256/9bfb88336a1433e2328a - // Which in turn was seemingly ported from - // http://jonskeet.uk/csharp/ebcdic/ - // using only safe (managed) code - // -------------------------------------------------- + private readonly byte[] _zeroByte = new byte[0]; + private readonly char[] _zeroChar = new char[0]; + private byte[] _byteBuffer = new byte[256]; + private char[] _charBuffer = new char[256]; - internal partial class ConsoleEncoding + private void ExpandByteBuffer(int count) { - private readonly byte[] _zeroByte = new byte[0]; - private readonly char[] _zeroChar = new char[0]; - private byte[] _byteBuffer = new byte[256]; - private char[] _charBuffer = new char[256]; + if (_byteBuffer.Length < count) + _byteBuffer = new byte[count]; + } - private void ExpandByteBuffer(int count) - { - if (_byteBuffer.Length < count) - _byteBuffer = new byte[count]; - } + private void ExpandCharBuffer(int count) + { + if (_charBuffer.Length < count) + _charBuffer = new char[count]; + } - private void ExpandCharBuffer(int count) - { - if (_charBuffer.Length < count) - _charBuffer = new char[count]; - } + private void ReadByteBuffer(byte[] bytes, int index, int count) + { + for (var i = 0; i < count; i++) + bytes[index + i] = _byteBuffer[i]; + } - private void ReadByteBuffer(byte[] bytes, int index, int count) - { - for (var i = 0; i < count; i++) - bytes[index + i] = _byteBuffer[i]; - } + private void ReadCharBuffer(char[] chars, int index, int count) + { + for (var i = 0; i < count; i++) + chars[index + i] = _charBuffer[i]; + } - private void ReadCharBuffer(char[] chars, int index, int count) - { - for (var i = 0; i < count; i++) - chars[index + i] = _charBuffer[i]; - } + private void WriteByteBuffer(byte[] bytes, int index, int count) + { + ExpandByteBuffer(count); + for (var i = 0; i < count; i++) + _byteBuffer[i] = bytes[index + i]; + } - private void WriteByteBuffer(byte[] bytes, int index, int count) - { - ExpandByteBuffer(count); - for (var i = 0; i < count; i++) - _byteBuffer[i] = bytes[index + i]; - } - - private void WriteCharBuffer(char[] chars, int index, int count) - { - ExpandCharBuffer(count); - for (var i = 0; i < count; i++) - _charBuffer[i] = chars[index + i]; - } + private void WriteCharBuffer(char[] chars, int index, int count) + { + ExpandCharBuffer(count); + for (var i = 0; i < count; i++) + _charBuffer[i] = chars[index + i]; } } diff --git a/BepInEx.Core/Console/Windows/ConsoleEncoding/ConsoleEncoding.PInvoke.cs b/BepInEx.Core/Console/Windows/ConsoleEncoding/ConsoleEncoding.PInvoke.cs index f8d098e..97ad9e9 100644 --- a/BepInEx.Core/Console/Windows/ConsoleEncoding/ConsoleEncoding.PInvoke.cs +++ b/BepInEx.Core/Console/Windows/ConsoleEncoding/ConsoleEncoding.PInvoke.cs @@ -6,45 +6,44 @@ using System; using System.Runtime.InteropServices; -namespace UnityInjector.ConsoleUtil +namespace UnityInjector.ConsoleUtil; + +// -------------------------------------------------- +// Code ported from +// https://gist.github.com/asm256/9bfb88336a1433e2328a +// Which in turn was seemingly ported from +// http://jonskeet.uk/csharp/ebcdic/ +// using only safe (managed) code +// -------------------------------------------------- +internal partial class ConsoleEncoding { - // -------------------------------------------------- - // Code ported from - // https://gist.github.com/asm256/9bfb88336a1433e2328a - // Which in turn was seemingly ported from - // http://jonskeet.uk/csharp/ebcdic/ - // using only safe (managed) code - // -------------------------------------------------- - internal partial class ConsoleEncoding - { - [DllImport("kernel32.dll")] - private static extern uint GetConsoleOutputCP(); + [DllImport("kernel32.dll")] + private static extern uint GetConsoleOutputCP(); - [DllImport("kernel32.dll")] - private static extern uint GetACP(); + [DllImport("kernel32.dll")] + private static extern uint GetACP(); - [DllImport("kernel32.dll", SetLastError = true)] - private static extern int MultiByteToWideChar(uint codePage, - uint dwFlags, - [In] [MarshalAs(UnmanagedType.LPArray)] - byte[] lpMultiByteStr, - int cbMultiByte, - [Out] [MarshalAs(UnmanagedType.LPWStr)] - char[] lpWideCharStr, - int cchWideChar); + [DllImport("kernel32.dll", SetLastError = true)] + private static extern int MultiByteToWideChar(uint codePage, + uint dwFlags, + [In] [MarshalAs(UnmanagedType.LPArray)] + byte[] lpMultiByteStr, + int cbMultiByte, + [Out] [MarshalAs(UnmanagedType.LPWStr)] + char[] lpWideCharStr, + int cchWideChar); - [DllImport("kernel32.dll")] - private static extern IntPtr SetConsoleOutputCP(uint codepage); + [DllImport("kernel32.dll")] + private static extern IntPtr SetConsoleOutputCP(uint codepage); - [DllImport("kernel32.dll", SetLastError = true)] - private static extern int WideCharToMultiByte(uint codePage, - uint dwFlags, - [In] [MarshalAs(UnmanagedType.LPWStr)] char[] lpWideCharStr, - int cchWideChar, - [Out] [MarshalAs(UnmanagedType.LPArray)] - byte[] lpMultiByteStr, - int cbMultiByte, - IntPtr lpDefaultChar, - IntPtr lpUsedDefaultChar); - } + [DllImport("kernel32.dll", SetLastError = true)] + private static extern int WideCharToMultiByte(uint codePage, + uint dwFlags, + [In] [MarshalAs(UnmanagedType.LPWStr)] char[] lpWideCharStr, + int cchWideChar, + [Out] [MarshalAs(UnmanagedType.LPArray)] + byte[] lpMultiByteStr, + int cbMultiByte, + IntPtr lpDefaultChar, + IntPtr lpUsedDefaultChar); } diff --git a/BepInEx.Core/Console/Windows/ConsoleEncoding/ConsoleEncoding.cs b/BepInEx.Core/Console/Windows/ConsoleEncoding/ConsoleEncoding.cs index a58e28f..d4d51fb 100644 --- a/BepInEx.Core/Console/Windows/ConsoleEncoding/ConsoleEncoding.cs +++ b/BepInEx.Core/Console/Windows/ConsoleEncoding/ConsoleEncoding.cs @@ -6,80 +6,79 @@ using System; using System.Text; -namespace UnityInjector.ConsoleUtil +namespace UnityInjector.ConsoleUtil; + +// -------------------------------------------------- +// Code ported from +// https://gist.github.com/asm256/9bfb88336a1433e2328a +// Which in turn was seemingly ported from +// http://jonskeet.uk/csharp/ebcdic/ +// using only safe (managed) code +// -------------------------------------------------- +internal partial class ConsoleEncoding : Encoding { - // -------------------------------------------------- - // Code ported from - // https://gist.github.com/asm256/9bfb88336a1433e2328a - // Which in turn was seemingly ported from - // http://jonskeet.uk/csharp/ebcdic/ - // using only safe (managed) code - // -------------------------------------------------- - internal partial class ConsoleEncoding : Encoding + private readonly uint _codePage; + + private ConsoleEncoding(uint codePage) { - private readonly uint _codePage; - - private ConsoleEncoding(uint codePage) - { - _codePage = codePage; - } - - public override int CodePage => (int)_codePage; - - public static Encoding OutputEncoding => new ConsoleEncoding(ConsoleCodePage); - - public static uint ConsoleCodePage - { - get => GetConsoleOutputCP(); - set => SetConsoleOutputCP(value); - } - - public static uint GetActiveCodePage() => GetACP(); - - public static ConsoleEncoding GetEncoding(uint codePage) => new(codePage); - - public override int GetByteCount(char[] chars, int index, int count) - { - WriteCharBuffer(chars, index, count); - var result = WideCharToMultiByte(_codePage, 0, _charBuffer, count, _zeroByte, 0, IntPtr.Zero, IntPtr.Zero); - return result; - } - - public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex) - { - var byteCount = GetByteCount(chars, charIndex, charCount); - - WriteCharBuffer(chars, charIndex, charCount); - - ExpandByteBuffer(byteCount); - var result = WideCharToMultiByte(_codePage, 0, chars, charCount, _byteBuffer, byteCount, IntPtr.Zero, - IntPtr.Zero); - ReadByteBuffer(bytes, byteIndex, byteCount); - - return result; - } - - public override int GetCharCount(byte[] bytes, int index, int count) - { - WriteByteBuffer(bytes, index, count); - var result = MultiByteToWideChar(_codePage, 0, bytes, count, _zeroChar, 0); - return result; - } - - public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex) - { - var charCount = GetCharCount(bytes, byteIndex, byteCount); - - WriteByteBuffer(bytes, byteIndex, byteCount); - - ExpandCharBuffer(charCount); - var result = MultiByteToWideChar(_codePage, 0, bytes, byteCount, _charBuffer, charCount); - ReadCharBuffer(chars, charIndex, charCount); - - return result; - } - - public override int GetMaxByteCount(int charCount) => charCount * 2; - public override int GetMaxCharCount(int byteCount) => byteCount; + _codePage = codePage; } + + public override int CodePage => (int) _codePage; + + public static Encoding OutputEncoding => new ConsoleEncoding(ConsoleCodePage); + + public static uint ConsoleCodePage + { + get => GetConsoleOutputCP(); + set => SetConsoleOutputCP(value); + } + + public static uint GetActiveCodePage() => GetACP(); + + public static ConsoleEncoding GetEncoding(uint codePage) => new(codePage); + + public override int GetByteCount(char[] chars, int index, int count) + { + WriteCharBuffer(chars, index, count); + var result = WideCharToMultiByte(_codePage, 0, _charBuffer, count, _zeroByte, 0, IntPtr.Zero, IntPtr.Zero); + return result; + } + + public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex) + { + var byteCount = GetByteCount(chars, charIndex, charCount); + + WriteCharBuffer(chars, charIndex, charCount); + + ExpandByteBuffer(byteCount); + var result = WideCharToMultiByte(_codePage, 0, chars, charCount, _byteBuffer, byteCount, IntPtr.Zero, + IntPtr.Zero); + ReadByteBuffer(bytes, byteIndex, byteCount); + + return result; + } + + public override int GetCharCount(byte[] bytes, int index, int count) + { + WriteByteBuffer(bytes, index, count); + var result = MultiByteToWideChar(_codePage, 0, bytes, count, _zeroChar, 0); + return result; + } + + public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex) + { + var charCount = GetCharCount(bytes, byteIndex, byteCount); + + WriteByteBuffer(bytes, byteIndex, byteCount); + + ExpandCharBuffer(charCount); + var result = MultiByteToWideChar(_codePage, 0, bytes, byteCount, _charBuffer, charCount); + ReadCharBuffer(chars, charIndex, charCount); + + return result; + } + + public override int GetMaxByteCount(int charCount) => charCount * 2; + public override int GetMaxCharCount(int byteCount) => byteCount; } diff --git a/BepInEx.Core/Console/Windows/ConsoleWindow.cs b/BepInEx.Core/Console/Windows/ConsoleWindow.cs index 2be1d9e..fe27dfd 100644 --- a/BepInEx.Core/Console/Windows/ConsoleWindow.cs +++ b/BepInEx.Core/Console/Windows/ConsoleWindow.cs @@ -9,169 +9,168 @@ using BepInEx; using BepInEx.ConsoleUtil; using MonoMod.Utils; -namespace UnityInjector.ConsoleUtil +namespace UnityInjector.ConsoleUtil; + +internal class ConsoleWindow { - internal class ConsoleWindow + private const uint SC_CLOSE = 0xF060; + private const uint MF_BYCOMMAND = 0x00000000; + + private const uint LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800; + public static IntPtr ConsoleOutHandle; + public static IntPtr OriginalStdoutHandle; + + private static bool methodsInited; + private static SetForegroundWindowDelegate setForeground; + private static GetForegroundWindowDelegate getForeground; + private static GetSystemMenuDelegate getSystemMenu; + private static DeleteMenuDelegate deleteMenu; + public static bool IsAttached { get; private set; } + + public static string Title { - private const uint SC_CLOSE = 0xF060; - private const uint MF_BYCOMMAND = 0x00000000; - - private const uint LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800; - public static IntPtr ConsoleOutHandle; - public static IntPtr OriginalStdoutHandle; - - private static bool methodsInited; - private static SetForegroundWindowDelegate setForeground; - private static GetForegroundWindowDelegate getForeground; - private static GetSystemMenuDelegate getSystemMenu; - private static DeleteMenuDelegate deleteMenu; - public static bool IsAttached { get; private set; } - - public static string Title - { - set - { - if (!IsAttached) - return; - - if (value == null) - throw new ArgumentNullException(nameof(value)); - - if (value.Length > 24500) - throw new InvalidOperationException("Console title too long"); - - if (!SetConsoleTitle(value)) - throw new InvalidOperationException("Console title invalid"); - } - } - - public static void Attach() - { - if (IsAttached) - return; - Initialize(); - - if (OriginalStdoutHandle == IntPtr.Zero) - OriginalStdoutHandle = GetStdHandle(-11); - - // Store Current Window - var currWnd = getForeground(); - - var cur = GetConsoleWindow(); - - //Check for existing console before allocating - if (GetConsoleWindow() == IntPtr.Zero) - if (!AllocConsole()) - throw new Exception("AllocConsole() failed"); - // Restore Foreground - setForeground(currWnd); - - ConsoleOutHandle = CreateFile("CONOUT$", 0x80000000 | 0x40000000, 2, IntPtr.Zero, 3, 0, IntPtr.Zero); - Kon.conOut = ConsoleOutHandle; - - if (!SetStdHandle(-11, ConsoleOutHandle)) - throw new Exception("SetStdHandle() failed"); - - if (OriginalStdoutHandle != IntPtr.Zero && ConsoleManager.ConfigConsoleOutRedirectType.Value == - ConsoleManager.ConsoleOutRedirectType.ConsoleOut) - CloseHandle(OriginalStdoutHandle); - - IsAttached = true; - } - - public static void PreventClose() - { - if (!IsAttached) - return; - Initialize(); - - var hwnd = GetConsoleWindow(); - var hmenu = getSystemMenu(hwnd, false); - if (hmenu != IntPtr.Zero) - deleteMenu(hmenu, SC_CLOSE, MF_BYCOMMAND); - } - - public static void Detach() + set { if (!IsAttached) return; - if (!CloseHandle(ConsoleOutHandle)) - throw new Exception("CloseHandle() failed"); + if (value == null) + throw new ArgumentNullException(nameof(value)); - ConsoleOutHandle = IntPtr.Zero; + if (value.Length > 24500) + throw new InvalidOperationException("Console title too long"); - if (!FreeConsole()) - throw new Exception("FreeConsole() failed"); - - if (!SetStdHandle(-11, OriginalStdoutHandle)) - throw new Exception("SetStdHandle() failed"); - - IsAttached = false; + if (!SetConsoleTitle(value)) + throw new InvalidOperationException("Console title invalid"); } - - private static void Initialize() - { - if (methodsInited) - return; - methodsInited = true; - - // Some games may ship user32.dll with some methods missing. As such, we load the DLL explicitly from system folder - var user32Dll = LoadLibraryEx("user32.dll", IntPtr.Zero, LOAD_LIBRARY_SEARCH_SYSTEM32); - setForeground = GetProcAddress(user32Dll, "SetForegroundWindow").AsDelegate(); - getForeground = GetProcAddress(user32Dll, "GetForegroundWindow").AsDelegate(); - getSystemMenu = GetProcAddress(user32Dll, "GetSystemMenu").AsDelegate(); - deleteMenu = GetProcAddress(user32Dll, "DeleteMenu").AsDelegate(); - } - - [DllImport("kernel32.dll", SetLastError = true)] - static extern IntPtr GetProcAddress(IntPtr hModule, string procName); - - [DllImport("kernel32.dll", SetLastError = true)] - private static extern bool AllocConsole(); - - [DllImport("kernel32.dll")] - private static extern IntPtr GetConsoleWindow(); - - [DllImport("kernel32.dll", ExactSpelling = true, SetLastError = true)] - private static extern bool CloseHandle(IntPtr handle); - - [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] - private static extern IntPtr CreateFile(string fileName, - uint desiredAccess, - int shareMode, - IntPtr securityAttributes, - int creationDisposition, - int flagsAndAttributes, - IntPtr templateFile); - - [DllImport("kernel32.dll", SetLastError = false)] - private static extern bool FreeConsole(); - - [DllImport("kernel32.dll", SetLastError = true)] - private static extern IntPtr GetStdHandle(int nStdHandle); - - [DllImport("kernel32.dll", SetLastError = true)] - private static extern bool SetStdHandle(int nStdHandle, IntPtr hConsoleOutput); - - [DllImport("kernel32.dll", BestFitMapping = true, CharSet = CharSet.Auto, SetLastError = true)] - private static extern bool SetConsoleTitle(string title); - - [DllImport("kernel32.dll", SetLastError = true)] - private static extern IntPtr LoadLibraryEx(string lpLibFileName, IntPtr hFile, uint dwFlags); - - - [UnmanagedFunctionPointer(CallingConvention.Winapi)] - [return: MarshalAs(UnmanagedType.Bool)] - private delegate bool SetForegroundWindowDelegate(IntPtr hWnd); - - [UnmanagedFunctionPointer(CallingConvention.Winapi)] - private delegate IntPtr GetForegroundWindowDelegate(); - - [UnmanagedFunctionPointer(CallingConvention.Winapi)] - private delegate IntPtr GetSystemMenuDelegate(IntPtr hwnd, bool bRevert); - - [UnmanagedFunctionPointer(CallingConvention.Winapi)] - private delegate bool DeleteMenuDelegate(IntPtr hMenu, uint uPosition, uint uFlags); } + + public static void Attach() + { + if (IsAttached) + return; + Initialize(); + + if (OriginalStdoutHandle == IntPtr.Zero) + OriginalStdoutHandle = GetStdHandle(-11); + + // Store Current Window + var currWnd = getForeground(); + + var cur = GetConsoleWindow(); + + //Check for existing console before allocating + if (GetConsoleWindow() == IntPtr.Zero) + if (!AllocConsole()) + throw new Exception("AllocConsole() failed"); + // Restore Foreground + setForeground(currWnd); + + ConsoleOutHandle = CreateFile("CONOUT$", 0x80000000 | 0x40000000, 2, IntPtr.Zero, 3, 0, IntPtr.Zero); + Kon.conOut = ConsoleOutHandle; + + if (!SetStdHandle(-11, ConsoleOutHandle)) + throw new Exception("SetStdHandle() failed"); + + if (OriginalStdoutHandle != IntPtr.Zero && ConsoleManager.ConfigConsoleOutRedirectType.Value == + ConsoleManager.ConsoleOutRedirectType.ConsoleOut) + CloseHandle(OriginalStdoutHandle); + + IsAttached = true; + } + + public static void PreventClose() + { + if (!IsAttached) + return; + Initialize(); + + var hwnd = GetConsoleWindow(); + var hmenu = getSystemMenu(hwnd, false); + if (hmenu != IntPtr.Zero) + deleteMenu(hmenu, SC_CLOSE, MF_BYCOMMAND); + } + + public static void Detach() + { + if (!IsAttached) + return; + + if (!CloseHandle(ConsoleOutHandle)) + throw new Exception("CloseHandle() failed"); + + ConsoleOutHandle = IntPtr.Zero; + + if (!FreeConsole()) + throw new Exception("FreeConsole() failed"); + + if (!SetStdHandle(-11, OriginalStdoutHandle)) + throw new Exception("SetStdHandle() failed"); + + IsAttached = false; + } + + private static void Initialize() + { + if (methodsInited) + return; + methodsInited = true; + + // Some games may ship user32.dll with some methods missing. As such, we load the DLL explicitly from system folder + var user32Dll = LoadLibraryEx("user32.dll", IntPtr.Zero, LOAD_LIBRARY_SEARCH_SYSTEM32); + setForeground = GetProcAddress(user32Dll, "SetForegroundWindow").AsDelegate(); + getForeground = GetProcAddress(user32Dll, "GetForegroundWindow").AsDelegate(); + getSystemMenu = GetProcAddress(user32Dll, "GetSystemMenu").AsDelegate(); + deleteMenu = GetProcAddress(user32Dll, "DeleteMenu").AsDelegate(); + } + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr GetProcAddress(IntPtr hModule, string procName); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool AllocConsole(); + + [DllImport("kernel32.dll")] + private static extern IntPtr GetConsoleWindow(); + + [DllImport("kernel32.dll", ExactSpelling = true, SetLastError = true)] + private static extern bool CloseHandle(IntPtr handle); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern IntPtr CreateFile(string fileName, + uint desiredAccess, + int shareMode, + IntPtr securityAttributes, + int creationDisposition, + int flagsAndAttributes, + IntPtr templateFile); + + [DllImport("kernel32.dll", SetLastError = false)] + private static extern bool FreeConsole(); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr GetStdHandle(int nStdHandle); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool SetStdHandle(int nStdHandle, IntPtr hConsoleOutput); + + [DllImport("kernel32.dll", BestFitMapping = true, CharSet = CharSet.Auto, SetLastError = true)] + private static extern bool SetConsoleTitle(string title); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr LoadLibraryEx(string lpLibFileName, IntPtr hFile, uint dwFlags); + + + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + [return: MarshalAs(UnmanagedType.Bool)] + private delegate bool SetForegroundWindowDelegate(IntPtr hWnd); + + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + private delegate IntPtr GetForegroundWindowDelegate(); + + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + private delegate IntPtr GetSystemMenuDelegate(IntPtr hwnd, bool bRevert); + + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + private delegate bool DeleteMenuDelegate(IntPtr hMenu, uint uPosition, uint uFlags); } diff --git a/BepInEx.Core/Console/Windows/WindowsConsoleDriver.cs b/BepInEx.Core/Console/Windows/WindowsConsoleDriver.cs index d9c9d29..7608e35 100644 --- a/BepInEx.Core/Console/Windows/WindowsConsoleDriver.cs +++ b/BepInEx.Core/Console/Windows/WindowsConsoleDriver.cs @@ -8,133 +8,132 @@ using Microsoft.Win32.SafeHandles; using MonoMod.Utils; using UnityInjector.ConsoleUtil; -namespace BepInEx +namespace BepInEx; + +internal class WindowsConsoleDriver : IConsoleDriver { - internal class WindowsConsoleDriver : IConsoleDriver + // Apparently on some versions of Unity (e.g. 2018.4) using old mono causes crashes on game close if + // IntPtr overload is used for file streams (check #139). + // On the other hand, not all Unity games come with SafeFileHandle overload for FileStream + // As such, we're trying to use SafeFileHandle when it's available and go back to IntPtr overload if not available + private static readonly ConstructorInfo FileStreamCtor = new[] { - // Apparently on some versions of Unity (e.g. 2018.4) using old mono causes crashes on game close if - // IntPtr overload is used for file streams (check #139). - // On the other hand, not all Unity games come with SafeFileHandle overload for FileStream - // As such, we're trying to use SafeFileHandle when it's available and go back to IntPtr overload if not available - private static readonly ConstructorInfo FileStreamCtor = new[] + AccessTools.Constructor(typeof(FileStream), new[] { typeof(SafeFileHandle), typeof(FileAccess) }), + AccessTools.Constructor(typeof(FileStream), new[] { typeof(IntPtr), typeof(FileAccess) }) + }.FirstOrDefault(m => m != null); + + private readonly Func getWindowHeight = AccessTools + .PropertyGetter(typeof(Console), nameof(Console.WindowHeight)) + ?.CreateDelegate>(); + + private readonly Func getWindowWidth = AccessTools + .PropertyGetter(typeof(Console), nameof(Console.WindowWidth)) + ?.CreateDelegate>(); + + private int ConsoleWidth => getWindowWidth?.Invoke() ?? 0; + private int ConsoleHeight => getWindowHeight?.Invoke() ?? 0; + + public TextWriter StandardOut { get; private set; } + public TextWriter ConsoleOut { get; private set; } + + public bool ConsoleActive { get; private set; } + public bool ConsoleIsExternal => true; + + public void Initialize(bool alreadyActive) + { + ConsoleActive = alreadyActive || ConsoleWidth != 0 && ConsoleHeight != 0; + + if (ConsoleActive) { - AccessTools.Constructor(typeof(FileStream), new[] { typeof(SafeFileHandle), typeof(FileAccess) }), - AccessTools.Constructor(typeof(FileStream), new[] { typeof(IntPtr), typeof(FileAccess) }) - }.FirstOrDefault(m => m != null); - - private readonly Func getWindowHeight = AccessTools - .PropertyGetter(typeof(Console), nameof(Console.WindowHeight)) - ?.CreateDelegate>(); - - private readonly Func getWindowWidth = AccessTools - .PropertyGetter(typeof(Console), nameof(Console.WindowWidth)) - ?.CreateDelegate>(); - - private int ConsoleWidth => getWindowWidth?.Invoke() ?? 0; - private int ConsoleHeight => getWindowHeight?.Invoke() ?? 0; - - public TextWriter StandardOut { get; private set; } - public TextWriter ConsoleOut { get; private set; } - - public bool ConsoleActive { get; private set; } - public bool ConsoleIsExternal => true; - - public void Initialize(bool alreadyActive) + // We're in a .NET framework / XNA environment; console *is* stdout + ConsoleOut = Console.Out; + StandardOut = new StreamWriter(Console.OpenStandardOutput()); + } + else { - ConsoleActive = alreadyActive || ConsoleWidth != 0 && ConsoleHeight != 0; + StandardOut = Console.Out; + } + } - if (ConsoleActive) - { - // We're in a .NET framework / XNA environment; console *is* stdout - ConsoleOut = Console.Out; - StandardOut = new StreamWriter(Console.OpenStandardOutput()); - } - else - { - StandardOut = Console.Out; - } + public void CreateConsole(uint codepage) + { + ConsoleWindow.Attach(); + + // 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 + 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? + var stdout = GetOutHandle(); + if (stdout == IntPtr.Zero) + { + StandardOut = TextWriter.Null; + ConsoleOut = TextWriter.Null; + return; } - public void CreateConsole(uint codepage) + var originalOutStream = OpenFileStream(stdout); + StandardOut = new StreamWriter(originalOutStream, Utility.UTF8NoBom) { - ConsoleWindow.Attach(); + AutoFlush = true + }; - // 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 - 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? - var stdout = GetOutHandle(); - if (stdout == IntPtr.Zero) - { - StandardOut = TextWriter.Null; - ConsoleOut = TextWriter.Null; - return; - } - - var originalOutStream = OpenFileStream(stdout); - StandardOut = new StreamWriter(originalOutStream, Utility.UTF8NoBom) - { - AutoFlush = true - }; - - var consoleOutStream = OpenFileStream(ConsoleWindow.ConsoleOutHandle); - // Can't use Console.OutputEncoding because it can be null (i.e. not preference by user) - ConsoleOut = new StreamWriter(consoleOutStream, ConsoleEncoding.OutputEncoding) - { - AutoFlush = true - }; - ConsoleActive = true; - } - - public void PreventClose() => ConsoleWindow.PreventClose(); - - public void DetachConsole() + var consoleOutStream = OpenFileStream(ConsoleWindow.ConsoleOutHandle); + // Can't use Console.OutputEncoding because it can be null (i.e. not preference by user) + ConsoleOut = new StreamWriter(consoleOutStream, ConsoleEncoding.OutputEncoding) { - ConsoleWindow.Detach(); + AutoFlush = true + }; + ConsoleActive = true; + } - ConsoleOut.Close(); - ConsoleOut = null; + public void PreventClose() => ConsoleWindow.PreventClose(); - ConsoleActive = false; - } + public void DetachConsole() + { + ConsoleWindow.Detach(); - public void SetConsoleColor(ConsoleColor color) + ConsoleOut.Close(); + ConsoleOut = null; + + ConsoleActive = false; + } + + public void SetConsoleColor(ConsoleColor color) + { + SafeConsole.ForegroundColor = color; + Kon.ForegroundColor = color; + } + + public void SetConsoleTitle(string title) => ConsoleWindow.Title = title; + + private static FileStream OpenFileStream(IntPtr handle) + { + var fileHandle = new SafeFileHandle(handle, false); + var ctorParams = AccessTools.ActualParameters(FileStreamCtor, + new object[] + { + fileHandle, fileHandle.DangerousGetHandle(), + FileAccess.Write + }); + return (FileStream) Activator.CreateInstance(typeof(FileStream), ctorParams); + } + + private IntPtr GetOutHandle() + { + switch (ConsoleManager.ConfigConsoleOutRedirectType.Value) { - SafeConsole.ForegroundColor = color; - Kon.ForegroundColor = color; - } - - public void SetConsoleTitle(string title) => ConsoleWindow.Title = title; - - private static FileStream OpenFileStream(IntPtr handle) - { - var fileHandle = new SafeFileHandle(handle, false); - var ctorParams = AccessTools.ActualParameters(FileStreamCtor, - new object[] - { - fileHandle, fileHandle.DangerousGetHandle(), - FileAccess.Write - }); - return (FileStream)Activator.CreateInstance(typeof(FileStream), ctorParams); - } - - private IntPtr GetOutHandle() - { - switch (ConsoleManager.ConfigConsoleOutRedirectType.Value) - { - case ConsoleManager.ConsoleOutRedirectType.ConsoleOut: - return ConsoleWindow.ConsoleOutHandle; - case ConsoleManager.ConsoleOutRedirectType.StandardOut: - return ConsoleWindow.OriginalStdoutHandle; - case ConsoleManager.ConsoleOutRedirectType.Auto: - default: - return ConsoleWindow.OriginalStdoutHandle != IntPtr.Zero - ? ConsoleWindow.OriginalStdoutHandle - : ConsoleWindow.ConsoleOutHandle; - } + case ConsoleManager.ConsoleOutRedirectType.ConsoleOut: + return ConsoleWindow.ConsoleOutHandle; + case ConsoleManager.ConsoleOutRedirectType.StandardOut: + return ConsoleWindow.OriginalStdoutHandle; + case ConsoleManager.ConsoleOutRedirectType.Auto: + default: + return ConsoleWindow.OriginalStdoutHandle != IntPtr.Zero + ? ConsoleWindow.OriginalStdoutHandle + : ConsoleWindow.ConsoleOutHandle; } } } diff --git a/BepInEx.Core/Contract/Attributes.cs b/BepInEx.Core/Contract/Attributes.cs index 46aeb73..0de9e93 100644 --- a/BepInEx.Core/Contract/Attributes.cs +++ b/BepInEx.Core/Contract/Attributes.cs @@ -8,323 +8,322 @@ using Mono.Cecil; using Range = SemanticVersioning.Range; using Version = SemanticVersioning.Version; -namespace BepInEx +namespace BepInEx; + +#region BaseUnityPlugin + +/// +/// This attribute denotes that a class is a plugin, and specifies the required metadata. +/// +[AttributeUsage(AttributeTargets.Class)] +public class BepInPlugin : Attribute { - #region BaseUnityPlugin + /// 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 BepInPlugin(string GUID, string Name, string Version) + { + this.GUID = GUID; + this.Name = Name; + this.Version = TryParseLongVersion(Version); + } /// - /// This attribute denotes that a class is a plugin, and specifies the required metadata. + /// The unique identifier of the plugin. Should not change between plugin versions. /// - [AttributeUsage(AttributeTargets.Class)] - public class BepInPlugin : Attribute + public string GUID { get; protected set; } + + + /// + /// The user friendly name of the plugin. Is able to be changed between versions. + /// + public string Name { get; protected set; } + + + /// + /// The specific version of the plugin. + /// + public Version Version { get; protected set; } + + private static Version TryParseLongVersion(string version) { - /// 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 BepInPlugin(string GUID, string Name, string Version) + if (Version.TryParse(version, out var v)) + return v; + + // no System.Version.TryParse() on .NET 3.5 + try { - this.GUID = GUID; - this.Name = Name; - this.Version = TryParseLongVersion(Version); + var longVersion = new System.Version(version); + + return new Version(longVersion.Major, longVersion.Minor, + longVersion.Build != -1 ? longVersion.Build : 0); } + catch { } - /// - /// The unique identifier of the plugin. Should not change between plugin versions. - /// - public string GUID { get; protected set; } + return null; + } + internal static BepInPlugin FromCecilType(TypeDefinition td) + { + var attr = MetadataHelper.GetCustomAttributes(td, false).FirstOrDefault(); - /// - /// The user friendly name of the plugin. Is able to be changed between versions. - /// - public string Name { get; protected set; } - - - /// - /// The specific version of the plugin. - /// - public Version Version { get; protected set; } - - private static Version TryParseLongVersion(string version) - { - if (Version.TryParse(version, out var v)) - return v; - - // no System.Version.TryParse() on .NET 3.5 - try - { - var longVersion = new System.Version(version); - - return new Version(longVersion.Major, longVersion.Minor, - longVersion.Build != -1 ? longVersion.Build : 0); - } - catch { } - + if (attr == null) return null; - } - internal static BepInPlugin FromCecilType(TypeDefinition td) - { - var attr = MetadataHelper.GetCustomAttributes(td, false).FirstOrDefault(); - - if (attr == null) - return null; - - return new BepInPlugin((string)attr.ConstructorArguments[0].Value, - (string)attr.ConstructorArguments[1].Value, - (string)attr.ConstructorArguments[2].Value); - } + return new BepInPlugin((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. - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - public class BepInDependency : Attribute, ICacheable - { - /// - /// Flags that are applied to a dependency - /// - [Flags] - public enum DependencyFlags - { - /// - /// The plugin has a hard dependency on the referenced plugin, and will not run without it. - /// - HardDependency = 1, - - /// - /// This plugin has a soft dependency on the referenced plugin, and is able to run without it. - /// - SoftDependency = 2 - } - - /// - /// Marks this as dependent on another plugin. The other plugin will be loaded before - /// this one. - /// If the other plugin doesn't exist, what happens depends on the parameter. - /// - /// The GUID of the referenced plugin. - /// The flags associated with this dependency definition. - public BepInDependency(string DependencyGUID, DependencyFlags Flags = DependencyFlags.HardDependency) - { - this.DependencyGUID = DependencyGUID; - this.Flags = Flags; - VersionRange = null; - } - - /// - /// Marks this as dependent on another plugin. The other plugin will be loaded before - /// this one. - /// If the other plugin doesn't exist or is of a version not satisfying , this plugin will - /// 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 - public BepInDependency(string guid, string version) : this(guid) - { - VersionRange = Range.Parse(version); - } - - /// - /// The GUID of the referenced plugin. - /// - public string DependencyGUID { get; protected set; } - - /// - /// The flags associated with this dependency definition. - /// - public DependencyFlags Flags { get; protected set; } - - /// - /// The version range of the referenced plugin. - /// - public Range VersionRange { get; protected set; } - - void ICacheable.Save(BinaryWriter bw) - { - bw.Write(DependencyGUID); - bw.Write((int)Flags); - bw.Write(VersionRange?.ToString() ?? string.Empty); - } - - void ICacheable.Load(BinaryReader br) - { - DependencyGUID = br.ReadString(); - Flags = (DependencyFlags)br.ReadInt32(); - - var versionRange = br.ReadString(); - VersionRange = versionRange == string.Empty ? null : Range.Parse(versionRange); - } - - internal static IEnumerable FromCecilType(TypeDefinition td) - { - var attrs = MetadataHelper.GetCustomAttributes(td, true); - return attrs.Select(customAttribute => - { - var dependencyGuid = (string)customAttribute.ConstructorArguments[0].Value; - var secondArg = customAttribute.ConstructorArguments[1].Value; - if (secondArg is string minVersion) return new BepInDependency(dependencyGuid, minVersion); - return new BepInDependency(dependencyGuid, (DependencyFlags)secondArg); - }).ToList(); - } - } - - /// - /// This attribute specifies other plugins that are incompatible with this plugin. - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - public class BepInIncompatibility : Attribute, ICacheable - { - /// - /// Marks this as incompatible with another plugin. - /// If the other plugin exists, this plugin will not be loaded and a warning will be shown. - /// - /// The GUID of the referenced plugin. - public BepInIncompatibility(string IncompatibilityGUID) - { - this.IncompatibilityGUID = IncompatibilityGUID; - } - - /// - /// The GUID of the referenced plugin. - /// - public string IncompatibilityGUID { get; protected set; } - - void ICacheable.Save(BinaryWriter bw) => bw.Write(IncompatibilityGUID); - - void ICacheable.Load(BinaryReader br) => IncompatibilityGUID = br.ReadString(); - - internal static IEnumerable FromCecilType(TypeDefinition td) - { - var attrs = MetadataHelper.GetCustomAttributes(td, true); - return attrs.Select(customAttribute => - { - var dependencyGuid = (string)customAttribute.ConstructorArguments[0].Value; - return new BepInIncompatibility(dependencyGuid); - }).ToList(); - } - } - - /// - /// This attribute specifies which processes this plugin should be run for. Not specifying this attribute will load the - /// plugin under every process. - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - public class BepInProcess : Attribute - { - /// The name of the process that this plugin will run under. - public BepInProcess(string ProcessName) - { - this.ProcessName = ProcessName; - } - - /// - /// The name of the process that this plugin will run under. - /// - public string ProcessName { get; protected set; } - - internal static List FromCecilType(TypeDefinition td) - { - var attrs = MetadataHelper.GetCustomAttributes(td, true); - return attrs.Select(customAttribute => - new BepInProcess((string)customAttribute.ConstructorArguments[0].Value)).ToList(); - } - } - - #endregion - - #region MetadataHelper - - /// - /// Helper class to use for retrieving metadata about a plugin, defined as attributes. - /// - public static class MetadataHelper - { - internal static IEnumerable GetCustomAttributes(TypeDefinition td, bool inherit) - where T : Attribute - { - var result = new List(); - var type = typeof(T); - var currentType = td; - - do - { - result.AddRange(currentType.CustomAttributes.Where(ca => ca.AttributeType.FullName == type.FullName)); - currentType = currentType.BaseType?.Resolve(); - } while (inherit && currentType?.FullName != "System.Object"); - - - return result; - } - - /// - /// Retrieves the BepInPlugin metadata from a plugin type. - /// - /// The plugin type. - /// The BepInPlugin metadata of the plugin type. - public static BepInPlugin GetMetadata(Type pluginType) - { - var attributes = pluginType.GetCustomAttributes(typeof(BepInPlugin), false); - - if (attributes.Length == 0) - return null; - - return (BepInPlugin)attributes[0]; - } - - /// - /// Retrieves the BepInPlugin metadata from a plugin instance. - /// - /// The plugin instance. - /// The BepInPlugin metadata of the plugin instance. - public static BepInPlugin GetMetadata(object plugin) => GetMetadata(plugin.GetType()); - - /// - /// Gets the specified attributes of a type, if they exist. - /// - /// The attribute type to retrieve. - /// The plugin type. - /// The attributes of the type, if existing. - public static T[] GetAttributes(Type pluginType) where T : Attribute => - (T[])pluginType.GetCustomAttributes(typeof(T), true); - - /// - /// Gets the specified attributes of an assembly, if they exist. - /// - /// The assembly. - /// The attribute type to retrieve. - /// The attributes of the type, if existing. - public static T[] GetAttributes(Assembly assembly) where T : Attribute => - (T[])assembly.GetCustomAttributes(typeof(T), true); - - /// - /// Gets the specified attributes of an instance, if they exist. - /// - /// The attribute type to retrieve. - /// The plugin instance. - /// The attributes of the instance, if existing. - public static IEnumerable GetAttributes(object plugin) where T : Attribute => - GetAttributes(plugin.GetType()); - - /// - /// Gets the specified attributes of a reflection metadata type, if they exist. - /// - /// The attribute type to retrieve. - /// The reflection metadata instance. - /// The attributes of the instance, if existing. - public static T[] GetAttributes(MemberInfo member) where T : Attribute => - (T[])member.GetCustomAttributes(typeof(T), true); - - /// - /// Retrieves the dependencies of the specified plugin type. - /// - /// The plugin type. - /// A list of all plugin types that the specified plugin type depends upon. - public static IEnumerable GetDependencies(Type plugin) => - plugin.GetCustomAttributes(typeof(BepInDependency), true).Cast(); - } - - #endregion } + +/// +/// This attribute specifies any dependencies that this plugin has on other plugins. +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +public class BepInDependency : Attribute, ICacheable +{ + /// + /// Flags that are applied to a dependency + /// + [Flags] + public enum DependencyFlags + { + /// + /// The plugin has a hard dependency on the referenced plugin, and will not run without it. + /// + HardDependency = 1, + + /// + /// This plugin has a soft dependency on the referenced plugin, and is able to run without it. + /// + SoftDependency = 2 + } + + /// + /// Marks this as dependent on another plugin. The other plugin will be loaded before + /// this one. + /// If the other plugin doesn't exist, what happens depends on the parameter. + /// + /// The GUID of the referenced plugin. + /// The flags associated with this dependency definition. + public BepInDependency(string DependencyGUID, DependencyFlags Flags = DependencyFlags.HardDependency) + { + this.DependencyGUID = DependencyGUID; + this.Flags = Flags; + VersionRange = null; + } + + /// + /// Marks this as dependent on another plugin. The other plugin will be loaded before + /// this one. + /// If the other plugin doesn't exist or is of a version not satisfying , this plugin will + /// 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 + public BepInDependency(string guid, string version) : this(guid) + { + VersionRange = Range.Parse(version); + } + + /// + /// The GUID of the referenced plugin. + /// + public string DependencyGUID { get; protected set; } + + /// + /// The flags associated with this dependency definition. + /// + public DependencyFlags Flags { get; protected set; } + + /// + /// The version range of the referenced plugin. + /// + public Range VersionRange { get; protected set; } + + void ICacheable.Save(BinaryWriter bw) + { + bw.Write(DependencyGUID); + bw.Write((int) Flags); + bw.Write(VersionRange?.ToString() ?? string.Empty); + } + + void ICacheable.Load(BinaryReader br) + { + DependencyGUID = br.ReadString(); + Flags = (DependencyFlags) br.ReadInt32(); + + var versionRange = br.ReadString(); + VersionRange = versionRange == string.Empty ? null : Range.Parse(versionRange); + } + + internal static IEnumerable FromCecilType(TypeDefinition td) + { + var attrs = MetadataHelper.GetCustomAttributes(td, true); + return attrs.Select(customAttribute => + { + var dependencyGuid = (string) customAttribute.ConstructorArguments[0].Value; + var secondArg = customAttribute.ConstructorArguments[1].Value; + if (secondArg is string minVersion) return new BepInDependency(dependencyGuid, minVersion); + return new BepInDependency(dependencyGuid, (DependencyFlags) secondArg); + }).ToList(); + } +} + +/// +/// This attribute specifies other plugins that are incompatible with this plugin. +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +public class BepInIncompatibility : Attribute, ICacheable +{ + /// + /// Marks this as incompatible with another plugin. + /// If the other plugin exists, this plugin will not be loaded and a warning will be shown. + /// + /// The GUID of the referenced plugin. + public BepInIncompatibility(string IncompatibilityGUID) + { + this.IncompatibilityGUID = IncompatibilityGUID; + } + + /// + /// The GUID of the referenced plugin. + /// + public string IncompatibilityGUID { get; protected set; } + + void ICacheable.Save(BinaryWriter bw) => bw.Write(IncompatibilityGUID); + + void ICacheable.Load(BinaryReader br) => IncompatibilityGUID = br.ReadString(); + + internal static IEnumerable FromCecilType(TypeDefinition td) + { + var attrs = MetadataHelper.GetCustomAttributes(td, true); + return attrs.Select(customAttribute => + { + var dependencyGuid = (string) customAttribute.ConstructorArguments[0].Value; + return new BepInIncompatibility(dependencyGuid); + }).ToList(); + } +} + +/// +/// This attribute specifies which processes this plugin should be run for. Not specifying this attribute will load the +/// plugin under every process. +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +public class BepInProcess : Attribute +{ + /// The name of the process that this plugin will run under. + public BepInProcess(string ProcessName) + { + this.ProcessName = ProcessName; + } + + /// + /// The name of the process that this plugin will run under. + /// + public string ProcessName { get; protected set; } + + internal static List FromCecilType(TypeDefinition td) + { + var attrs = MetadataHelper.GetCustomAttributes(td, true); + return attrs.Select(customAttribute => + new BepInProcess((string) customAttribute.ConstructorArguments[0].Value)).ToList(); + } +} + +#endregion + +#region MetadataHelper + +/// +/// Helper class to use for retrieving metadata about a plugin, defined as attributes. +/// +public static class MetadataHelper +{ + internal static IEnumerable GetCustomAttributes(TypeDefinition td, bool inherit) + where T : Attribute + { + var result = new List(); + var type = typeof(T); + var currentType = td; + + do + { + result.AddRange(currentType.CustomAttributes.Where(ca => ca.AttributeType.FullName == type.FullName)); + currentType = currentType.BaseType?.Resolve(); + } while (inherit && currentType?.FullName != "System.Object"); + + + return result; + } + + /// + /// Retrieves the BepInPlugin metadata from a plugin type. + /// + /// The plugin type. + /// The BepInPlugin metadata of the plugin type. + public static BepInPlugin GetMetadata(Type pluginType) + { + var attributes = pluginType.GetCustomAttributes(typeof(BepInPlugin), false); + + if (attributes.Length == 0) + return null; + + return (BepInPlugin) attributes[0]; + } + + /// + /// Retrieves the BepInPlugin metadata from a plugin instance. + /// + /// The plugin instance. + /// The BepInPlugin metadata of the plugin instance. + public static BepInPlugin GetMetadata(object plugin) => GetMetadata(plugin.GetType()); + + /// + /// Gets the specified attributes of a type, if they exist. + /// + /// The attribute type to retrieve. + /// The plugin type. + /// The attributes of the type, if existing. + public static T[] GetAttributes(Type pluginType) where T : Attribute => + (T[]) pluginType.GetCustomAttributes(typeof(T), true); + + /// + /// Gets the specified attributes of an assembly, if they exist. + /// + /// The assembly. + /// The attribute type to retrieve. + /// The attributes of the type, if existing. + public static T[] GetAttributes(Assembly assembly) where T : Attribute => + (T[]) assembly.GetCustomAttributes(typeof(T), true); + + /// + /// Gets the specified attributes of an instance, if they exist. + /// + /// The attribute type to retrieve. + /// The plugin instance. + /// The attributes of the instance, if existing. + public static IEnumerable GetAttributes(object plugin) where T : Attribute => + GetAttributes(plugin.GetType()); + + /// + /// Gets the specified attributes of a reflection metadata type, if they exist. + /// + /// The attribute type to retrieve. + /// The reflection metadata instance. + /// The attributes of the instance, if existing. + public static T[] GetAttributes(MemberInfo member) where T : Attribute => + (T[]) member.GetCustomAttributes(typeof(T), true); + + /// + /// Retrieves the dependencies of the specified plugin type. + /// + /// The plugin type. + /// A list of all plugin types that the specified plugin type depends upon. + public static IEnumerable GetDependencies(Type plugin) => + plugin.GetCustomAttributes(typeof(BepInDependency), true).Cast(); +} + +#endregion diff --git a/BepInEx.Core/Contract/PluginInfo.cs b/BepInEx.Core/Contract/PluginInfo.cs index 636eae8..0aa7585 100644 --- a/BepInEx.Core/Contract/PluginInfo.cs +++ b/BepInEx.Core/Contract/PluginInfo.cs @@ -4,115 +4,114 @@ using System.IO; using System.Linq; using BepInEx.Bootstrap; -namespace BepInEx +namespace BepInEx; + +/// +/// Data class that represents information about a loadable BepInEx plugin. +/// Contains all metadata and additional info required for plugin loading by . +/// +public class PluginInfo : ICacheable { /// - /// Data class that represents information about a loadable BepInEx plugin. - /// Contains all metadata and additional info required for plugin loading by . + /// General metadata about a plugin. /// - public class PluginInfo : ICacheable + public BepInPlugin Metadata { get; internal set; } + + /// + /// Collection of attributes that describe what processes the plugin can run on. + /// + public IEnumerable Processes { get; internal set; } + + /// + /// Collection of attributes that describe what plugins this plugin depends on. + /// + public IEnumerable Dependencies { get; internal set; } + + /// + /// Collection of attributes that describe what plugins this plugin + /// is incompatible with. + /// + public IEnumerable Incompatibilities { 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; } + + void ICacheable.Save(BinaryWriter bw) { - /// - /// General metadata about a plugin. - /// - public BepInPlugin Metadata { get; internal set; } + bw.Write(TypeName); + bw.Write(Location); - /// - /// Collection of attributes that describe what processes the plugin can run on. - /// - public IEnumerable Processes { get; internal set; } + bw.Write(Metadata.GUID); + bw.Write(Metadata.Name); + bw.Write(Metadata.Version.ToString()); - /// - /// Collection of attributes that describe what plugins this plugin depends on. - /// - public IEnumerable Dependencies { get; internal set; } + var processList = Processes.ToList(); + bw.Write(processList.Count); + foreach (var bepInProcess in processList) + bw.Write(bepInProcess.ProcessName); - /// - /// Collection of attributes that describe what plugins this plugin - /// is incompatible with. - /// - public IEnumerable Incompatibilities { get; internal set; } + var depList = Dependencies.ToList(); + bw.Write(depList.Count); + foreach (var bepInDependency in depList) + ((ICacheable) bepInDependency).Save(bw); - /// - /// File path to the plugin DLL - /// - public string Location { get; internal set; } + var incList = Incompatibilities.ToList(); + bw.Write(incList.Count); + foreach (var bepInIncompatibility in incList) + ((ICacheable) bepInIncompatibility).Save(bw); - /// - /// 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; } - - void ICacheable.Save(BinaryWriter bw) - { - bw.Write(TypeName); - bw.Write(Location); - - bw.Write(Metadata.GUID); - bw.Write(Metadata.Name); - bw.Write(Metadata.Version.ToString()); - - var processList = Processes.ToList(); - bw.Write(processList.Count); - foreach (var bepInProcess in processList) - bw.Write(bepInProcess.ProcessName); - - var depList = Dependencies.ToList(); - bw.Write(depList.Count); - foreach (var bepInDependency in depList) - ((ICacheable) bepInDependency).Save(bw); - - var incList = Incompatibilities.ToList(); - bw.Write(incList.Count); - foreach (var bepInIncompatibility in incList) - ((ICacheable) bepInIncompatibility).Save(bw); - - bw.Write(TargettedBepInExVersion.ToString(4)); - } - - void ICacheable.Load(BinaryReader br) - { - TypeName = br.ReadString(); - Location = br.ReadString(); - - Metadata = new BepInPlugin(br.ReadString(), br.ReadString(), br.ReadString()); - - var processListCount = br.ReadInt32(); - var processList = new List(processListCount); - for (var i = 0; i < processListCount; i++) - processList.Add(new BepInProcess(br.ReadString())); - Processes = processList; - - var depCount = br.ReadInt32(); - var depList = new List(depCount); - for (var i = 0; i < depCount; i++) - { - var dep = new BepInDependency(""); - ((ICacheable) dep).Load(br); - depList.Add(dep); - } - - Dependencies = depList; - - var incCount = br.ReadInt32(); - var incList = new List(incCount); - for (var i = 0; i < incCount; i++) - { - var inc = new BepInIncompatibility(""); - ((ICacheable) inc).Load(br); - incList.Add(inc); - } - - Incompatibilities = incList; - - TargettedBepInExVersion = new Version(br.ReadString()); - } - - /// - public override string ToString() => $"{Metadata?.Name} {Metadata?.Version}"; + bw.Write(TargettedBepInExVersion.ToString(4)); } + + void ICacheable.Load(BinaryReader br) + { + TypeName = br.ReadString(); + Location = br.ReadString(); + + Metadata = new BepInPlugin(br.ReadString(), br.ReadString(), br.ReadString()); + + var processListCount = br.ReadInt32(); + var processList = new List(processListCount); + for (var i = 0; i < processListCount; i++) + processList.Add(new BepInProcess(br.ReadString())); + Processes = processList; + + var depCount = br.ReadInt32(); + var depList = new List(depCount); + for (var i = 0; i < depCount; i++) + { + var dep = new BepInDependency(""); + ((ICacheable) dep).Load(br); + depList.Add(dep); + } + + Dependencies = depList; + + var incCount = br.ReadInt32(); + var incList = new List(incCount); + for (var i = 0; i < incCount; i++) + { + var inc = new BepInIncompatibility(""); + ((ICacheable) inc).Load(br); + incList.Add(inc); + } + + Incompatibilities = incList; + + TargettedBepInExVersion = new Version(br.ReadString()); + } + + /// + public override string ToString() => $"{Metadata?.Name} {Metadata?.Version}"; } diff --git a/BepInEx.Core/Logging/ConsoleLogListener.cs b/BepInEx.Core/Logging/ConsoleLogListener.cs index 0ea249d..09ea187 100644 --- a/BepInEx.Core/Logging/ConsoleLogListener.cs +++ b/BepInEx.Core/Logging/ConsoleLogListener.cs @@ -1,30 +1,29 @@ using System; using BepInEx.Configuration; -namespace BepInEx.Logging +namespace BepInEx.Logging; + +/// +/// Logs entries using a console spawned by BepInEx. +/// +public class ConsoleLogListener : ILogListener { - /// - /// Logs entries using a console spawned by BepInEx. - /// - public class ConsoleLogListener : ILogListener + protected static readonly ConfigEntry ConfigConsoleDisplayedLevel = ConfigFile.CoreConfig.Bind( + "Logging.Console", "LogLevels", + LogLevel.Fatal | LogLevel.Error | LogLevel.Warning | LogLevel.Message | LogLevel.Info, + "Only displays the specified log levels in the console output."); + + /// + public void LogEvent(object sender, LogEventArgs eventArgs) { - protected static readonly ConfigEntry ConfigConsoleDisplayedLevel = ConfigFile.CoreConfig.Bind( - "Logging.Console", "LogLevels", - LogLevel.Fatal | LogLevel.Error | LogLevel.Warning | LogLevel.Message | LogLevel.Info, - "Only displays the specified log levels in the console output."); + if ((eventArgs.Level & ConfigConsoleDisplayedLevel.Value) == 0) + return; - /// - public void LogEvent(object sender, LogEventArgs eventArgs) - { - if ((eventArgs.Level & ConfigConsoleDisplayedLevel.Value) == 0) - return; - - ConsoleManager.SetConsoleColor(eventArgs.Level.GetConsoleColor()); - ConsoleManager.ConsoleStream?.Write(eventArgs.ToStringLine()); - ConsoleManager.SetConsoleColor(ConsoleColor.Gray); - } - - /// - public void Dispose() { } + ConsoleManager.SetConsoleColor(eventArgs.Level.GetConsoleColor()); + ConsoleManager.ConsoleStream?.Write(eventArgs.ToStringLine()); + ConsoleManager.SetConsoleColor(ConsoleColor.Gray); } + + /// + public void Dispose() { } } diff --git a/BepInEx.Core/Logging/DiskLogListener.cs b/BepInEx.Core/Logging/DiskLogListener.cs index b5b8beb..8ff41ea 100644 --- a/BepInEx.Core/Logging/DiskLogListener.cs +++ b/BepInEx.Core/Logging/DiskLogListener.cs @@ -5,112 +5,111 @@ using System.Text; using System.Threading; using Mono.Cecil; -namespace BepInEx.Logging +namespace BepInEx.Logging; + +/// +/// Logs entries using Unity specific outputs. +/// +public class DiskLogListener : ILogListener { + public static HashSet BlacklistedSources = new(); + /// - /// Logs entries using Unity specific outputs. + /// Creates a new disk log listener. /// - public class DiskLogListener : ILogListener + /// Path to the log. + /// Log levels to display. + /// Whether to append logs to an already existing log file. + /// + /// Whether to delay flushing to disk to improve performance. Useful to set this to false + /// when debugging crashes. + /// + /// Maximum amount of concurrently opened log files. Can help with infinite game boot loops. + public DiskLogListener(string localPath, + LogLevel displayedLogLevel = LogLevel.Info, + bool appendLog = false, + bool delayedFlushing = true, + int fileLimit = 5) { - public static HashSet BlacklistedSources = new(); + DisplayedLogLevel = displayedLogLevel; - /// - /// Creates a new disk log listener. - /// - /// Path to the log. - /// Log levels to display. - /// Whether to append logs to an already existing log file. - /// - /// Whether to delay flushing to disk to improve performance. Useful to set this to false - /// when debugging crashes. - /// - /// Maximum amount of concurrently opened log files. Can help with infinite game boot loops. - public DiskLogListener(string localPath, - LogLevel displayedLogLevel = LogLevel.Info, - bool appendLog = false, - bool delayedFlushing = true, - int fileLimit = 5) + var counter = 1; + + FileStream fileStream; + + while (!Utility.TryOpenFileStream(Path.Combine(Paths.BepInExRootPath, localPath), + appendLog ? FileMode.Append : FileMode.Create, out fileStream, + share: FileShare.Read, access: FileAccess.Write)) { - DisplayedLogLevel = displayedLogLevel; - - var counter = 1; - - FileStream fileStream; - - while (!Utility.TryOpenFileStream(Path.Combine(Paths.BepInExRootPath, localPath), - appendLog ? FileMode.Append : FileMode.Create, out fileStream, - share: FileShare.Read, access: FileAccess.Write)) + if (counter == fileLimit) { - if (counter == fileLimit) - { - Logger.LogError("Couldn't open a log file for writing. Skipping log file creation"); + Logger.LogError("Couldn't open a log file for writing. Skipping log file creation"); - return; - } - - Logger.LogWarning($"Couldn't open log file '{localPath}' for writing, trying another..."); - - localPath = $"LogOutput.{counter++}.log"; + return; } - LogWriter = TextWriter.Synchronized(new StreamWriter(fileStream, Utility.UTF8NoBom)); + Logger.LogWarning($"Couldn't open log file '{localPath}' for writing, trying another..."); - if (delayedFlushing) FlushTimer = new Timer(o => { LogWriter?.Flush(); }, null, 2000, 2000); - - InstantFlushing = !delayedFlushing; + localPath = $"LogOutput.{counter++}.log"; } - /// - /// Log levels to display. - /// - public LogLevel DisplayedLogLevel { get; set; } + LogWriter = TextWriter.Synchronized(new StreamWriter(fileStream, Utility.UTF8NoBom)); - /// - /// Writer for the disk log. - /// - public TextWriter LogWriter { get; protected set; } + if (delayedFlushing) FlushTimer = new Timer(o => { LogWriter?.Flush(); }, null, 2000, 2000); - /// - /// Timer for flushing the logs to a file. - /// - private Timer FlushTimer { get; } + InstantFlushing = !delayedFlushing; + } - private bool InstantFlushing { get; } + /// + /// Log levels to display. + /// + public LogLevel DisplayedLogLevel { get; set; } - /// - public void LogEvent(object sender, LogEventArgs eventArgs) + /// + /// Writer for the disk log. + /// + public TextWriter LogWriter { get; protected set; } + + /// + /// Timer for flushing the logs to a file. + /// + private Timer FlushTimer { get; } + + private bool InstantFlushing { get; } + + /// + public void LogEvent(object sender, LogEventArgs eventArgs) + { + if (LogWriter == null) + return; + + if (BlacklistedSources.Contains(eventArgs.Source.SourceName)) + return; + + if ((eventArgs.Level & DisplayedLogLevel) == 0) + return; + + LogWriter.WriteLine(eventArgs.ToString()); + + if (InstantFlushing) + LogWriter.Flush(); + } + + /// + public void Dispose() + { + FlushTimer?.Dispose(); + + try { - if (LogWriter == null) - return; - - if (BlacklistedSources.Contains(eventArgs.Source.SourceName)) - return; - - if ((eventArgs.Level & DisplayedLogLevel) == 0) - return; - - LogWriter.WriteLine(eventArgs.ToString()); - - if (InstantFlushing) - LogWriter.Flush(); + LogWriter?.Flush(); + LogWriter?.Dispose(); } + catch (ObjectDisposedException) { } + } - /// - public void Dispose() - { - FlushTimer?.Dispose(); - - try - { - LogWriter?.Flush(); - LogWriter?.Dispose(); - } - catch (ObjectDisposedException) { } - } - - ~DiskLogListener() - { - Dispose(); - } + ~DiskLogListener() + { + Dispose(); } } diff --git a/BepInEx.Core/Logging/HarmonyLogSource.cs b/BepInEx.Core/Logging/HarmonyLogSource.cs index e12fd34..f3a428e 100644 --- a/BepInEx.Core/Logging/HarmonyLogSource.cs +++ b/BepInEx.Core/Logging/HarmonyLogSource.cs @@ -3,41 +3,40 @@ using System.Collections.Generic; using BepInEx.Configuration; using HarmonyLogger = HarmonyLib.Tools.Logger; -namespace BepInEx.Logging +namespace BepInEx.Logging; + +public class HarmonyLogSource : ILogSource { - public class HarmonyLogSource : ILogSource + private static readonly ConfigEntry LogChannels = ConfigFile.CoreConfig.Bind( + "Harmony.Logger", + "LogChannels", + HarmonyLogger.LogChannel.Warn | HarmonyLogger.LogChannel.Error, + "Specifies which Harmony log channels to listen to.\nNOTE: IL channel dumps the whole patch methods, use only when needed!"); + + private static readonly Dictionary LevelMap = new() { - private static readonly ConfigEntry LogChannels = ConfigFile.CoreConfig.Bind( - "Harmony.Logger", - "LogChannels", - HarmonyLogger.LogChannel.Warn | HarmonyLogger.LogChannel.Error, - "Specifies which Harmony log channels to listen to.\nNOTE: IL channel dumps the whole patch methods, use only when needed!"); + [HarmonyLogger.LogChannel.Info] = LogLevel.Info, + [HarmonyLogger.LogChannel.Warn] = LogLevel.Warning, + [HarmonyLogger.LogChannel.Error] = LogLevel.Error, + [HarmonyLogger.LogChannel.IL] = LogLevel.Debug + }; - private static readonly Dictionary LevelMap = new() - { - [HarmonyLogger.LogChannel.Info] = LogLevel.Info, - [HarmonyLogger.LogChannel.Warn] = LogLevel.Warning, - [HarmonyLogger.LogChannel.Error] = LogLevel.Error, - [HarmonyLogger.LogChannel.IL] = LogLevel.Debug - }; + public HarmonyLogSource() + { + HarmonyLogger.ChannelFilter = LogChannels.Value; + HarmonyLogger.MessageReceived += HandleHarmonyMessage; + } - public HarmonyLogSource() - { - HarmonyLogger.ChannelFilter = LogChannels.Value; - HarmonyLogger.MessageReceived += HandleHarmonyMessage; - } + public void Dispose() => HarmonyLogger.MessageReceived -= HandleHarmonyMessage; - public void Dispose() => HarmonyLogger.MessageReceived -= HandleHarmonyMessage; + public string SourceName { get; } = "HarmonyX"; + public event EventHandler LogEvent; - public string SourceName { get; } = "HarmonyX"; - public event EventHandler LogEvent; + private void HandleHarmonyMessage(object sender, HarmonyLogger.LogEventArgs e) + { + if (!LevelMap.TryGetValue(e.LogChannel, out var level)) + return; - private void HandleHarmonyMessage(object sender, HarmonyLogger.LogEventArgs e) - { - if (!LevelMap.TryGetValue(e.LogChannel, out var level)) - return; - - LogEvent?.Invoke(this, new LogEventArgs(e.Message, level, this)); - } + LogEvent?.Invoke(this, new LogEventArgs(e.Message, level, this)); } } diff --git a/BepInEx.Core/Logging/ILogListener.cs b/BepInEx.Core/Logging/ILogListener.cs index 7ac7b68..2234323 100644 --- a/BepInEx.Core/Logging/ILogListener.cs +++ b/BepInEx.Core/Logging/ILogListener.cs @@ -1,17 +1,16 @@ using System; -namespace BepInEx.Logging +namespace BepInEx.Logging; + +/// +/// A generic log listener that receives log events and can route them to some output (e.g. file, console, socket). +/// +public interface ILogListener : IDisposable { /// - /// A generic log listener that receives log events and can route them to some output (e.g. file, console, socket). + /// Handle an incoming log event. /// - public interface ILogListener : IDisposable - { - /// - /// Handle an incoming log event. - /// - /// Log source that sent the event. Don't use; instead use - /// Information about the log message. - void LogEvent(object sender, LogEventArgs eventArgs); - } + /// Log source that sent the event. Don't use; instead use + /// Information about the log message. + void LogEvent(object sender, LogEventArgs eventArgs); } diff --git a/BepInEx.Core/Logging/ILogSource.cs b/BepInEx.Core/Logging/ILogSource.cs index ca55a66..b07cbee 100644 --- a/BepInEx.Core/Logging/ILogSource.cs +++ b/BepInEx.Core/Logging/ILogSource.cs @@ -1,20 +1,19 @@ using System; -namespace BepInEx.Logging +namespace BepInEx.Logging; + +/// +/// Log source that can output log messages. +/// +public interface ILogSource : IDisposable { /// - /// Log source that can output log messages. + /// Name of the log source. /// - public interface ILogSource : IDisposable - { - /// - /// Name of the log source. - /// - string SourceName { get; } + string SourceName { get; } - /// - /// Event that sends the log message. Call to send a log message. - /// - event EventHandler LogEvent; - } + /// + /// Event that sends the log message. Call to send a log message. + /// + event EventHandler LogEvent; } diff --git a/BepInEx.Core/Logging/LogEventArgs.cs b/BepInEx.Core/Logging/LogEventArgs.cs index c821f89..26760eb 100644 --- a/BepInEx.Core/Logging/LogEventArgs.cs +++ b/BepInEx.Core/Logging/LogEventArgs.cs @@ -1,47 +1,46 @@ using System; -namespace BepInEx.Logging +namespace BepInEx.Logging; + +/// +/// Log event arguments. Contains info about the log message. +/// +public class LogEventArgs : EventArgs { /// - /// Log event arguments. Contains info about the log message. + /// Creates the log event args- /// - public class LogEventArgs : EventArgs + /// Logged data. + /// Log level of the data. + /// Log source that emits these args. + public LogEventArgs(object data, LogLevel level, ILogSource source) { - /// - /// Creates the log event args- - /// - /// Logged data. - /// Log level of the data. - /// Log source that emits these args. - public LogEventArgs(object data, LogLevel level, ILogSource source) - { - Data = data; - Level = level; - Source = source; - } - - /// - /// Logged data. - /// - public object Data { get; } - - /// - /// Log levels for the data. - /// - public LogLevel Level { get; } - - /// - /// Log source that emitted the log event. - /// - public ILogSource Source { get; } - - /// - public override string ToString() => $"[{Level,-7}:{Source.SourceName,10}] {Data}"; - - /// - /// Like but appends newline at the end. - /// - /// Same output as but with new line. - public string ToStringLine() => $"[{Level,-7}:{Source.SourceName,10}] {Data}{Environment.NewLine}"; + Data = data; + Level = level; + Source = source; } + + /// + /// Logged data. + /// + public object Data { get; } + + /// + /// Log levels for the data. + /// + public LogLevel Level { get; } + + /// + /// Log source that emitted the log event. + /// + public ILogSource Source { get; } + + /// + public override string ToString() => $"[{Level,-7}:{Source.SourceName,10}] {Data}"; + + /// + /// Like but appends newline at the end. + /// + /// Same output as but with new line. + public string ToStringLine() => $"[{Level,-7}:{Source.SourceName,10}] {Data}{Environment.NewLine}"; } diff --git a/BepInEx.Core/Logging/LogLevel.cs b/BepInEx.Core/Logging/LogLevel.cs index c24d213..bfd3ee5 100644 --- a/BepInEx.Core/Logging/LogLevel.cs +++ b/BepInEx.Core/Logging/LogLevel.cs @@ -1,94 +1,93 @@ using System; -namespace BepInEx.Logging +namespace BepInEx.Logging; + +/// +/// The level, or severity of a log entry. +/// +[Flags] +public enum LogLevel { /// - /// The level, or severity of a log entry. + /// No level selected. /// - [Flags] - public enum LogLevel + None = 0, + + /// + /// A fatal error has occurred, which cannot be recovered from. + /// + Fatal = 1, + + /// + /// An error has occured, but can be recovered from. + /// + Error = 2, + + /// + /// A warning has been produced, but does not necessarily mean that something wrong has happened. + /// + Warning = 4, + + /// + /// An important message that should be displayed to the user. + /// + Message = 8, + + /// + /// A message of low importance. + /// + Info = 16, + + /// + /// A message that would likely only interest a developer. + /// + Debug = 32, + + /// + /// All log levels. + /// + All = Fatal | Error | Warning | Message | Info | Debug +} + +/// +/// Helper methods for log level handling. +/// +public static class LogLevelExtensions +{ + /// + /// Gets the highest log level when there could potentially be multiple levels provided. + /// + /// The log level(s). + /// The highest log level supplied. + public static LogLevel GetHighestLevel(this LogLevel levels) { - /// - /// No level selected. - /// - None = 0, + var enums = Enum.GetValues(typeof(LogLevel)); + Array.Sort(enums); - /// - /// A fatal error has occurred, which cannot be recovered from. - /// - Fatal = 1, + foreach (LogLevel e in enums) + if ((levels & e) != LogLevel.None) + return e; - /// - /// An error has occured, but can be recovered from. - /// - Error = 2, - - /// - /// A warning has been produced, but does not necessarily mean that something wrong has happened. - /// - Warning = 4, - - /// - /// An important message that should be displayed to the user. - /// - Message = 8, - - /// - /// A message of low importance. - /// - Info = 16, - - /// - /// A message that would likely only interest a developer. - /// - Debug = 32, - - /// - /// All log levels. - /// - All = Fatal | Error | Warning | Message | Info | Debug + return LogLevel.None; } /// - /// Helper methods for log level handling. + /// Returns a translation of a log level to it's associated console colour. /// - public static class LogLevelExtensions + /// The log level(s). + /// A console color associated with the highest log level supplied. + public static ConsoleColor GetConsoleColor(this LogLevel level) { - /// - /// Gets the highest log level when there could potentially be multiple levels provided. - /// - /// The log level(s). - /// The highest log level supplied. - public static LogLevel GetHighestLevel(this LogLevel levels) + level = GetHighestLevel(level); + return level switch { - var enums = Enum.GetValues(typeof(LogLevel)); - Array.Sort(enums); - - foreach (LogLevel e in enums) - if ((levels & e) != LogLevel.None) - return e; - - return LogLevel.None; - } - - /// - /// Returns a translation of a log level to it's associated console colour. - /// - /// The log level(s). - /// A console color associated with the highest log level supplied. - public static ConsoleColor GetConsoleColor(this LogLevel level) - { - level = GetHighestLevel(level); - return level switch - { - LogLevel.Fatal => ConsoleColor.Red, - LogLevel.Error => ConsoleColor.DarkRed, - LogLevel.Warning => ConsoleColor.Yellow, - LogLevel.Message => ConsoleColor.White, - LogLevel.Info => ConsoleColor.DarkGray, - LogLevel.Debug => ConsoleColor.DarkGray, - _ => ConsoleColor.Gray - }; - } + LogLevel.Fatal => ConsoleColor.Red, + LogLevel.Error => ConsoleColor.DarkRed, + LogLevel.Warning => ConsoleColor.Yellow, + LogLevel.Message => ConsoleColor.White, + LogLevel.Info => ConsoleColor.DarkGray, + LogLevel.Debug => ConsoleColor.DarkGray, + _ => ConsoleColor.Gray + }; } } diff --git a/BepInEx.Core/Logging/Logger.cs b/BepInEx.Core/Logging/Logger.cs index d9a8300..66e4949 100644 --- a/BepInEx.Core/Logging/Logger.cs +++ b/BepInEx.Core/Logging/Logger.cs @@ -1,96 +1,95 @@ using System; using System.Collections.Generic; -namespace BepInEx.Logging +namespace BepInEx.Logging; + +/// +/// Handles pub-sub event marshalling across all log listeners and sources. +/// +public static class Logger { + private static readonly ManualLogSource InternalLogSource; + /// - /// Handles pub-sub event marshalling across all log listeners and sources. + /// Collection of all log listeners that receive log events. /// - public static class Logger + public static ICollection Listeners { get; } + + /// + /// Collection of all log source that output log events. + /// + public static ICollection Sources { get; } + + static Logger() { - private static readonly ManualLogSource InternalLogSource; + Sources = new LogSourceCollection(); + Listeners = new List(); - /// - /// Collection of all log listeners that receive log events. - /// - public static ICollection Listeners { get; } + InternalLogSource = CreateLogSource("BepInEx"); + } - /// - /// Collection of all log source that output log events. - /// - public static ICollection Sources { get; } + internal static void InternalLogEvent(object sender, LogEventArgs eventArgs) + { + foreach (var listener in Listeners) listener?.LogEvent(sender, eventArgs); + } - static Logger() + /// + /// Logs an entry to the internal logger instance. + /// + /// The level of the entry. + /// The data of the entry. + internal static void Log(LogLevel level, object data) => InternalLogSource.Log(level, data); + + internal static void LogFatal(object data) => Log(LogLevel.Fatal, data); + internal static void LogError(object data) => Log(LogLevel.Error, data); + internal static void LogWarning(object data) => Log(LogLevel.Warning, data); + internal static void LogMessage(object data) => Log(LogLevel.Message, data); + internal static void LogInfo(object data) => Log(LogLevel.Info, data); + internal static void LogDebug(object data) => Log(LogLevel.Debug, data); + + /// + /// Creates a new log source with a name and attaches it to . + /// + /// Name of the log source to create. + /// An instance of that allows to write logs. + public static ManualLogSource CreateLogSource(string sourceName) + { + var source = new ManualLogSource(sourceName); + + Sources.Add(source); + + return source; + } + + + private class LogSourceCollection : List, ICollection + { + void ICollection.Add(ILogSource item) { - Sources = new LogSourceCollection(); - Listeners = new List(); + if (item == null) + throw new ArgumentNullException(nameof(item), + "Log sources cannot be null when added to the source list."); - InternalLogSource = CreateLogSource("BepInEx"); + item.LogEvent += InternalLogEvent; + + base.Add(item); } - internal static void InternalLogEvent(object sender, LogEventArgs eventArgs) + void ICollection.Clear() { - foreach (var listener in Listeners) listener?.LogEvent(sender, eventArgs); - } - - /// - /// Logs an entry to the internal logger instance. - /// - /// The level of the entry. - /// The data of the entry. - internal static void Log(LogLevel level, object data) => InternalLogSource.Log(level, data); - - internal static void LogFatal(object data) => Log(LogLevel.Fatal, data); - internal static void LogError(object data) => Log(LogLevel.Error, data); - internal static void LogWarning(object data) => Log(LogLevel.Warning, data); - internal static void LogMessage(object data) => Log(LogLevel.Message, data); - internal static void LogInfo(object data) => Log(LogLevel.Info, data); - internal static void LogDebug(object data) => Log(LogLevel.Debug, data); - - /// - /// Creates a new log source with a name and attaches it to . - /// - /// Name of the log source to create. - /// An instance of that allows to write logs. - public static ManualLogSource CreateLogSource(string sourceName) - { - var source = new ManualLogSource(sourceName); - - Sources.Add(source); - - return source; - } - - - private class LogSourceCollection : List, ICollection - { - void ICollection.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); - } - - void ICollection.Clear() - { - foreach (var item in this) - item.LogEvent -= InternalLogEvent; - - base.Clear(); - } - - bool ICollection.Remove(ILogSource item) - { - if (item == null || !Remove(item)) - return false; - + foreach (var item in this) item.LogEvent -= InternalLogEvent; - return true; - } + + base.Clear(); + } + + bool ICollection.Remove(ILogSource item) + { + if (item == null || !Remove(item)) + return false; + + item.LogEvent -= InternalLogEvent; + return true; } } } diff --git a/BepInEx.Core/Logging/ManualLogSource.cs b/BepInEx.Core/Logging/ManualLogSource.cs index 40eac75..4b9e767 100644 --- a/BepInEx.Core/Logging/ManualLogSource.cs +++ b/BepInEx.Core/Logging/ManualLogSource.cs @@ -1,71 +1,70 @@ using System; -namespace BepInEx.Logging +namespace BepInEx.Logging; + +/// +/// A generic, multi-purpose log source. Exposes simple API to manually emit logs. +/// +public class ManualLogSource : ILogSource { /// - /// A generic, multi-purpose log source. Exposes simple API to manually emit logs. + /// Creates a manual log source. /// - public class ManualLogSource : ILogSource + /// Name of the log source. + public ManualLogSource(string sourceName) { - /// - /// Creates a manual log source. - /// - /// Name of the log source. - public ManualLogSource(string sourceName) - { - SourceName = sourceName; - } - - /// - public string SourceName { get; } - - /// - public event EventHandler LogEvent; - - /// - public void Dispose() { } - - /// - /// Logs a message with the specified log level. - /// - /// Log levels to attach to the message. Multiple can be used with bitwise ORing. - /// Data to log. - public void Log(LogLevel level, object data) => LogEvent?.Invoke(this, new LogEventArgs(data, level, this)); - - /// - /// Logs a message with level. - /// - /// Data to log. - public void LogFatal(object data) => Log(LogLevel.Fatal, data); - - /// - /// Logs a message with level. - /// - /// Data to log. - public void LogError(object data) => Log(LogLevel.Error, data); - - /// - /// Logs a message with level. - /// - /// Data to log. - public void LogWarning(object data) => Log(LogLevel.Warning, data); - - /// - /// Logs a message with level. - /// - /// Data to log. - public void LogMessage(object data) => Log(LogLevel.Message, data); - - /// - /// Logs a message with level. - /// - /// Data to log. - public void LogInfo(object data) => Log(LogLevel.Info, data); - - /// - /// Logs a message with level. - /// - /// Data to log. - public void LogDebug(object data) => Log(LogLevel.Debug, data); + SourceName = sourceName; } + + /// + public string SourceName { get; } + + /// + public event EventHandler LogEvent; + + /// + public void Dispose() { } + + /// + /// Logs a message with the specified log level. + /// + /// Log levels to attach to the message. Multiple can be used with bitwise ORing. + /// Data to log. + public void Log(LogLevel level, object data) => LogEvent?.Invoke(this, new LogEventArgs(data, level, this)); + + /// + /// Logs a message with level. + /// + /// Data to log. + public void LogFatal(object data) => Log(LogLevel.Fatal, data); + + /// + /// Logs a message with level. + /// + /// Data to log. + public void LogError(object data) => Log(LogLevel.Error, data); + + /// + /// Logs a message with level. + /// + /// Data to log. + public void LogWarning(object data) => Log(LogLevel.Warning, data); + + /// + /// Logs a message with level. + /// + /// Data to log. + public void LogMessage(object data) => Log(LogLevel.Message, data); + + /// + /// Logs a message with level. + /// + /// Data to log. + public void LogInfo(object data) => Log(LogLevel.Info, data); + + /// + /// Logs a message with level. + /// + /// Data to log. + public void LogDebug(object data) => Log(LogLevel.Debug, data); } diff --git a/BepInEx.Core/Logging/TraceLogSource.cs b/BepInEx.Core/Logging/TraceLogSource.cs index 6a3779f..964d856 100644 --- a/BepInEx.Core/Logging/TraceLogSource.cs +++ b/BepInEx.Core/Logging/TraceLogSource.cs @@ -1,86 +1,85 @@ using System.Diagnostics; -namespace BepInEx.Logging +namespace BepInEx.Logging; + +/// +/// A source that routes all logs from the inbuilt .NET API to the BepInEx logging system. +/// +/// +public class TraceLogSource : TraceListener { + private static TraceLogSource traceListener; + /// - /// A source that routes all logs from the inbuilt .NET API to the BepInEx logging system. + /// Creates a new trace log source. /// - /// - public class TraceLogSource : TraceListener + protected TraceLogSource() { - private static TraceLogSource traceListener; + LogSource = new ManualLogSource("Trace"); + } - /// - /// Creates a new trace log source. - /// - protected TraceLogSource() + /// + /// Whether Trace logs are currently being rerouted. + /// + public static bool IsListening { get; private set; } + + /// + /// Internal log source. + /// + protected ManualLogSource LogSource { get; } + + /// + /// Creates a new trace log source. + /// + /// New log source (or already existing one). + public static ILogSource CreateSource() + { + if (traceListener == null) { - LogSource = new ManualLogSource("Trace"); + traceListener = new TraceLogSource(); + Trace.Listeners.Add(traceListener); + IsListening = true; } - /// - /// Whether Trace logs are currently being rerouted. - /// - public static bool IsListening { get; private set; } + return traceListener.LogSource; + } - /// - /// Internal log source. - /// - protected ManualLogSource LogSource { get; } + /// + /// Writes a message to the underlying instance. + /// + /// The message to write. + public override void Write(string message) => LogSource.LogInfo(message); - /// - /// Creates a new trace log source. - /// - /// New log source (or already existing one). - public static ILogSource CreateSource() + /// + /// Writes a message and a newline to the underlying instance. + /// + /// The message to write. + public override void WriteLine(string message) => LogSource.LogInfo(message); + + /// + public override void TraceEvent(TraceEventCache eventCache, + string source, + TraceEventType eventType, + int id, + string format, + params object[] args) => + TraceEvent(eventCache, source, eventType, id, string.Format(format, args)); + + /// + public override void TraceEvent(TraceEventCache eventCache, + string source, + TraceEventType eventType, + int id, + string message) + { + var level = eventType switch { - if (traceListener == null) - { - traceListener = new TraceLogSource(); - Trace.Listeners.Add(traceListener); - IsListening = true; - } - - return traceListener.LogSource; - } - - /// - /// Writes a message to the underlying instance. - /// - /// The message to write. - public override void Write(string message) => LogSource.LogInfo(message); - - /// - /// Writes a message and a newline to the underlying instance. - /// - /// The message to write. - public override void WriteLine(string message) => LogSource.LogInfo(message); - - /// - public override void TraceEvent(TraceEventCache eventCache, - string source, - TraceEventType eventType, - int id, - string format, - params object[] args) => - TraceEvent(eventCache, source, eventType, id, string.Format(format, args)); - - /// - public override void TraceEvent(TraceEventCache eventCache, - string source, - TraceEventType eventType, - int id, - string message) - { - var level = eventType switch - { - TraceEventType.Critical => LogLevel.Fatal, - TraceEventType.Error => LogLevel.Error, - TraceEventType.Warning => LogLevel.Warning, - TraceEventType.Information => LogLevel.Info, - _ => LogLevel.Debug - }; - LogSource.Log(level, $"{message}".Trim()); - } + TraceEventType.Critical => LogLevel.Fatal, + TraceEventType.Error => LogLevel.Error, + TraceEventType.Warning => LogLevel.Warning, + TraceEventType.Information => LogLevel.Info, + _ => LogLevel.Debug + }; + LogSource.Log(level, $"{message}".Trim()); } } diff --git a/BepInEx.Core/Paths.cs b/BepInEx.Core/Paths.cs index 1dddda7..2634448 100644 --- a/BepInEx.Core/Paths.cs +++ b/BepInEx.Core/Paths.cs @@ -4,112 +4,111 @@ using System.Reflection; using MonoMod.Utils; using SemanticVersioning; -namespace BepInEx +namespace BepInEx; + +/// +/// Paths used by BepInEx +/// +public static class Paths { + public static Version BepInExVersion { get; } = + Version.Parse(MetadataHelper.GetAttributes(typeof(Paths).Assembly)[0] + .InformationalVersion); + /// - /// Paths used by BepInEx + /// The path to the Managed folder of the currently running Unity game. /// - public static class Paths + public static string ManagedPath { get; private set; } + + /// + /// The directory that the core BepInEx DLLs reside in. + /// + public static string BepInExAssemblyDirectory { get; private set; } + + /// + /// The path to the core BepInEx DLL. + /// + public static string BepInExAssemblyPath { get; private set; } + + /// + /// The path to the main BepInEx folder. + /// + public static string BepInExRootPath { get; private set; } + + /// + /// The path of the currently executing program BepInEx is encapsulated in. + /// + public static string ExecutablePath { get; private set; } + + /// + /// The directory that the currently executing process resides in. + /// On OSX however, this is the parent directory of the game.app folder. + /// + public static string GameRootPath { get; private set; } + + /// + /// The path to the config directory. + /// + public static string ConfigPath { get; private set; } + + /// + /// The path to the global BepInEx configuration file. + /// + public static string BepInExConfigPath { get; private set; } + + /// + /// The path to temporary cache files. + /// + public static string CachePath { get; private set; } + + /// + /// The path to the patcher plugin folder which resides in the BepInEx folder. + /// + public static string PatcherPluginPath { get; private set; } + + /// + /// The path to the plugin folder which resides in the BepInEx folder. + /// + /// This is ONLY guaranteed to be set correctly when Chainloader has been initialized. + /// + /// + public static string PluginPath { get; private set; } + + /// + /// The name of the currently executing process. + /// + public static string ProcessName { get; private set; } + + /// + /// List of directories from where Mono will search assemblies before assembly resolving is invoked. + /// + public static string[] DllSearchPaths { get; private set; } + + public static void SetExecutablePath(string executablePath, + string bepinRootPath = null, + string managedPath = null, + string[] dllSearchPath = null) { - public static Version BepInExVersion { get; } = - Version.Parse(MetadataHelper.GetAttributes(typeof(Paths).Assembly)[0] - .InformationalVersion); + ExecutablePath = executablePath; + ProcessName = Path.GetFileNameWithoutExtension(executablePath); - /// - /// The path to the Managed folder of the currently running Unity game. - /// - public static string ManagedPath { get; private set; } + GameRootPath = PlatformHelper.Is(Platform.MacOS) + ? Utility.ParentDirectory(executablePath, 4) + : Path.GetDirectoryName(executablePath); - /// - /// The directory that the core BepInEx DLLs reside in. - /// - public static string BepInExAssemblyDirectory { get; private set; } - - /// - /// The path to the core BepInEx DLL. - /// - public static string BepInExAssemblyPath { get; private set; } - - /// - /// The path to the main BepInEx folder. - /// - public static string BepInExRootPath { get; private set; } - - /// - /// The path of the currently executing program BepInEx is encapsulated in. - /// - public static string ExecutablePath { get; private set; } - - /// - /// The directory that the currently executing process resides in. - /// On OSX however, this is the parent directory of the game.app folder. - /// - public static string GameRootPath { get; private set; } - - /// - /// The path to the config directory. - /// - public static string ConfigPath { get; private set; } - - /// - /// The path to the global BepInEx configuration file. - /// - public static string BepInExConfigPath { get; private set; } - - /// - /// The path to temporary cache files. - /// - public static string CachePath { get; private set; } - - /// - /// The path to the patcher plugin folder which resides in the BepInEx folder. - /// - public static string PatcherPluginPath { get; private set; } - - /// - /// The path to the plugin folder which resides in the BepInEx folder. - /// - /// This is ONLY guaranteed to be set correctly when Chainloader has been initialized. - /// - /// - public static string PluginPath { get; private set; } - - /// - /// The name of the currently executing process. - /// - public static string ProcessName { get; private set; } - - /// - /// List of directories from where Mono will search assemblies before assembly resolving is invoked. - /// - public static string[] DllSearchPaths { get; private set; } - - public static void SetExecutablePath(string executablePath, - string bepinRootPath = null, - string managedPath = null, - string[] dllSearchPath = null) - { - ExecutablePath = executablePath; - ProcessName = Path.GetFileNameWithoutExtension(executablePath); - - GameRootPath = PlatformHelper.Is(Platform.MacOS) - ? Utility.ParentDirectory(executablePath, 4) - : Path.GetDirectoryName(executablePath); - - ManagedPath = managedPath ?? Utility.CombinePaths(GameRootPath, $"{ProcessName}_Data", "Managed"); - BepInExRootPath = bepinRootPath ?? Path.Combine(GameRootPath, "BepInEx"); - ConfigPath = Path.Combine(BepInExRootPath, "config"); - BepInExConfigPath = Path.Combine(ConfigPath, "BepInEx.cfg"); - PluginPath = Path.Combine(BepInExRootPath, "plugins"); - PatcherPluginPath = Path.Combine(BepInExRootPath, "patchers"); - BepInExAssemblyDirectory = Path.Combine(BepInExRootPath, "core"); - BepInExAssemblyPath = Path.Combine(BepInExAssemblyDirectory, - $"{Assembly.GetExecutingAssembly().GetName().Name}.dll"); - CachePath = Path.Combine(BepInExRootPath, "cache"); - DllSearchPaths = (dllSearchPath ?? new string[0]).Concat(new[] { ManagedPath }).Distinct().ToArray(); - } - - internal static void SetPluginPath(string pluginPath) => - PluginPath = Utility.CombinePaths(BepInExRootPath, pluginPath); + ManagedPath = managedPath ?? Utility.CombinePaths(GameRootPath, $"{ProcessName}_Data", "Managed"); + BepInExRootPath = bepinRootPath ?? Path.Combine(GameRootPath, "BepInEx"); + ConfigPath = Path.Combine(BepInExRootPath, "config"); + BepInExConfigPath = Path.Combine(ConfigPath, "BepInEx.cfg"); + PluginPath = Path.Combine(BepInExRootPath, "plugins"); + PatcherPluginPath = Path.Combine(BepInExRootPath, "patchers"); + BepInExAssemblyDirectory = Path.Combine(BepInExRootPath, "core"); + BepInExAssemblyPath = Path.Combine(BepInExAssemblyDirectory, + $"{Assembly.GetExecutingAssembly().GetName().Name}.dll"); + CachePath = Path.Combine(BepInExRootPath, "cache"); + DllSearchPaths = (dllSearchPath ?? new string[0]).Concat(new[] { ManagedPath }).Distinct().ToArray(); } + + internal static void SetPluginPath(string pluginPath) => + PluginPath = Utility.CombinePaths(BepInExRootPath, pluginPath); } diff --git a/BepInEx.Core/Utility.cs b/BepInEx.Core/Utility.cs index b497f34..be4eb21 100644 --- a/BepInEx.Core/Utility.cs +++ b/BepInEx.Core/Utility.cs @@ -8,391 +8,390 @@ using System.Security.Cryptography; using System.Text; using Mono.Cecil; -namespace BepInEx +namespace BepInEx; + +/// +/// Generic helper properties and methods. +/// +public static class Utility { + private const string TRUSTED_PLATFORM_ASSEMBLIES = "TRUSTED_PLATFORM_ASSEMBLIES"; + private static bool? sreEnabled; + /// - /// Generic helper properties and methods. + /// Whether current Common Language Runtime supports dynamic method generation using + /// namespace. /// - public static class Utility + public static bool CLRSupportsDynamicAssemblies => CheckSRE(); + + /// + /// An encoding for UTF-8 which does not emit a byte order mark (BOM). + /// + public static Encoding UTF8NoBom { get; } = new UTF8Encoding(false); + + private static bool CheckSRE() { - private const string TRUSTED_PLATFORM_ASSEMBLIES = "TRUSTED_PLATFORM_ASSEMBLIES"; - private static bool? sreEnabled; - - /// - /// Whether current Common Language Runtime supports dynamic method generation using - /// namespace. - /// - public static bool CLRSupportsDynamicAssemblies => CheckSRE(); - - /// - /// An encoding for UTF-8 which does not emit a byte order mark (BOM). - /// - public static Encoding UTF8NoBom { get; } = new UTF8Encoding(false); - - private static bool CheckSRE() + try { - try - { - if (sreEnabled.HasValue) - return sreEnabled.Value; - - // ReSharper disable once AssignNullToNotNullAttribute - _ = new CustomAttributeBuilder(null, new object[0]); - } - catch (PlatformNotSupportedException) - { - sreEnabled = false; + if (sreEnabled.HasValue) return sreEnabled.Value; - } - catch (ArgumentNullException) - { - // Suppress ArgumentNullException - } - sreEnabled = true; + // ReSharper disable once AssignNullToNotNullAttribute + _ = new CustomAttributeBuilder(null, new object[0]); + } + catch (PlatformNotSupportedException) + { + sreEnabled = false; return sreEnabled.Value; } - - /// - /// Try to perform an action. - /// - /// Action to perform. - /// Possible exception that gets returned. - /// True, if action succeeded, false if an exception occured. - public static bool TryDo(Action action, out Exception exception) + catch (ArgumentNullException) { - exception = null; - try - { - action(); - return true; - } - catch (Exception e) - { - exception = e; - return false; - } + // Suppress ArgumentNullException } - /// - /// Combines multiple paths together, as the specific method is not available in .NET 3.5. - /// - /// The multiple paths to combine together. - /// A combined path. - public static string CombinePaths(params string[] parts) => parts.Aggregate(Path.Combine); + sreEnabled = true; + return sreEnabled.Value; + } - /// - /// Returns the parent directory of a path, optionally specifying the amount of levels. - /// - /// The path to get the parent directory of. - /// The amount of levels to traverse. Defaults to 1 - /// The parent directory. - public static string ParentDirectory(string path, int levels = 1) + /// + /// Try to perform an action. + /// + /// Action to perform. + /// Possible exception that gets returned. + /// True, if action succeeded, false if an exception occured. + public static bool TryDo(Action action, out Exception exception) + { + exception = null; + try { - for (var i = 0; i < levels; i++) - path = Path.GetDirectoryName(path); + action(); + return true; + } + catch (Exception e) + { + exception = e; + return false; + } + } - return path; + /// + /// Combines multiple paths together, as the specific method is not available in .NET 3.5. + /// + /// The multiple paths to combine together. + /// A combined path. + public static string CombinePaths(params string[] parts) => parts.Aggregate(Path.Combine); + + /// + /// Returns the parent directory of a path, optionally specifying the amount of levels. + /// + /// The path to get the parent directory of. + /// The amount of levels to traverse. Defaults to 1 + /// The parent directory. + public static string ParentDirectory(string path, int levels = 1) + { + for (var i = 0; i < levels; i++) + path = Path.GetDirectoryName(path); + + return path; + } + + /// + /// Tries to parse a bool, with a default value if unable to parse. + /// + /// The string to parse + /// The value to return if parsing is unsuccessful. + /// Boolean value of input if able to be parsed, otherwise default value. + public static bool SafeParseBool(string input, bool defaultValue = false) => + bool.TryParse(input, out var result) ? result : defaultValue; + + /// + /// Converts a file path into a UnityEngine.WWW format. + /// + /// The file path to convert. + /// A converted file path. + public static string ConvertToWWWFormat(string path) => $"file://{path.Replace('\\', '/')}"; + + /// + /// Indicates whether a specified string is null, empty, or consists only of white-space characters. + /// + /// The string to test. + /// True if the value parameter is null or empty, or if value consists exclusively of white-space characters. + public static bool IsNullOrWhiteSpace(this string self) => self == null || self.All(char.IsWhiteSpace); + + /// + /// Sorts a given dependency graph using a direct toposort, reporting possible cyclic dependencies. + /// + /// Nodes to sort + /// Function that maps a node to a collection of its dependencies. + /// Type of the node in a dependency graph. + /// Collection of nodes sorted in the order of least dependencies to the most. + /// Thrown when a cyclic dependency occurs. + public static IEnumerable TopologicalSort(IEnumerable nodes, + Func> dependencySelector) + { + var sorted_list = new List(); + + var visited = new HashSet(); + var sorted = new HashSet(); + + foreach (var input in nodes) + { + var currentStack = new Stack(); + if (!Visit(input, currentStack)) + throw new Exception("Cyclic Dependency:\r\n" + currentStack.Select(x => $" - {x}") //append dashes + .Aggregate((a, b) => + $"{a}\r\n{b}")); //add new lines inbetween } - /// - /// Tries to parse a bool, with a default value if unable to parse. - /// - /// The string to parse - /// The value to return if parsing is unsuccessful. - /// Boolean value of input if able to be parsed, otherwise default value. - public static bool SafeParseBool(string input, bool defaultValue = false) => - bool.TryParse(input, out var result) ? result : defaultValue; - /// - /// Converts a file path into a UnityEngine.WWW format. - /// - /// The file path to convert. - /// A converted file path. - public static string ConvertToWWWFormat(string path) => $"file://{path.Replace('\\', '/')}"; + return sorted_list; - /// - /// Indicates whether a specified string is null, empty, or consists only of white-space characters. - /// - /// The string to test. - /// True if the value parameter is null or empty, or if value consists exclusively of white-space characters. - public static bool IsNullOrWhiteSpace(this string self) => self == null || self.All(char.IsWhiteSpace); - - /// - /// Sorts a given dependency graph using a direct toposort, reporting possible cyclic dependencies. - /// - /// Nodes to sort - /// Function that maps a node to a collection of its dependencies. - /// Type of the node in a dependency graph. - /// Collection of nodes sorted in the order of least dependencies to the most. - /// Thrown when a cyclic dependency occurs. - public static IEnumerable TopologicalSort(IEnumerable nodes, - Func> dependencySelector) + bool Visit(TNode node, Stack stack) { - var sorted_list = new List(); - - var visited = new HashSet(); - var sorted = new HashSet(); - - foreach (var input in nodes) + if (visited.Contains(node)) { - var currentStack = new Stack(); - if (!Visit(input, currentStack)) - throw new Exception("Cyclic Dependency:\r\n" + currentStack.Select(x => $" - {x}") //append dashes - .Aggregate((a, b) => - $"{a}\r\n{b}")); //add new lines inbetween + if (!sorted.Contains(node)) return false; + } + else + { + visited.Add(node); + stack.Push(node); + if (dependencySelector(node).Any(dep => !Visit(dep, stack))) + return false; + + sorted.Add(node); + sorted_list.Add(node); + + stack.Pop(); } + return true; + } + } - return sorted_list; + /// + /// Try to resolve and load the given assembly DLL. + /// + /// Name of the assembly, of the type . + /// Directory to search the assembly from. + /// The loaded assembly. + /// True, if the assembly was found and loaded. Otherwise, false. + private static bool TryResolveDllAssembly(AssemblyName assemblyName, + string directory, + Func loader, + out T assembly) where T : class + { + assembly = null; - bool Visit(TNode node, Stack stack) + var potentialDirectories = new List { directory }; + + if (!Directory.Exists(directory)) + return false; + + potentialDirectories.AddRange(Directory.GetDirectories(directory, "*", SearchOption.AllDirectories)); + + foreach (var subDirectory in potentialDirectories) + { + var potentialPaths = new[] { - if (visited.Contains(node)) + $"{assemblyName.Name}.dll", + $"{assemblyName.Name}.exe" + }; + + foreach (var potentialPath in potentialPaths) + { + var path = Path.Combine(subDirectory, potentialPath); + + if (!File.Exists(path)) + continue; + + try { - if (!sorted.Contains(node)) return false; + assembly = loader(path); } - else + catch (Exception) { - visited.Add(node); - stack.Push(node); - if (dependencySelector(node).Any(dep => !Visit(dep, stack))) - return false; - - sorted.Add(node); - sorted_list.Add(node); - - stack.Pop(); + continue; } return true; } } - /// - /// Try to resolve and load the given assembly DLL. - /// - /// Name of the assembly, of the type . - /// Directory to search the assembly from. - /// The loaded assembly. - /// True, if the assembly was found and loaded. Otherwise, false. - private static bool TryResolveDllAssembly(AssemblyName assemblyName, - string directory, - Func loader, - out T assembly) where T : class + return false; + } + + /// + /// Checks whether a given cecil type definition is a subtype of a provided type. + /// + /// Cecil type definition + /// Type to check against + /// Whether the given cecil type is a subtype of the type. + public static bool IsSubtypeOf(this TypeDefinition self, Type td) + { + if (self.FullName == td.FullName) + return true; + return self.FullName != "System.Object" && (self.BaseType?.Resolve()?.IsSubtypeOf(td) ?? false); + } + + /// + /// Try to resolve and load the given assembly DLL. + /// + /// Name of the assembly, of the type . + /// Directory to search the assembly from. + /// The loaded assembly. + /// True, if the assembly was found and loaded. Otherwise, false. + public static bool TryResolveDllAssembly(AssemblyName assemblyName, string directory, out Assembly assembly) => + TryResolveDllAssembly(assemblyName, directory, Assembly.LoadFile, out assembly); + + /// + /// Try to resolve and load the given assembly DLL. + /// + /// Name of the assembly, of the type . + /// Directory to search the assembly from. + /// Reader parameters that contain possible custom assembly resolver. + /// The loaded assembly. + /// True, if the assembly was found and loaded. Otherwise, false. + public static bool TryResolveDllAssembly(AssemblyName assemblyName, + string directory, + ReaderParameters readerParameters, + out AssemblyDefinition assembly) => + TryResolveDllAssembly(assemblyName, directory, + s => AssemblyDefinition.ReadAssembly(s, readerParameters), out assembly); + + /// + /// Tries to create a file with the given name + /// + /// Path of the file to create + /// File open mode + /// Resulting filestream + /// File access options + /// File share options + /// + public static bool TryOpenFileStream(string path, + FileMode mode, + out FileStream fileStream, + FileAccess access = FileAccess.ReadWrite, + FileShare share = FileShare.Read) + { + try { - assembly = null; + fileStream = new FileStream(path, mode, access, share); - var potentialDirectories = new List { directory }; - - if (!Directory.Exists(directory)) - return false; - - potentialDirectories.AddRange(Directory.GetDirectories(directory, "*", SearchOption.AllDirectories)); - - foreach (var subDirectory in potentialDirectories) - { - var potentialPaths = new[] - { - $"{assemblyName.Name}.dll", - $"{assemblyName.Name}.exe" - }; - - foreach (var potentialPath in potentialPaths) - { - var path = Path.Combine(subDirectory, potentialPath); - - if (!File.Exists(path)) - continue; - - try - { - assembly = loader(path); - } - catch (Exception) - { - continue; - } - - return true; - } - } + return true; + } + catch (IOException) + { + fileStream = null; return false; } + } - /// - /// Checks whether a given cecil type definition is a subtype of a provided type. - /// - /// Cecil type definition - /// Type to check against - /// Whether the given cecil type is a subtype of the type. - public static bool IsSubtypeOf(this TypeDefinition self, Type td) + public static IEnumerable EnumerateAllMethods(this TypeDefinition type) + { + var currentType = type; + + while (currentType != null) { - if (self.FullName == td.FullName) - return true; - return self.FullName != "System.Object" && (self.BaseType?.Resolve()?.IsSubtypeOf(td) ?? false); - } + foreach (var method in currentType.Methods) + yield return method; - /// - /// Try to resolve and load the given assembly DLL. - /// - /// Name of the assembly, of the type . - /// Directory to search the assembly from. - /// The loaded assembly. - /// True, if the assembly was found and loaded. Otherwise, false. - public static bool TryResolveDllAssembly(AssemblyName assemblyName, string directory, out Assembly assembly) => - TryResolveDllAssembly(assemblyName, directory, Assembly.LoadFile, out assembly); - - /// - /// Try to resolve and load the given assembly DLL. - /// - /// Name of the assembly, of the type . - /// Directory to search the assembly from. - /// Reader parameters that contain possible custom assembly resolver. - /// The loaded assembly. - /// True, if the assembly was found and loaded. Otherwise, false. - public static bool TryResolveDllAssembly(AssemblyName assemblyName, - string directory, - ReaderParameters readerParameters, - out AssemblyDefinition assembly) => - TryResolveDllAssembly(assemblyName, directory, - s => AssemblyDefinition.ReadAssembly(s, readerParameters), out assembly); - - /// - /// Tries to create a file with the given name - /// - /// Path of the file to create - /// File open mode - /// Resulting filestream - /// File access options - /// File share options - /// - public static bool TryOpenFileStream(string path, - FileMode mode, - out FileStream fileStream, - FileAccess access = FileAccess.ReadWrite, - FileShare share = FileShare.Read) - { - try - { - fileStream = new FileStream(path, mode, access, share); - - return true; - } - catch (IOException) - { - fileStream = null; - - return false; - } - } - - public static IEnumerable EnumerateAllMethods(this TypeDefinition type) - { - var currentType = type; - - while (currentType != null) - { - foreach (var method in currentType.Methods) - yield return method; - - currentType = currentType.BaseType?.Resolve(); - } - } - - /// - /// Compute a MD5 hash of the given stream. - /// - /// Stream to hash - /// MD5 hash as a hex string - public static string HashStream(Stream stream) - { - using var md5 = MD5.Create(); - - var buf = new byte[4096]; - int read; - while ((read = stream.Read(buf, 0, buf.Length)) > 0) - md5.TransformBlock(buf, 0, read, buf, 0); - - md5.TransformFinalBlock(new byte[0], 0, 0); - - return ByteArrayToString(md5.Hash); - } - - /// - /// Convert the given array to a hex string. - /// - /// Bytes to convert. - /// Bytes reinterpreted as a hex number. - public static string ByteArrayToString(byte[] data) - { - var builder = new StringBuilder(data.Length * 2); - - foreach (var b in data) - builder.AppendFormat("{0:x2}", b); - - return builder.ToString(); - } - - /// - /// Try to parse given string as an assembly name - /// - /// Fully qualified assembly name - /// Resulting instance - /// true, if parsing was successful, otherwise false - /// - /// On some versions of mono, using fails because it runs on unmanaged side - /// which has problems with encoding. - /// Using solves this by doing parsing on managed side instead. - /// - public static bool TryParseAssemblyName(string fullName, out AssemblyName assemblyName) - { - try - { - assemblyName = new AssemblyName(fullName); - return true; - } - catch (Exception) - { - assemblyName = null; - return false; - } - } - - internal static void AddCecilPlatformAssemblies(this AppDomain appDomain, string assemblyDir) - { - if (!Directory.Exists(assemblyDir)) - return; - // Cecil 0.11 requires one to manually set up list of trusted assemblies for assembly resolving - var curTrusted = appDomain.GetData(TRUSTED_PLATFORM_ASSEMBLIES) as string; - var addTrusted = string.Join(Path.PathSeparator.ToString(), - Directory.GetFiles(assemblyDir, "*.dll", - SearchOption.TopDirectoryOnly)); - var newTrusted = curTrusted == null ? addTrusted : $"{curTrusted}{Path.PathSeparator}{addTrusted}"; - appDomain.SetData(TRUSTED_PLATFORM_ASSEMBLIES, newTrusted); - } - - /// - /// Gets unique files in all given directories. If the file with the same name exists in multiple directories, - /// only the first occurrence is returned. - /// - /// Directories to search from. - /// File pattern to search. - /// Collection of all files in the directories. - public static IEnumerable GetUniqueFilesInDirectories(IEnumerable directories, - string pattern = "*") - { - 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; - } - - return result.Values; + currentType = currentType.BaseType?.Resolve(); } } + + /// + /// Compute a MD5 hash of the given stream. + /// + /// Stream to hash + /// MD5 hash as a hex string + public static string HashStream(Stream stream) + { + using var md5 = MD5.Create(); + + var buf = new byte[4096]; + int read; + while ((read = stream.Read(buf, 0, buf.Length)) > 0) + md5.TransformBlock(buf, 0, read, buf, 0); + + md5.TransformFinalBlock(new byte[0], 0, 0); + + return ByteArrayToString(md5.Hash); + } + + /// + /// Convert the given array to a hex string. + /// + /// Bytes to convert. + /// Bytes reinterpreted as a hex number. + public static string ByteArrayToString(byte[] data) + { + var builder = new StringBuilder(data.Length * 2); + + foreach (var b in data) + builder.AppendFormat("{0:x2}", b); + + return builder.ToString(); + } + + /// + /// Try to parse given string as an assembly name + /// + /// Fully qualified assembly name + /// Resulting instance + /// true, if parsing was successful, otherwise false + /// + /// On some versions of mono, using fails because it runs on unmanaged side + /// which has problems with encoding. + /// Using solves this by doing parsing on managed side instead. + /// + public static bool TryParseAssemblyName(string fullName, out AssemblyName assemblyName) + { + try + { + assemblyName = new AssemblyName(fullName); + return true; + } + catch (Exception) + { + assemblyName = null; + return false; + } + } + + internal static void AddCecilPlatformAssemblies(this AppDomain appDomain, string assemblyDir) + { + if (!Directory.Exists(assemblyDir)) + return; + // Cecil 0.11 requires one to manually set up list of trusted assemblies for assembly resolving + var curTrusted = appDomain.GetData(TRUSTED_PLATFORM_ASSEMBLIES) as string; + var addTrusted = string.Join(Path.PathSeparator.ToString(), + Directory.GetFiles(assemblyDir, "*.dll", + SearchOption.TopDirectoryOnly)); + var newTrusted = curTrusted == null ? addTrusted : $"{curTrusted}{Path.PathSeparator}{addTrusted}"; + appDomain.SetData(TRUSTED_PLATFORM_ASSEMBLIES, newTrusted); + } + + /// + /// Gets unique files in all given directories. If the file with the same name exists in multiple directories, + /// only the first occurrence is returned. + /// + /// Directories to search from. + /// File pattern to search. + /// Collection of all files in the directories. + public static IEnumerable GetUniqueFilesInDirectories(IEnumerable directories, + string pattern = "*") + { + 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; + } + + return result.Values; + } } diff --git a/BepInEx.IL2CPP/BasePlugin.cs b/BepInEx.IL2CPP/BasePlugin.cs index eb083f0..d3cde6a 100644 --- a/BepInEx.IL2CPP/BasePlugin.cs +++ b/BepInEx.IL2CPP/BasePlugin.cs @@ -2,33 +2,32 @@ using BepInEx.Logging; using UnhollowerBaseLib; -namespace BepInEx.IL2CPP +namespace BepInEx.IL2CPP; + +public abstract class BasePlugin { - public abstract class BasePlugin + protected BasePlugin() { - protected BasePlugin() - { - var metadata = MetadataHelper.GetMetadata(this); + var metadata = MetadataHelper.GetMetadata(this); - Log = Logger.CreateLogSource(metadata.Name); + Log = Logger.CreateLogSource(metadata.Name); - Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, metadata.GUID + ".cfg"), false, metadata); - } - - public ManualLogSource Log { get; } - - public ConfigFile Config { get; } - - public abstract void Load(); - - public virtual bool Unload() => false; - - /// - /// Add a Component (e.g. MonoBehaviour) into Unity scene. - /// - /// Automatically registers the type with Il2Cpp Type system if it isn't already. - /// - /// Type of the component to add. - public T AddComponent() where T : Il2CppObjectBase => IL2CPPChainloader.AddUnityComponent(); + Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, metadata.GUID + ".cfg"), false, metadata); } + + public ManualLogSource Log { get; } + + public ConfigFile Config { get; } + + public abstract void Load(); + + public virtual bool Unload() => false; + + /// + /// Add a Component (e.g. MonoBehaviour) into Unity scene. + /// + /// Automatically registers the type with Il2Cpp Type system if it isn't already. + /// + /// Type of the component to add. + public T AddComponent() where T : Il2CppObjectBase => IL2CPPChainloader.AddUnityComponent(); } diff --git a/BepInEx.IL2CPP/BepInEx.IL2CPP.csproj b/BepInEx.IL2CPP/BepInEx.IL2CPP.csproj index d3c997b..21d703a 100644 --- a/BepInEx.IL2CPP/BepInEx.IL2CPP.csproj +++ b/BepInEx.IL2CPP/BepInEx.IL2CPP.csproj @@ -7,7 +7,7 @@ BepInEx.IL2CPP true ..\bin\IL2CPP\ - 9 + 10 Debug;Release AnyCPU false diff --git a/BepInEx.IL2CPP/DoorstopEntrypoint.cs b/BepInEx.IL2CPP/DoorstopEntrypoint.cs index 5ab9fec..7d071b7 100644 --- a/BepInEx.IL2CPP/DoorstopEntrypoint.cs +++ b/BepInEx.IL2CPP/DoorstopEntrypoint.cs @@ -6,110 +6,109 @@ using System.Reflection; using System.Threading; using BepInEx.Preloader.Core; -namespace BepInEx.IL2CPP +namespace BepInEx.IL2CPP; + +internal static class UnityPreloaderRunner { - internal static class UnityPreloaderRunner + public static void PreloaderMain(string[] args) { - public static void PreloaderMain(string[] args) + var bepinPath = + Path.GetDirectoryName(Path.GetDirectoryName(Path.GetFullPath(EnvVars.DOORSTOP_INVOKE_DLL_PATH))); + + PlatformUtils.SetPlatform(); + + Paths.SetExecutablePath(EnvVars.DOORSTOP_PROCESS_PATH, bepinPath, EnvVars.DOORSTOP_MANAGED_FOLDER_DIR, + EnvVars.DOORSTOP_DLL_SEARCH_DIRS); + + // Cecil 0.11 requires one to manually set up list of trusted assemblies for assembly resolving + AppDomain.CurrentDomain.AddCecilPlatformAssemblies(Paths.ManagedPath); + + Preloader.IL2CPPUnhollowedPath = Path.Combine(Paths.BepInExRootPath, "unhollowed"); + + AppDomain.CurrentDomain.AssemblyResolve += LocalResolve; + AppDomain.CurrentDomain.AssemblyResolve -= DoorstopEntrypoint.ResolveCurrentDirectory; + + + Preloader.Run(); + } + + internal static Assembly LocalResolve(object sender, ResolveEventArgs args) + { + var assemblyName = new AssemblyName(args.Name); + + var foundAssembly = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(x => x.GetName().Name == assemblyName.Name); + + if (foundAssembly != null) + return foundAssembly; + + if (Utility.TryResolveDllAssembly(assemblyName, Paths.BepInExAssemblyDirectory, out foundAssembly) + || Utility.TryResolveDllAssembly(assemblyName, Paths.PatcherPluginPath, out foundAssembly) + || Utility.TryResolveDllAssembly(assemblyName, Paths.PluginPath, out foundAssembly) + || Utility.TryResolveDllAssembly(assemblyName, Preloader.IL2CPPUnhollowedPath, out foundAssembly)) + return foundAssembly; + + return null; + } +} + +internal static class DoorstopEntrypoint +{ + private static string preloaderPath; + + /// + /// The main entrypoint of BepInEx, called from Doorstop. + /// + /// + /// The arguments passed in from Doorstop. First argument is the path of the currently executing + /// process. + /// + public static void Main(string[] args) + { + // We set it to the current directory first as a fallback, but try to use the same location as the .exe file. + var silentExceptionLog = $"preloader_{DateTime.Now:yyyyMMdd_HHmmss_fff}.log"; + Mutex mutex = null; + + try { - var bepinPath = - Path.GetDirectoryName(Path.GetDirectoryName(Path.GetFullPath(EnvVars.DOORSTOP_INVOKE_DLL_PATH))); + EnvVars.LoadVars(); - PlatformUtils.SetPlatform(); + silentExceptionLog = + Path.Combine(Path.GetDirectoryName(EnvVars.DOORSTOP_PROCESS_PATH), silentExceptionLog); - Paths.SetExecutablePath(EnvVars.DOORSTOP_PROCESS_PATH, bepinPath, EnvVars.DOORSTOP_MANAGED_FOLDER_DIR, - EnvVars.DOORSTOP_DLL_SEARCH_DIRS); + // Get the path of this DLL via Doorstop env var because Assembly.Location mangles non-ASCII characters on some versions of Mono for unknown reasons + preloaderPath = Path.GetDirectoryName(Path.GetFullPath(EnvVars.DOORSTOP_INVOKE_DLL_PATH)); - // Cecil 0.11 requires one to manually set up list of trusted assemblies for assembly resolving - AppDomain.CurrentDomain.AddCecilPlatformAssemblies(Paths.ManagedPath); + mutex = new Mutex(false, + Process.GetCurrentProcess().ProcessName + EnvVars.DOORSTOP_PROCESS_PATH + + typeof(DoorstopEntrypoint).FullName); + mutex.WaitOne(); - Preloader.IL2CPPUnhollowedPath = Path.Combine(Paths.BepInExRootPath, "unhollowed"); + AppDomain.CurrentDomain.AssemblyResolve += ResolveCurrentDirectory; - AppDomain.CurrentDomain.AssemblyResolve += LocalResolve; - AppDomain.CurrentDomain.AssemblyResolve -= DoorstopEntrypoint.ResolveCurrentDirectory; - - - Preloader.Run(); + UnityPreloaderRunner.PreloaderMain(args); } - - internal static Assembly LocalResolve(object sender, ResolveEventArgs args) + catch (Exception ex) { - var assemblyName = new AssemblyName(args.Name); + File.WriteAllText(silentExceptionLog, ex.ToString()); + } + finally + { + mutex?.ReleaseMutex(); + } + } - var foundAssembly = AppDomain.CurrentDomain.GetAssemblies() - .FirstOrDefault(x => x.GetName().Name == assemblyName.Name); - - if (foundAssembly != null) - return foundAssembly; - - if (Utility.TryResolveDllAssembly(assemblyName, Paths.BepInExAssemblyDirectory, out foundAssembly) - || Utility.TryResolveDllAssembly(assemblyName, Paths.PatcherPluginPath, out foundAssembly) - || Utility.TryResolveDllAssembly(assemblyName, Paths.PluginPath, out foundAssembly) - || Utility.TryResolveDllAssembly(assemblyName, Preloader.IL2CPPUnhollowedPath, out foundAssembly)) - return foundAssembly; + public static Assembly ResolveCurrentDirectory(object sender, ResolveEventArgs args) + { + var name = new AssemblyName(args.Name); + try + { + return Assembly.LoadFile(Path.Combine(preloaderPath, $"{name.Name}.dll")); + } + catch (Exception) + { return null; } } - - internal static class DoorstopEntrypoint - { - private static string preloaderPath; - - /// - /// The main entrypoint of BepInEx, called from Doorstop. - /// - /// - /// The arguments passed in from Doorstop. First argument is the path of the currently executing - /// process. - /// - public static void Main(string[] args) - { - // We set it to the current directory first as a fallback, but try to use the same location as the .exe file. - var silentExceptionLog = $"preloader_{DateTime.Now:yyyyMMdd_HHmmss_fff}.log"; - Mutex mutex = null; - - try - { - EnvVars.LoadVars(); - - silentExceptionLog = - Path.Combine(Path.GetDirectoryName(EnvVars.DOORSTOP_PROCESS_PATH), silentExceptionLog); - - // Get the path of this DLL via Doorstop env var because Assembly.Location mangles non-ASCII characters on some versions of Mono for unknown reasons - preloaderPath = Path.GetDirectoryName(Path.GetFullPath(EnvVars.DOORSTOP_INVOKE_DLL_PATH)); - - mutex = new Mutex(false, - Process.GetCurrentProcess().ProcessName + EnvVars.DOORSTOP_PROCESS_PATH + - typeof(DoorstopEntrypoint).FullName); - mutex.WaitOne(); - - AppDomain.CurrentDomain.AssemblyResolve += ResolveCurrentDirectory; - - UnityPreloaderRunner.PreloaderMain(args); - } - catch (Exception ex) - { - File.WriteAllText(silentExceptionLog, ex.ToString()); - } - finally - { - mutex?.ReleaseMutex(); - } - } - - public static Assembly ResolveCurrentDirectory(object sender, ResolveEventArgs args) - { - var name = new AssemblyName(args.Name); - - try - { - return Assembly.LoadFile(Path.Combine(preloaderPath, $"{name.Name}.dll")); - } - catch (Exception) - { - return null; - } - } - } } diff --git a/BepInEx.IL2CPP/Hook/Allocator/LinuxPageAllocator.cs b/BepInEx.IL2CPP/Hook/Allocator/LinuxPageAllocator.cs index b1881ee..22eaad0 100644 --- a/BepInEx.IL2CPP/Hook/Allocator/LinuxPageAllocator.cs +++ b/BepInEx.IL2CPP/Hook/Allocator/LinuxPageAllocator.cs @@ -2,26 +2,25 @@ using System.Globalization; using System.IO; -namespace BepInEx.IL2CPP.Hook.Allocator -{ - internal class LinuxPageAllocator : UnixPageAllocator - { - protected override IEnumerable<(nint, nint)> MapMemoryAreas() - { - // Each row of /proc/self/maps is as follows: - // - - // More info: https://stackoverflow.com/a/1401595 - using var procMap = new StreamReader(File.OpenRead("/proc/self/maps")); +namespace BepInEx.IL2CPP.Hook.Allocator; - string line; - while ((line = procMap.ReadLine()) != null) - { - var startIndex = line.IndexOf('-'); - var endIndex = line.IndexOf(' '); - var startAddr = long.Parse(line[..startIndex], NumberStyles.HexNumber); - var endAddr = long.Parse(line[(startIndex + 1)..endIndex], NumberStyles.HexNumber); - yield return ((nint) startAddr, (nint) endAddr); - } +internal class LinuxPageAllocator : UnixPageAllocator +{ + protected override IEnumerable<(nint, nint)> MapMemoryAreas() + { + // Each row of /proc/self/maps is as follows: + // - + // More info: https://stackoverflow.com/a/1401595 + using var procMap = new StreamReader(File.OpenRead("/proc/self/maps")); + + string line; + while ((line = procMap.ReadLine()) != null) + { + var startIndex = line.IndexOf('-'); + var endIndex = line.IndexOf(' '); + var startAddr = long.Parse(line[..startIndex], NumberStyles.HexNumber); + var endAddr = long.Parse(line[(startIndex + 1)..endIndex], NumberStyles.HexNumber); + yield return ((nint) startAddr, (nint) endAddr); } } } diff --git a/BepInEx.IL2CPP/Hook/Allocator/MacOsPageAllocator.cs b/BepInEx.IL2CPP/Hook/Allocator/MacOsPageAllocator.cs index fc4aa17..1eb8882 100644 --- a/BepInEx.IL2CPP/Hook/Allocator/MacOsPageAllocator.cs +++ b/BepInEx.IL2CPP/Hook/Allocator/MacOsPageAllocator.cs @@ -2,83 +2,82 @@ using System.Runtime.InteropServices; using MonoMod.Utils; -namespace BepInEx.IL2CPP.Hook.Allocator +namespace BepInEx.IL2CPP.Hook.Allocator; + +internal class MacOsPageAllocator : UnixPageAllocator { - internal class MacOsPageAllocator : UnixPageAllocator + protected override IEnumerable<(nint, nint)> MapMemoryAreas() { - protected override IEnumerable<(nint, nint)> MapMemoryAreas() + var info = new LibSystem.vm_region_basic_info_64(); + var infoCount = (uint) (Marshal.SizeOf() / sizeof(int)); + var objectName = 0u; + nint address = 0; + nint size = 0; + + while (LibSystem.vm_region_64(LibSystem.TaskSelf, ref address, ref size, LibSystem.VM_REGION_BASIC_INFO_64, + ref info, ref infoCount, ref objectName) == LibSystem.KERN_SUCCESS) { - var info = new LibSystem.vm_region_basic_info_64(); - var infoCount = (uint) (Marshal.SizeOf() / sizeof(int)); - var objectName = 0u; - nint address = 0; - nint size = 0; - - while (LibSystem.vm_region_64(LibSystem.TaskSelf, ref address, ref size, LibSystem.VM_REGION_BASIC_INFO_64, - ref info, ref infoCount, ref objectName) == LibSystem.KERN_SUCCESS) - { - var start = address; - var end = address + size; - address = end; - yield return (start, end); - } - } - - private static class LibSystem - { - public const int VM_REGION_BASIC_INFO_64 = 9; - public const int KERN_SUCCESS = 0; - public static readonly nint TaskSelf; - - static LibSystem() - { - typeof(LibSystem).ResolveDynDllImports(new Dictionary> - { - ["libSystem"] = new() - { - "/usr/lib/libSystem.dylib" // OSX POSIX - } - }); - - var libsystem = DynDll.OpenLibrary("/usr/lib/libSystem.dylib"); - TaskSelf = libsystem - .GetFunction("mach_task_self_"); // This isn't a function but rather an exported symbol - } - - // ReSharper disable InconsistentNaming - [StructLayout(LayoutKind.Sequential)] - public readonly struct vm_region_basic_info_64 - { - public readonly int protection; - public readonly int max_protection; - public readonly uint inheritance; - - [MarshalAs(UnmanagedType.I4)] - public readonly bool shared; - - [MarshalAs(UnmanagedType.I4)] - public readonly bool reserved; - - public readonly ulong offset; - public readonly int behavior; - public readonly ushort user_wired_count; - } - // ReSharper restore InconsistentNaming - - // ReSharper disable InconsistentNaming -#pragma warning disable 649 // Set by MonoMod - [DynDllImport("libSystem")] - public static vm_region_64Delegate vm_region_64; -#pragma warning restore 649 - - public delegate int vm_region_64Delegate(nint target_task, - ref nint address, - ref nint size, - int flavor, - ref vm_region_basic_info_64 info, - ref uint infoCnt, - ref uint object_name); - // ReSharper restore InconsistentNaming + var start = address; + var end = address + size; + address = end; + yield return (start, end); } } + + private static class LibSystem + { + public const int VM_REGION_BASIC_INFO_64 = 9; + public const int KERN_SUCCESS = 0; + public static readonly nint TaskSelf; + + static LibSystem() + { + typeof(LibSystem).ResolveDynDllImports(new Dictionary> + { + ["libSystem"] = new() + { + "/usr/lib/libSystem.dylib" // OSX POSIX + } + }); + + var libsystem = DynDll.OpenLibrary("/usr/lib/libSystem.dylib"); + TaskSelf = libsystem + .GetFunction("mach_task_self_"); // This isn't a function but rather an exported symbol + } + + // ReSharper disable InconsistentNaming + [StructLayout(LayoutKind.Sequential)] + public readonly struct vm_region_basic_info_64 + { + public readonly int protection; + public readonly int max_protection; + public readonly uint inheritance; + + [MarshalAs(UnmanagedType.I4)] + public readonly bool shared; + + [MarshalAs(UnmanagedType.I4)] + public readonly bool reserved; + + public readonly ulong offset; + public readonly int behavior; + public readonly ushort user_wired_count; + } + // ReSharper restore InconsistentNaming + + // ReSharper disable InconsistentNaming +#pragma warning disable 649 // Set by MonoMod + [DynDllImport("libSystem")] + public static vm_region_64Delegate vm_region_64; +#pragma warning restore 649 + + public delegate int vm_region_64Delegate(nint target_task, + ref nint address, + ref nint size, + int flavor, + ref vm_region_basic_info_64 info, + ref uint infoCnt, + ref uint object_name); + // ReSharper restore InconsistentNaming + } } diff --git a/BepInEx.IL2CPP/Hook/Allocator/PageAllocator.cs b/BepInEx.IL2CPP/Hook/Allocator/PageAllocator.cs index e30d13a..42bcb95 100644 --- a/BepInEx.IL2CPP/Hook/Allocator/PageAllocator.cs +++ b/BepInEx.IL2CPP/Hook/Allocator/PageAllocator.cs @@ -3,135 +3,134 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using MonoMod.Utils; -namespace BepInEx.IL2CPP.Hook.Allocator +namespace BepInEx.IL2CPP.Hook.Allocator; + +internal class PageAllocatorException : Exception { - internal class PageAllocatorException : Exception + public PageAllocatorException(string message) : base(message) { } +} + +/// +/// A general purpose page allocator for patching purposes. +/// Allows to allocate pages (4k memory chunks) within the 1GB radius of a given address. +/// +/// Based on https://github.com/kubo/funchook +internal abstract class PageAllocator +{ + /// + /// Common page size on Unix and Windows (4k). + /// Call to will allocate a single page of this size. + /// + public const int PAGE_SIZE = 0x1000; + + /// + /// Allocation granularity on Windows (but can be reused in other implementations). + /// + protected const int ALLOCATION_UNIT = 0x100000; + + protected const int PAGES_PER_UNIT = ALLOCATION_UNIT / PAGE_SIZE; + + private static PageAllocator instance; + + private readonly List allocatedChunks = new(); + + /// + /// Platform-specific instance of page allocator. + /// + public static PageAllocator Instance => instance ??= Init(); + + /// + /// Allocates a single 64k chunk of memory near the given address + /// + /// Address near which to attempt allocate the chunk + /// Allocated chunk + /// Allocation failed + protected abstract nint AllocateChunk(nint hint); + + /// + /// Allocates a single page of size near the provided address. + /// Attempts to allocate the page within the +-1GB region of the hinted address. + /// + /// Address near which to attempt to allocate the page. + /// Address to the allocated page. + public virtual nint Allocate(nint hint) { - public PageAllocatorException(string message) : base(message) { } + foreach (var allocatedChunk in allocatedChunks) + { + // Small shortcut to speed up page lookup + if (allocatedChunk.UsedPages == PAGES_PER_UNIT) + continue; + for (var i = 0; i < allocatedChunk.Pages.Length; i++) + { + if (allocatedChunk.Pages[i]) + continue; + var pageAddr = allocatedChunk.GetPage(i); + if (!IsInRelJmpRange(hint, pageAddr)) + continue; + allocatedChunk.Pages[i] = true; + allocatedChunk.UsedPages++; + return pageAddr; + } + } + + var chunk = new PageChunk + { + BaseAddress = AllocateChunk(hint) + }; + allocatedChunks.Add(chunk); + chunk.Pages[0] = true; + chunk.UsedPages++; + return chunk.BaseAddress; } /// - /// A general purpose page allocator for patching purposes. - /// Allows to allocate pages (4k memory chunks) within the 1GB radius of a given address. + /// Frees the page allocated with /// - /// Based on https://github.com/kubo/funchook - internal abstract class PageAllocator + /// + public void Free(nint page) { - /// - /// Common page size on Unix and Windows (4k). - /// Call to will allocate a single page of this size. - /// - public const int PAGE_SIZE = 0x1000; - - /// - /// Allocation granularity on Windows (but can be reused in other implementations). - /// - protected const int ALLOCATION_UNIT = 0x100000; - - protected const int PAGES_PER_UNIT = ALLOCATION_UNIT / PAGE_SIZE; - - private static PageAllocator instance; - - private readonly List allocatedChunks = new(); - - /// - /// Platform-specific instance of page allocator. - /// - public static PageAllocator Instance => instance ??= Init(); - - /// - /// Allocates a single 64k chunk of memory near the given address - /// - /// Address near which to attempt allocate the chunk - /// Allocated chunk - /// Allocation failed - protected abstract nint AllocateChunk(nint hint); - - /// - /// Allocates a single page of size near the provided address. - /// Attempts to allocate the page within the +-1GB region of the hinted address. - /// - /// Address near which to attempt to allocate the page. - /// Address to the allocated page. - public virtual nint Allocate(nint hint) + foreach (var allocatedChunk in allocatedChunks) { - foreach (var allocatedChunk in allocatedChunks) - { - // Small shortcut to speed up page lookup - if (allocatedChunk.UsedPages == PAGES_PER_UNIT) - continue; - for (var i = 0; i < allocatedChunk.Pages.Length; i++) - { - if (allocatedChunk.Pages[i]) - continue; - var pageAddr = allocatedChunk.GetPage(i); - if (!IsInRelJmpRange(hint, pageAddr)) - continue; - allocatedChunk.Pages[i] = true; - allocatedChunk.UsedPages++; - return pageAddr; - } - } - - var chunk = new PageChunk - { - BaseAddress = AllocateChunk(hint) - }; - allocatedChunks.Add(chunk); - chunk.Pages[0] = true; - chunk.UsedPages++; - return chunk.BaseAddress; - } - - /// - /// Frees the page allocated with - /// - /// - public void Free(nint page) - { - foreach (var allocatedChunk in allocatedChunks) - { - long index = (page - allocatedChunk.BaseAddress) / PAGE_SIZE; - if (index < 0 || index >= PAGES_PER_UNIT) - continue; - if (!allocatedChunk.Pages[index]) - return; - allocatedChunk.Pages[index] = false; - allocatedChunk.UsedPages--; + long index = (page - allocatedChunk.BaseAddress) / PAGE_SIZE; + if (index < 0 || index >= PAGES_PER_UNIT) + continue; + if (!allocatedChunk.Pages[index]) return; - } - } - - /// - /// Checks if the given address is within the relative jump range. - /// - /// Source address to jump from. - /// Destination address to jump to. - /// True, if the distance between the addresses is within the relative jump range (usually 1GB), otherwise false. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsInRelJmpRange(nint src, nint dst) => dst - src is <= int.MaxValue and >= int.MinValue; - - private static PageAllocator Init() => - PlatformHelper.Current switch - { - var v when v.Is(Platform.Windows) => new WindowsPageAllocator(), - var v when v.Is(Platform.Linux) => new LinuxPageAllocator(), - var v when v.Is(Platform.MacOS) => new MacOsPageAllocator(), - _ => throw new NotSupportedException() - }; - - private class PageChunk - { - public readonly bool[] Pages = new bool[PAGES_PER_UNIT]; - public nint BaseAddress; - public int UsedPages; - - public nint GetPage(int index) => BaseAddress + index * PAGE_SIZE; + allocatedChunk.Pages[index] = false; + allocatedChunk.UsedPages--; + return; } } - internal static class PlatformExt + /// + /// Checks if the given address is within the relative jump range. + /// + /// Source address to jump from. + /// Destination address to jump to. + /// True, if the distance between the addresses is within the relative jump range (usually 1GB), otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsInRelJmpRange(nint src, nint dst) => dst - src is <= int.MaxValue and >= int.MinValue; + + private static PageAllocator Init() => + PlatformHelper.Current switch + { + var v when v.Is(Platform.Windows) => new WindowsPageAllocator(), + var v when v.Is(Platform.Linux) => new LinuxPageAllocator(), + var v when v.Is(Platform.MacOS) => new MacOsPageAllocator(), + _ => throw new NotSupportedException() + }; + + private class PageChunk { - public static bool Is(this Platform pl, Platform val) => (pl & val) == val; + public readonly bool[] Pages = new bool[PAGES_PER_UNIT]; + public nint BaseAddress; + public int UsedPages; + + public nint GetPage(int index) => BaseAddress + index * PAGE_SIZE; } } + +internal static class PlatformExt +{ + public static bool Is(this Platform pl, Platform val) => (pl & val) == val; +} diff --git a/BepInEx.IL2CPP/Hook/Allocator/UnixPageAllocator.cs b/BepInEx.IL2CPP/Hook/Allocator/UnixPageAllocator.cs index faacaa0..fcf0930 100644 --- a/BepInEx.IL2CPP/Hook/Allocator/UnixPageAllocator.cs +++ b/BepInEx.IL2CPP/Hook/Allocator/UnixPageAllocator.cs @@ -5,138 +5,137 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using MonoMod.Utils; -namespace BepInEx.IL2CPP.Hook.Allocator +namespace BepInEx.IL2CPP.Hook.Allocator; + +/// +/// Based on https://github.com/kubo/funchook +/// +internal abstract class UnixPageAllocator : PageAllocator { - /// - /// Based on https://github.com/kubo/funchook - /// - internal abstract class UnixPageAllocator : PageAllocator + protected abstract IEnumerable<(nint, nint)> MapMemoryAreas(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool CheckFreeRegionBefore(nint start, nint hint, ref (nint Start, nint End) region) { - protected abstract IEnumerable<(nint, nint)> MapMemoryAreas(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool CheckFreeRegionBefore(nint start, nint hint, ref (nint Start, nint End) region) + if (start < hint) { - if (start < hint) - { - var addr = start - PAGE_SIZE; - if (hint - addr < int.MaxValue) - region.Start = addr; - } - - return false; + var addr = start - PAGE_SIZE; + if (hint - addr < int.MaxValue) + region.Start = addr; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool CheckFreeRegionAfter(nint end, nint hint, ref (nint Start, nint End) region) - { - if (hint < end) - { - if (end - hint < int.MaxValue) - region.End = end; - return true; - } + return false; + } - return false; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool CheckFreeRegionAfter(nint end, nint hint, ref (nint Start, nint End) region) + { + if (hint < end) + { + if (end - hint < int.MaxValue) + region.End = end; + return true; } - private (nint, nint) GetFreeArea(nint hint) + return false; + } + + private (nint, nint) GetFreeArea(nint hint) + { + (nint, nint) result = (0, 0); + nint prevEnd = 0; + + foreach (var (start, end) in MapMemoryAreas()) { - (nint, nint) result = (0, 0); - nint prevEnd = 0; - - foreach (var (start, end) in MapMemoryAreas()) - { - if (prevEnd + PAGE_SIZE <= start) - if (CheckFreeRegionBefore(start, hint, ref result) || - CheckFreeRegionAfter(prevEnd, hint, ref result)) - return result; - prevEnd = end; - } - - if (CheckFreeRegionAfter(prevEnd, hint, ref result)) - return result; - throw new PageAllocatorException($"Could not find free region near {(long) hint:X8}"); + if (prevEnd + PAGE_SIZE <= start) + if (CheckFreeRegionBefore(start, hint, ref result) || + CheckFreeRegionAfter(prevEnd, hint, ref result)) + return result; + prevEnd = end; } - protected override nint AllocateChunk(nint hint) - { - /* From https://github.com/kubo/funchook/blob/master/src/funchook_unix.c#L251-L254: - * Loop three times just to avoid rare cases such as - * unused memory region is used between 'get_free_address()' - * and 'mmap()'. - */ - const int retryCount = 3; + if (CheckFreeRegionAfter(prevEnd, hint, ref result)) + return result; + throw new PageAllocatorException($"Could not find free region near {(long) hint:X8}"); + } - for (var attempt = 0; attempt < retryCount; attempt++) + protected override nint AllocateChunk(nint hint) + { + /* From https://github.com/kubo/funchook/blob/master/src/funchook_unix.c#L251-L254: + * Loop three times just to avoid rare cases such as + * unused memory region is used between 'get_free_address()' + * and 'mmap()'. + */ + const int retryCount = 3; + + for (var attempt = 0; attempt < retryCount; attempt++) + { + var (start, end) = GetFreeArea(hint); + // Try to allocate to end (after original method) first, then try before + var addrs = new[] { end, start }; + foreach (var addr in addrs) { - var (start, end) = GetFreeArea(hint); - // Try to allocate to end (after original method) first, then try before - var addrs = new[] { end, start }; - foreach (var addr in addrs) + if (addr == 0) + continue; + var result = Unix.mmap(addr, PAGE_SIZE, Unix.Protection.PROT_READ | Unix.Protection.PROT_WRITE, + Unix.MapFlags.MAP_PRIVATE | Unix.MapFlags.MAP_ANONYMOUS, -1, 0); + if (result == addr) + return result; + if (result == Unix.MAP_FAILED) + throw new Win32Exception(Marshal.GetLastWin32Error()); // Yes, this should work on unix too + Unix.munmap(result, PAGE_SIZE); + } + } + + throw new PageAllocatorException("Failed to allocate memory in unused regions"); + } + + private static class Unix + { + public delegate nint mmapDelegate(nint addr, + nuint length, + Protection prot, + MapFlags flags, + int fd, + int offset); + + public delegate int munmapDelegate(nint addr, nuint length); + + [Flags] + public enum MapFlags + { + MAP_PRIVATE = 0x02, + MAP_ANONYMOUS = 0x20 + } + + [Flags] + public enum Protection + { + PROT_READ = 0x1, + PROT_WRITE = 0x2 + } + + public static readonly nint MAP_FAILED = -1; + + static Unix() + { + typeof(Unix).ResolveDynDllImports(new Dictionary> + { + ["libc"] = new() { - if (addr == 0) - continue; - var result = Unix.mmap(addr, PAGE_SIZE, Unix.Protection.PROT_READ | Unix.Protection.PROT_WRITE, - Unix.MapFlags.MAP_PRIVATE | Unix.MapFlags.MAP_ANONYMOUS, -1, 0); - if (result == addr) - return result; - if (result == Unix.MAP_FAILED) - throw new Win32Exception(Marshal.GetLastWin32Error()); // Yes, this should work on unix too - Unix.munmap(result, PAGE_SIZE); + "libc.so.6", // Ubuntu glibc + "libc", // Linux glibc, + "/usr/lib/libSystem.dylib" // OSX POSIX } - } - - throw new PageAllocatorException("Failed to allocate memory in unused regions"); + }); } - private static class Unix - { - public delegate nint mmapDelegate(nint addr, - nuint length, - Protection prot, - MapFlags flags, - int fd, - int offset); - - public delegate int munmapDelegate(nint addr, nuint length); - - [Flags] - public enum MapFlags - { - MAP_PRIVATE = 0x02, - MAP_ANONYMOUS = 0x20 - } - - [Flags] - public enum Protection - { - PROT_READ = 0x1, - PROT_WRITE = 0x2 - } - - public static readonly nint MAP_FAILED = -1; - - static Unix() - { - typeof(Unix).ResolveDynDllImports(new Dictionary> - { - ["libc"] = new() - { - "libc.so.6", // Ubuntu glibc - "libc", // Linux glibc, - "/usr/lib/libSystem.dylib" // OSX POSIX - } - }); - } - #pragma warning disable 649 // Set by MonoMod - [DynDllImport("mmap")] - public static mmapDelegate mmap; + [DynDllImport("mmap")] + public static mmapDelegate mmap; - [DynDllImport("munmap")] - public static munmapDelegate munmap; + [DynDllImport("munmap")] + public static munmapDelegate munmap; #pragma warning restore 649 - } } } diff --git a/BepInEx.IL2CPP/Hook/Allocator/WindowsPageAllocator.cs b/BepInEx.IL2CPP/Hook/Allocator/WindowsPageAllocator.cs index 48f0618..2c03c45 100644 --- a/BepInEx.IL2CPP/Hook/Allocator/WindowsPageAllocator.cs +++ b/BepInEx.IL2CPP/Hook/Allocator/WindowsPageAllocator.cs @@ -3,134 +3,133 @@ using System.ComponentModel; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -namespace BepInEx.IL2CPP.Hook.Allocator +namespace BepInEx.IL2CPP.Hook.Allocator; + +/// +/// Based on https://github.com/kubo/funchook +/// +internal class WindowsPageAllocator : PageAllocator { - /// - /// Based on https://github.com/kubo/funchook - /// - internal class WindowsPageAllocator : PageAllocator + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static nint RoundUp(nint num, nint unit) => (num + unit - 1) & ~ (unit - 1); + + private static Win32Exception Win32Error(string message = null, int code = -1) { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static nint RoundUp(nint num, nint unit) => (num + unit - 1) & ~ (unit - 1); + var ex = code >= 0 ? new Win32Exception(code) : new Win32Exception(); // Generate error message + return new Win32Exception(ex.NativeErrorCode, + !string.IsNullOrWhiteSpace(message) ? $"{message}: {ex.Message}" : ex.Message); + } - private static Win32Exception Win32Error(string message = null, int code = -1) + protected override nint AllocateChunk(nint hint) + { + while (true) { - var ex = code >= 0 ? new Win32Exception(code) : new Win32Exception(); // Generate error message - return new Win32Exception(ex.NativeErrorCode, - !string.IsNullOrWhiteSpace(message) ? $"{message}: {ex.Message}" : ex.Message); - } + var mbi = new WinApi.MEMORY_BASIC_INFORMATION(); + if (WinApi.VirtualQuery(hint, ref mbi, Marshal.SizeOf()) == 0) + throw Win32Error($"Failed to query memory information of 0x{(long) hint:X9}"); - protected override nint AllocateChunk(nint hint) - { - while (true) + if (mbi.State == WinApi.PageState.MEM_FREE) { - var mbi = new WinApi.MEMORY_BASIC_INFORMATION(); - if (WinApi.VirtualQuery(hint, ref mbi, Marshal.SizeOf()) == 0) - throw Win32Error($"Failed to query memory information of 0x{(long) hint:X9}"); - - if (mbi.State == WinApi.PageState.MEM_FREE) + var nextAddress = RoundUp(mbi.BaseAddress, ALLOCATION_UNIT); + var d = nextAddress - mbi.BaseAddress; + if (d >= 0 && mbi.RegionSize - d >= ALLOCATION_UNIT) { - var nextAddress = RoundUp(mbi.BaseAddress, ALLOCATION_UNIT); - var d = nextAddress - mbi.BaseAddress; - if (d >= 0 && mbi.RegionSize - d >= ALLOCATION_UNIT) - { - hint = nextAddress; - break; - } + hint = nextAddress; + break; } - - hint = mbi.BaseAddress + mbi.RegionSize; } - var chunk = WinApi.VirtualAlloc(hint, ALLOCATION_UNIT, WinApi.AllocationType.MEM_RESERVE, - WinApi.ProtectConstant.PAGE_NOACCESS); - if (chunk == 0) - throw Win32Error($"Failed to reserve address: 0x{(long) hint:X8}"); - var addr = WinApi.VirtualAlloc(chunk, PAGE_SIZE, WinApi.AllocationType.MEM_COMMIT, - WinApi.ProtectConstant.PAGE_READWRITE); - if (addr == 0) - { - var error = Marshal.GetLastWin32Error(); - WinApi.VirtualFree(chunk, 0, WinApi.FreeType.MEM_RELEASE); - throw - Win32Error($"Failed to commit memory 0x{(long) addr:X8} for read-write access for 0x{(long) chunk:X8}", - error); - } - - return chunk; + hint = mbi.BaseAddress + mbi.RegionSize; } - public override nint Allocate(nint hint) + var chunk = WinApi.VirtualAlloc(hint, ALLOCATION_UNIT, WinApi.AllocationType.MEM_RESERVE, + WinApi.ProtectConstant.PAGE_NOACCESS); + if (chunk == 0) + throw Win32Error($"Failed to reserve address: 0x{(long) hint:X8}"); + var addr = WinApi.VirtualAlloc(chunk, PAGE_SIZE, WinApi.AllocationType.MEM_COMMIT, + WinApi.ProtectConstant.PAGE_READWRITE); + if (addr == 0) { - var pageAddress = base.Allocate(hint); - if (WinApi.VirtualAlloc(pageAddress, PAGE_SIZE, WinApi.AllocationType.MEM_COMMIT, - WinApi.ProtectConstant.PAGE_READWRITE) == 0) - throw new Win32Exception(Marshal.GetLastWin32Error()); - return pageAddress; + var error = Marshal.GetLastWin32Error(); + WinApi.VirtualFree(chunk, 0, WinApi.FreeType.MEM_RELEASE); + throw + Win32Error($"Failed to commit memory 0x{(long) addr:X8} for read-write access for 0x{(long) chunk:X8}", + error); } - private static class WinApi + return chunk; + } + + public override nint Allocate(nint hint) + { + var pageAddress = base.Allocate(hint); + if (WinApi.VirtualAlloc(pageAddress, PAGE_SIZE, WinApi.AllocationType.MEM_COMMIT, + WinApi.ProtectConstant.PAGE_READWRITE) == 0) + throw new Win32Exception(Marshal.GetLastWin32Error()); + return pageAddress; + } + + private static class WinApi + { + [Flags] + public enum AllocationType : uint { - [Flags] - public enum AllocationType : uint - { - // ReSharper disable InconsistentNaming - MEM_COMMIT = 0x00001000, + // ReSharper disable InconsistentNaming + MEM_COMMIT = 0x00001000, - MEM_RESERVE = 0x00002000 - // ReSharper restore InconsistentNaming - } + MEM_RESERVE = 0x00002000 + // ReSharper restore InconsistentNaming + } - [Flags] - public enum FreeType : uint - { - // ReSharper disable InconsistentNaming - MEM_RELEASE = 0x00008000 - // ReSharper restore InconsistentNaming - } + [Flags] + public enum FreeType : uint + { + // ReSharper disable InconsistentNaming + MEM_RELEASE = 0x00008000 + // ReSharper restore InconsistentNaming + } - public enum PageState : uint - { - // ReSharper disable InconsistentNaming - MEM_FREE = 0x10000 - // ReSharper restore InconsistentNaming - } + public enum PageState : uint + { + // ReSharper disable InconsistentNaming + MEM_FREE = 0x10000 + // ReSharper restore InconsistentNaming + } - [Flags] - public enum ProtectConstant : uint - { - // ReSharper disable InconsistentNaming - PAGE_NOACCESS = 0x01, + [Flags] + public enum ProtectConstant : uint + { + // ReSharper disable InconsistentNaming + PAGE_NOACCESS = 0x01, - PAGE_READWRITE = 0x04 - // ReSharper restore InconsistentNaming - } + PAGE_READWRITE = 0x04 + // ReSharper restore InconsistentNaming + } - [DllImport("kernel32", SetLastError = true)] - public static extern int VirtualQuery(nint lpAddress, ref MEMORY_BASIC_INFORMATION lpBuffer, int dwLength); + [DllImport("kernel32", SetLastError = true)] + public static extern int VirtualQuery(nint lpAddress, ref MEMORY_BASIC_INFORMATION lpBuffer, int dwLength); - [DllImport("kernel32", SetLastError = true)] - public static extern nint VirtualAlloc(nint lpAddress, - nuint dwSize, - AllocationType flAllocationType, - ProtectConstant flProtect); + [DllImport("kernel32", SetLastError = true)] + public static extern nint VirtualAlloc(nint lpAddress, + nuint dwSize, + AllocationType flAllocationType, + ProtectConstant flProtect); - [DllImport("kernel32", SetLastError = true)] - public static extern bool VirtualFree(nint lpAddress, nuint dwSize, FreeType dwFreeType); + [DllImport("kernel32", SetLastError = true)] + public static extern bool VirtualFree(nint lpAddress, nuint dwSize, FreeType dwFreeType); - [StructLayout(LayoutKind.Sequential)] - // ReSharper disable once InconsistentNaming - public readonly struct MEMORY_BASIC_INFORMATION - { - public readonly nint BaseAddress; - public readonly nint AllocationBase; - public readonly uint AllocationProtect; - public readonly nint RegionSize; - public readonly PageState State; - public readonly uint Protect; - public readonly uint Type; - } + [StructLayout(LayoutKind.Sequential)] + // ReSharper disable once InconsistentNaming + public readonly struct MEMORY_BASIC_INFORMATION + { + public readonly nint BaseAddress; + public readonly nint AllocationBase; + public readonly uint AllocationProtect; + public readonly nint RegionSize; + public readonly PageState State; + public readonly uint Protect; + public readonly uint Type; } } } diff --git a/BepInEx.IL2CPP/Hook/DetourGenerator.cs b/BepInEx.IL2CPP/Hook/DetourGenerator.cs index c52d1fb..22e5928 100644 --- a/BepInEx.IL2CPP/Hook/DetourGenerator.cs +++ b/BepInEx.IL2CPP/Hook/DetourGenerator.cs @@ -6,243 +6,242 @@ using BepInEx.Logging; using Iced.Intel; using MonoMod.RuntimeDetour; -namespace BepInEx.IL2CPP +namespace BepInEx.IL2CPP; + +public static class DetourGenerator { - public static class DetourGenerator + private static ManualLogSource logger = Logger.CreateLogSource("DetourGen"); + + public static void Disassemble(ManualLogSource logSource, IntPtr memoryPtr, int size) { - private static ManualLogSource logger = Logger.CreateLogSource("DetourGen"); + var data = new byte[size]; + Marshal.Copy(memoryPtr, data, 0, size); - public static void Disassemble(ManualLogSource logSource, IntPtr memoryPtr, int size) + var formatter = new NasmFormatter(); + var output = new StringOutput(); + var codeReader = new ByteArrayCodeReader(data); + var decoder = Decoder.Create(64, codeReader); + decoder.IP = (ulong) memoryPtr.ToInt64(); + while (codeReader.CanReadByte) { - var data = new byte[size]; - Marshal.Copy(memoryPtr, data, 0, size); + decoder.Decode(out var instr); + formatter.Format(instr, output); + logSource.LogDebug($"{instr.IP:X16} {output.ToStringAndReset()}"); - var formatter = new NasmFormatter(); - var output = new StringOutput(); - var codeReader = new ByteArrayCodeReader(data); - var decoder = Decoder.Create(64, codeReader); - decoder.IP = (ulong) memoryPtr.ToInt64(); - while (codeReader.CanReadByte) + if (instr.Code == Code.Jmp_rm64 && instr.Immediate32 == 0 + ) // && instr.IsIPRelativeMemoryOperand && instr.IPRelativeMemoryAddress = 6 { - decoder.Decode(out var instr); - formatter.Format(instr, output); - logSource.LogDebug($"{instr.IP:X16} {output.ToStringAndReset()}"); + var address = new byte[8]; - if (instr.Code == Code.Jmp_rm64 && instr.Immediate32 == 0 - ) // && instr.IsIPRelativeMemoryOperand && instr.IPRelativeMemoryAddress = 6 - { - var address = new byte[8]; + for (var i = 0; i < 8; i++) + address[i] = (byte) codeReader.ReadByte(); - for (var i = 0; i < 8; i++) - address[i] = (byte) codeReader.ReadByte(); - - logSource.LogDebug($"{instr.IP + (ulong) instr.Length:X16} db 0x{BitConverter.ToUInt64(address, 0):X16}"); - decoder.IP += 8; - } + logSource.LogDebug($"{instr.IP + (ulong) instr.Length:X16} db 0x{BitConverter.ToUInt64(address, 0):X16}"); + decoder.IP += 8; } } - - public static int GetDetourLength(Architecture arch) => arch == Architecture.X64 ? 14 : 5; - - /// - /// Writes a detour on to redirect to . - /// - /// The pointer to the function to apply the detour to. - /// The pointer to the function to redirect to. - /// The architecture of the current platform. - /// - /// The minimum amount of length that the detour should consume. If the generated redirect is - /// smaller than this, the remaining space is padded with NOPs. - /// - public static void ApplyDetour(IntPtr functionPtr, - IntPtr detourPtr, - Architecture architecture, - int minimumLength = 0) + } + + public static int GetDetourLength(Architecture arch) => arch == Architecture.X64 ? 14 : 5; + + /// + /// Writes a detour on to redirect to . + /// + /// The pointer to the function to apply the detour to. + /// The pointer to the function to redirect to. + /// The architecture of the current platform. + /// + /// The minimum amount of length that the detour should consume. If the generated redirect is + /// smaller than this, the remaining space is padded with NOPs. + /// + public static void ApplyDetour(IntPtr functionPtr, + IntPtr detourPtr, + Architecture architecture, + int minimumLength = 0) + { + var jmp = GenerateAbsoluteJump(detourPtr, functionPtr, architecture); + + Marshal.Copy(jmp, 0, functionPtr, jmp.Length); + + // Fill remaining space with NOP instructions + for (var i = jmp.Length; i < minimumLength; i++) + Marshal.WriteByte(functionPtr + i, 0x90); + } + + public static IntPtr CreateTrampolineFromFunction(IntPtr originalFuncPointer, + out int trampolineLength, + out int jmpLength) + { + var instructionBuffer = new byte[32]; + Marshal.Copy(originalFuncPointer, instructionBuffer, 0, 32); + + var trampolinePtr = PageAllocator.Instance.Allocate(originalFuncPointer); + + DetourHelper.Native.MakeWritable(trampolinePtr, PageAllocator.PAGE_SIZE); + + var arch = IntPtr.Size == 8 ? Architecture.X64 : Architecture.X86; + + var minimumTrampolineLength = GetDetourLength(arch); + + CreateTrampolineFromFunction(instructionBuffer, originalFuncPointer, trampolinePtr, minimumTrampolineLength, + arch, out trampolineLength, out jmpLength); + + DetourHelper.Native.MakeExecutable(originalFuncPointer, 32); + DetourHelper.Native.MakeExecutable(trampolinePtr, PageAllocator.PAGE_SIZE); + + return trampolinePtr; + } + + /// + /// Reads assembly from (at least bytes), and writes + /// it to plus a jmp to continue execution. + /// + /// The buffer to copy assembly from. + /// The pointer to the function to copy assembly from. + /// The pointer to write the trampoline assembly to. + /// The architecture of the current platform. + /// Copies at least this many bytes of assembly from . + /// Returns the total length of the trampoline, in bytes. + /// Returns the length of the jmp at the end of the trampoline, in bytes. + public static void CreateTrampolineFromFunction(byte[] instructionBuffer, + IntPtr functionPtr, + IntPtr trampolinePtr, + int minimumTrampolineLength, + Architecture arch, + out int trampolineLength, + out int jmpLength) + { + // Decode original function up until we go past the needed bytes to write the jump to patchedFunctionPtr + + var codeReader = new ByteArrayCodeReader(instructionBuffer); + var decoder = Decoder.Create(arch == Architecture.X64 ? 64 : 32, codeReader); + decoder.IP = (ulong) functionPtr.ToInt64(); + + uint totalBytes = 0; + var origInstructions = new InstructionList(); + var readOverflowArea = false; + while (codeReader.CanReadByte) { - var jmp = GenerateAbsoluteJump(detourPtr, functionPtr, architecture); + decoder.Decode(out var instr); - Marshal.Copy(jmp, 0, functionPtr, jmp.Length); + origInstructions.Add(instr); - // Fill remaining space with NOP instructions - for (var i = jmp.Length; i < minimumLength; i++) - Marshal.WriteByte(functionPtr + i, 0x90); - } + totalBytes += (uint) instr.Length; + if (instr.Code == Code.INVALID) + throw new Exception("Found garbage"); + if (totalBytes >= minimumTrampolineLength) + break; - public static IntPtr CreateTrampolineFromFunction(IntPtr originalFuncPointer, - out int trampolineLength, - out int jmpLength) - { - var instructionBuffer = new byte[32]; - Marshal.Copy(originalFuncPointer, instructionBuffer, 0, 32); - - var trampolinePtr = PageAllocator.Instance.Allocate(originalFuncPointer); - - DetourHelper.Native.MakeWritable(trampolinePtr, PageAllocator.PAGE_SIZE); - - var arch = IntPtr.Size == 8 ? Architecture.X64 : Architecture.X86; - - var minimumTrampolineLength = GetDetourLength(arch); - - CreateTrampolineFromFunction(instructionBuffer, originalFuncPointer, trampolinePtr, minimumTrampolineLength, - arch, out trampolineLength, out jmpLength); + if (readOverflowArea) + { + if (instr.Mnemonic != Mnemonic.Int3 && instr.Mnemonic != Mnemonic.Nop) + throw new Exception("Function is too short to hook"); + continue; + } - DetourHelper.Native.MakeExecutable(originalFuncPointer, 32); - DetourHelper.Native.MakeExecutable(trampolinePtr, PageAllocator.PAGE_SIZE); - - return trampolinePtr; - } - - /// - /// Reads assembly from (at least bytes), and writes - /// it to plus a jmp to continue execution. - /// - /// The buffer to copy assembly from. - /// The pointer to the function to copy assembly from. - /// The pointer to write the trampoline assembly to. - /// The architecture of the current platform. - /// Copies at least this many bytes of assembly from . - /// Returns the total length of the trampoline, in bytes. - /// Returns the length of the jmp at the end of the trampoline, in bytes. - public static void CreateTrampolineFromFunction(byte[] instructionBuffer, - IntPtr functionPtr, - IntPtr trampolinePtr, - int minimumTrampolineLength, - Architecture arch, - out int trampolineLength, - out int jmpLength) - { - // Decode original function up until we go past the needed bytes to write the jump to patchedFunctionPtr - - var codeReader = new ByteArrayCodeReader(instructionBuffer); - var decoder = Decoder.Create(arch == Architecture.X64 ? 64 : 32, codeReader); - decoder.IP = (ulong) functionPtr.ToInt64(); - - uint totalBytes = 0; - var origInstructions = new InstructionList(); - var readOverflowArea = false; - while (codeReader.CanReadByte) + switch (instr.FlowControl) { - decoder.Decode(out var instr); + case FlowControl.Next: + break; - origInstructions.Add(instr); + // Interrupts are usually used for aligning + case FlowControl.Interrupt: // eg. int n + break; - totalBytes += (uint) instr.Length; - if (instr.Code == Code.INVALID) - throw new Exception("Found garbage"); - if (totalBytes >= minimumTrampolineLength) + // Handled by BlockEncoder in most cases + case FlowControl.UnconditionalBranch: + case FlowControl.IndirectBranch: // eg. jmp reg/mem + case FlowControl.ConditionalBranch: // eg. je, jno, etc break; + case FlowControl.Return: // eg. ret + // There is a possibility of an overflow area, attempt to allocate trampoline onto it + readOverflowArea = true; + break; + case FlowControl.Call: // eg. call method + case FlowControl.IndirectCall: // eg. call reg/mem + case FlowControl.XbeginXabortXend: + case FlowControl.Exception: // eg. ud0 + default: + throw new Exception("Not supported yet - " + instr.FlowControl); + } + } - if (readOverflowArea) - { - if (instr.Mnemonic != Mnemonic.Int3 && instr.Mnemonic != Mnemonic.Nop) - throw new Exception("Function is too short to hook"); - continue; - } - - switch (instr.FlowControl) - { - case FlowControl.Next: - break; - - // Interrupts are usually used for aligning - case FlowControl.Interrupt: // eg. int n - break; - - // Handled by BlockEncoder in most cases - case FlowControl.UnconditionalBranch: - case FlowControl.IndirectBranch: // eg. jmp reg/mem - case FlowControl.ConditionalBranch: // eg. je, jno, etc - break; - case FlowControl.Return: // eg. ret - // There is a possibility of an overflow area, attempt to allocate trampoline onto it - readOverflowArea = true; - break; - case FlowControl.Call: // eg. call method - case FlowControl.IndirectCall: // eg. call reg/mem - case FlowControl.XbeginXabortXend: - case FlowControl.Exception: // eg. ud0 - default: - throw new Exception("Not supported yet - " + instr.FlowControl); - } - } + if (totalBytes < minimumTrampolineLength) + throw new Exception("Not enough bytes!"); - if (totalBytes < minimumTrampolineLength) - throw new Exception("Not enough bytes!"); + if (origInstructions.Count == 0) + throw new Exception("Not enough instructions!"); - if (origInstructions.Count == 0) - throw new Exception("Not enough instructions!"); + ref readonly var lastInstr = ref origInstructions[origInstructions.Count - 1]; - ref readonly var lastInstr = ref origInstructions[origInstructions.Count - 1]; + if (lastInstr.FlowControl != FlowControl.Return) + { + Instruction detourInstruction; - if (lastInstr.FlowControl != FlowControl.Return) - { - Instruction detourInstruction; + if (arch == Architecture.X64) + detourInstruction = Instruction.CreateBranch(Code.Jmp_rel32_64, lastInstr.NextIP); + else + detourInstruction = Instruction.CreateBranch(Code.Jmp_rel32_32, lastInstr.NextIP); - if (arch == Architecture.X64) - detourInstruction = Instruction.CreateBranch(Code.Jmp_rel32_64, lastInstr.NextIP); - else - detourInstruction = Instruction.CreateBranch(Code.Jmp_rel32_32, lastInstr.NextIP); + origInstructions.Add(detourInstruction); + } - origInstructions.Add(detourInstruction); - } + // Generate trampoline from instruction list - // Generate trampoline from instruction list + var codeWriter = new CodeWriterImpl(); + var relocatedBaseAddress = (ulong) trampolinePtr; + var block = new InstructionBlock(codeWriter, origInstructions, relocatedBaseAddress); - var codeWriter = new CodeWriterImpl(); - var relocatedBaseAddress = (ulong) trampolinePtr; - var block = new InstructionBlock(codeWriter, origInstructions, relocatedBaseAddress); + var success = BlockEncoder.TryEncode(decoder.Bitness, block, out var errorMessage, out var result); - var success = BlockEncoder.TryEncode(decoder.Bitness, block, out var errorMessage, out var result); + if (!success) throw new Exception(errorMessage); - if (!success) throw new Exception(errorMessage); + // Write generated trampoline - // Write generated trampoline + var newCode = codeWriter.ToArray(); + Marshal.Copy(newCode, 0, trampolinePtr, newCode.Length); - var newCode = codeWriter.ToArray(); - Marshal.Copy(newCode, 0, trampolinePtr, newCode.Length); + jmpLength = newCode.Length - (int) totalBytes; + trampolineLength = newCode.Length; + } - jmpLength = newCode.Length - (int) totalBytes; - trampolineLength = newCode.Length; - } + public static byte[] GenerateAbsoluteJump(IntPtr targetAddress, IntPtr currentAddress, Architecture arch) + { + byte[] jmpBytes; - public static byte[] GenerateAbsoluteJump(IntPtr targetAddress, IntPtr currentAddress, Architecture arch) + if (arch == Architecture.X64) { - byte[] jmpBytes; - - if (arch == Architecture.X64) + jmpBytes = new byte[] { - jmpBytes = new byte[] - { - 0xFF, 0x25, 0x00, 0x00, 0x00, 0x00, // FF25 00000000: JMP [RIP+6] - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // Absolute destination address - }; + 0xFF, 0x25, 0x00, 0x00, 0x00, 0x00, // FF25 00000000: JMP [RIP+6] + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // Absolute destination address + }; - Array.Copy(BitConverter.GetBytes(targetAddress.ToInt64()), 0, jmpBytes, 6, 8); - } - else + Array.Copy(BitConverter.GetBytes(targetAddress.ToInt64()), 0, jmpBytes, 6, 8); + } + else + { + jmpBytes = new byte[] { - jmpBytes = new byte[] - { - 0xE9, // E9: JMP rel destination - 0x00, 0x00, 0x00, 0x00 // Relative destination address - }; - - Array.Copy(BitConverter.GetBytes(targetAddress.ToInt32() - (currentAddress.ToInt32() + 5)), 0, jmpBytes, - 1, 4); - } + 0xE9, // E9: JMP rel destination + 0x00, 0x00, 0x00, 0x00 // Relative destination address + }; - return jmpBytes; + Array.Copy(BitConverter.GetBytes(targetAddress.ToInt32() - (currentAddress.ToInt32() + 5)), 0, jmpBytes, + 1, 4); } + + return jmpBytes; + } - private sealed class CodeWriterImpl : CodeWriter - { - private readonly List allBytes = new(); - public override void WriteByte(byte value) => allBytes.Add(value); - public byte[] ToArray() => allBytes.ToArray(); - } + private sealed class CodeWriterImpl : CodeWriter + { + private readonly List allBytes = new(); + public override void WriteByte(byte value) => allBytes.Add(value); + public byte[] ToArray() => allBytes.ToArray(); } } diff --git a/BepInEx.IL2CPP/Hook/FastNativeDetour.cs b/BepInEx.IL2CPP/Hook/FastNativeDetour.cs index d705407..d7264fe 100644 --- a/BepInEx.IL2CPP/Hook/FastNativeDetour.cs +++ b/BepInEx.IL2CPP/Hook/FastNativeDetour.cs @@ -6,172 +6,171 @@ using BepInEx.Logging; using MonoMod.RuntimeDetour; using MonoMod.Utils; -namespace BepInEx.IL2CPP.Hook +namespace BepInEx.IL2CPP.Hook; + +public class FastNativeDetour : IDetour { - public class FastNativeDetour : IDetour + private static readonly ManualLogSource logger = Logger.CreateLogSource("FastNativeDetour"); + + + public FastNativeDetour(IntPtr originalFunctionPtr, IntPtr detourFunctionPtr) { - private static readonly ManualLogSource logger = Logger.CreateLogSource("FastNativeDetour"); + OriginalFunctionPtr = originalFunctionPtr; + DetourFunctionPtr = detourFunctionPtr; + + // TODO: This may not be safe during undo if the method is smaller than 20 bytes + BackupBytes = new byte[20]; + Marshal.Copy(originalFunctionPtr, BackupBytes, 0, 20); + } + + protected byte[] BackupBytes { get; set; } - public FastNativeDetour(IntPtr originalFunctionPtr, IntPtr detourFunctionPtr) + public IntPtr OriginalFunctionPtr { get; protected set; } + public IntPtr DetourFunctionPtr { get; protected set; } + + + public IntPtr TrampolinePtr { get; protected set; } = IntPtr.Zero; + public int TrampolineSize { get; protected set; } + protected int TrampolineJmpSize { get; set; } + + protected MethodInfo TrampolineMethod { get; set; } + + public bool IsValid { get; protected set; } = true; + public bool IsApplied { get; protected set; } + + public void Apply() => Apply(null); + + + public void Undo() + { + if (!IsApplied) + return; + + Marshal.Copy(BackupBytes, 0, OriginalFunctionPtr, BackupBytes.Length); + + PageAllocator.Instance.Free(TrampolinePtr); + + TrampolinePtr = IntPtr.Zero; + TrampolineSize = 0; + + IsApplied = false; + } + + public void Free() => IsValid = false; + + public MethodBase GenerateTrampoline(MethodBase signature = null) + { + if (TrampolineMethod == null) { - OriginalFunctionPtr = originalFunctionPtr; - DetourFunctionPtr = detourFunctionPtr; + // Generate trampoline without applying the detour + GenerateTrampolineInner(out _, out _); - // TODO: This may not be safe during undo if the method is smaller than 20 bytes - BackupBytes = new byte[20]; - Marshal.Copy(originalFunctionPtr, BackupBytes, 0, 20); + if (TrampolinePtr == IntPtr.Zero) + throw new InvalidOperationException("Trampoline pointer is not available"); + + TrampolineMethod = DetourHelper.GenerateNativeProxy(TrampolinePtr, signature); } - protected byte[] BackupBytes { get; set; } + return TrampolineMethod; + } + + public T GenerateTrampoline() where T : Delegate + { + if (!typeof(Delegate).IsAssignableFrom(typeof(T))) + throw new InvalidOperationException($"Type {typeof(T)} not a delegate type."); + + return GenerateTrampoline(typeof(T).GetMethod("Invoke")).CreateDelegate(typeof(T)) as T; + } + + public void Dispose() + { + if (!IsValid) + return; + + Undo(); + Free(); + } + + public void Apply(ManualLogSource debuggerLogSource) + { + if (IsApplied) + return; - public IntPtr OriginalFunctionPtr { get; protected set; } - public IntPtr DetourFunctionPtr { get; protected set; } + DetourHelper.Native.MakeWritable(OriginalFunctionPtr, 32); - - public IntPtr TrampolinePtr { get; protected set; } = IntPtr.Zero; - public int TrampolineSize { get; protected set; } - protected int TrampolineJmpSize { get; set; } - - protected MethodInfo TrampolineMethod { get; set; } - - public bool IsValid { get; protected set; } = true; - public bool IsApplied { get; protected set; } - - public void Apply() => Apply(null); - - - public void Undo() + if (debuggerLogSource != null) { - if (!IsApplied) - return; - - Marshal.Copy(BackupBytes, 0, OriginalFunctionPtr, BackupBytes.Length); - - PageAllocator.Instance.Free(TrampolinePtr); - - TrampolinePtr = IntPtr.Zero; - TrampolineSize = 0; - - IsApplied = false; + debuggerLogSource + .LogDebug($"Detouring 0x{OriginalFunctionPtr.ToString("X")} -> 0x{DetourFunctionPtr.ToString("X")}"); + debuggerLogSource.LogDebug("Original (32) asm"); + DetourGenerator.Disassemble(debuggerLogSource, OriginalFunctionPtr, 32); } - public void Free() => IsValid = false; + var arch = IntPtr.Size == 8 ? Architecture.X64 : Architecture.X86; - public MethodBase GenerateTrampoline(MethodBase signature = null) + GenerateTrampolineInner(out var trampolineLength, out var jmpLength); + + DetourGenerator.ApplyDetour(OriginalFunctionPtr, DetourFunctionPtr, arch, trampolineLength - jmpLength); + + if (debuggerLogSource != null) { - if (TrampolineMethod == null) - { - // Generate trampoline without applying the detour - GenerateTrampolineInner(out _, out _); - - if (TrampolinePtr == IntPtr.Zero) - throw new InvalidOperationException("Trampoline pointer is not available"); - - TrampolineMethod = DetourHelper.GenerateNativeProxy(TrampolinePtr, signature); - } - - return TrampolineMethod; + debuggerLogSource.LogDebug($"Trampoline allocation: 0x{TrampolinePtr.ToString("X")}"); + debuggerLogSource.LogDebug("Modified (32) asm"); + DetourGenerator.Disassemble(debuggerLogSource, OriginalFunctionPtr, 32); + debuggerLogSource.LogDebug($"Trampoline ({trampolineLength}) asm"); + DetourGenerator.Disassemble(debuggerLogSource, TrampolinePtr, trampolineLength); } - public T GenerateTrampoline() where T : Delegate - { - if (!typeof(Delegate).IsAssignableFrom(typeof(T))) - throw new InvalidOperationException($"Type {typeof(T)} not a delegate type."); + DetourHelper.Native.MakeExecutable(OriginalFunctionPtr, 32); - return GenerateTrampoline(typeof(T).GetMethod("Invoke")).CreateDelegate(typeof(T)) as T; + IsApplied = true; + } + + + private void GenerateTrampolineInner(out int trampolineLength, out int jmpLength) + { + if (TrampolinePtr != IntPtr.Zero) + { + trampolineLength = TrampolineSize; + jmpLength = TrampolineJmpSize; + return; } - public void Dispose() - { - if (!IsValid) - return; + var instructionBuffer = new byte[32]; + Marshal.Copy(OriginalFunctionPtr, instructionBuffer, 0, 32); - Undo(); - Free(); - } + var trampolineAlloc = PageAllocator.Instance.Allocate(OriginalFunctionPtr); - public void Apply(ManualLogSource debuggerLogSource) - { - if (IsApplied) - return; + logger.LogDebug($"Original: {OriginalFunctionPtr.ToInt64():X}, Trampoline: {(long) trampolineAlloc:X}, diff: {Math.Abs(OriginalFunctionPtr.ToInt64() - trampolineAlloc):X}; is within +-1GB range: {PageAllocator.IsInRelJmpRange(OriginalFunctionPtr, trampolineAlloc)}"); + DetourHelper.Native.MakeWritable(trampolineAlloc, PageAllocator.PAGE_SIZE); - DetourHelper.Native.MakeWritable(OriginalFunctionPtr, 32); + var arch = IntPtr.Size == 8 ? Architecture.X64 : Architecture.X86; - if (debuggerLogSource != null) - { - debuggerLogSource - .LogDebug($"Detouring 0x{OriginalFunctionPtr.ToString("X")} -> 0x{DetourFunctionPtr.ToString("X")}"); - debuggerLogSource.LogDebug("Original (32) asm"); - DetourGenerator.Disassemble(debuggerLogSource, OriginalFunctionPtr, 32); - } + DetourGenerator.CreateTrampolineFromFunction(instructionBuffer, OriginalFunctionPtr, trampolineAlloc, + DetourGenerator.GetDetourLength(arch), arch, + out trampolineLength, out jmpLength); - var arch = IntPtr.Size == 8 ? Architecture.X64 : Architecture.X86; + DetourHelper.Native.MakeExecutable(trampolineAlloc, PageAllocator.PAGE_SIZE); - GenerateTrampolineInner(out var trampolineLength, out var jmpLength); + TrampolinePtr = trampolineAlloc; + TrampolineSize = trampolineLength; + TrampolineJmpSize = jmpLength; + } - DetourGenerator.ApplyDetour(OriginalFunctionPtr, DetourFunctionPtr, arch, trampolineLength - jmpLength); - - if (debuggerLogSource != null) - { - debuggerLogSource.LogDebug($"Trampoline allocation: 0x{TrampolinePtr.ToString("X")}"); - debuggerLogSource.LogDebug("Modified (32) asm"); - DetourGenerator.Disassemble(debuggerLogSource, OriginalFunctionPtr, 32); - debuggerLogSource.LogDebug($"Trampoline ({trampolineLength}) asm"); - DetourGenerator.Disassemble(debuggerLogSource, TrampolinePtr, trampolineLength); - } - - DetourHelper.Native.MakeExecutable(OriginalFunctionPtr, 32); - - IsApplied = true; - } - - - private void GenerateTrampolineInner(out int trampolineLength, out int jmpLength) - { - if (TrampolinePtr != IntPtr.Zero) - { - trampolineLength = TrampolineSize; - jmpLength = TrampolineJmpSize; - return; - } - - var instructionBuffer = new byte[32]; - Marshal.Copy(OriginalFunctionPtr, instructionBuffer, 0, 32); - - var trampolineAlloc = PageAllocator.Instance.Allocate(OriginalFunctionPtr); - - logger.LogDebug($"Original: {OriginalFunctionPtr.ToInt64():X}, Trampoline: {(long) trampolineAlloc:X}, diff: {Math.Abs(OriginalFunctionPtr.ToInt64() - trampolineAlloc):X}; is within +-1GB range: {PageAllocator.IsInRelJmpRange(OriginalFunctionPtr, trampolineAlloc)}"); - - DetourHelper.Native.MakeWritable(trampolineAlloc, PageAllocator.PAGE_SIZE); - - var arch = IntPtr.Size == 8 ? Architecture.X64 : Architecture.X86; - - DetourGenerator.CreateTrampolineFromFunction(instructionBuffer, OriginalFunctionPtr, trampolineAlloc, - DetourGenerator.GetDetourLength(arch), arch, - out trampolineLength, out jmpLength); - - DetourHelper.Native.MakeExecutable(trampolineAlloc, PageAllocator.PAGE_SIZE); - - TrampolinePtr = trampolineAlloc; - TrampolineSize = trampolineLength; - TrampolineJmpSize = jmpLength; - } - - public static FastNativeDetour CreateAndApply(IntPtr from, - T to, - out T original, - CallingConvention? callingConvention = null) where T : Delegate - { - var toPtr = callingConvention != null - ? MonoExtensions.GetFunctionPointerForDelegate(to, callingConvention.Value) - : Marshal.GetFunctionPointerForDelegate(to); - var result = new FastNativeDetour(from, toPtr); - original = result.GenerateTrampoline(); - result.Apply(); - return result; - } + public static FastNativeDetour CreateAndApply(IntPtr from, + T to, + out T original, + CallingConvention? callingConvention = null) where T : Delegate + { + var toPtr = callingConvention != null + ? MonoExtensions.GetFunctionPointerForDelegate(to, callingConvention.Value) + : Marshal.GetFunctionPointerForDelegate(to); + var result = new FastNativeDetour(from, toPtr); + original = result.GenerateTrampoline(); + result.Apply(); + return result; } } diff --git a/BepInEx.IL2CPP/Hook/IL2CPPDetourMethodPatcher.cs b/BepInEx.IL2CPP/Hook/IL2CPPDetourMethodPatcher.cs index 06fcb91..2eacd8f 100644 --- a/BepInEx.IL2CPP/Hook/IL2CPPDetourMethodPatcher.cs +++ b/BepInEx.IL2CPP/Hook/IL2CPPDetourMethodPatcher.cs @@ -16,410 +16,409 @@ using UnhollowerBaseLib.Runtime.VersionSpecific.MethodInfo; using ValueType = Il2CppSystem.ValueType; using Void = Il2CppSystem.Void; -namespace BepInEx.IL2CPP.Hook +namespace BepInEx.IL2CPP.Hook; + +public unsafe class IL2CPPDetourMethodPatcher : MethodPatcher { - public unsafe class IL2CPPDetourMethodPatcher : MethodPatcher + private static readonly MethodInfo IL2CPPToManagedStringMethodInfo + = AccessTools.Method(typeof(UnhollowerBaseLib.IL2CPP), + nameof(UnhollowerBaseLib.IL2CPP.Il2CppStringToManaged)); + + private static readonly MethodInfo ManagedToIL2CPPStringMethodInfo + = AccessTools.Method(typeof(UnhollowerBaseLib.IL2CPP), + nameof(UnhollowerBaseLib.IL2CPP.ManagedStringToIl2Cpp)); + + private static readonly MethodInfo ObjectBaseToPtrMethodInfo + = AccessTools.Method(typeof(UnhollowerBaseLib.IL2CPP), + nameof(UnhollowerBaseLib.IL2CPP.Il2CppObjectBaseToPtr)); + + private static readonly MethodInfo ReportExceptionMethodInfo + = AccessTools.Method(typeof(IL2CPPDetourMethodPatcher), nameof(ReportException)); + + + private static readonly ManualLogSource DetourLogger = Logger.CreateLogSource("Detour"); + + // Map each value type to correctly sized store opcode to prevent memory overwrite + // Special case: bool is byte in Il2Cpp + private static readonly Dictionary StIndOpcodes = new() { - private static readonly MethodInfo IL2CPPToManagedStringMethodInfo - = AccessTools.Method(typeof(UnhollowerBaseLib.IL2CPP), - nameof(UnhollowerBaseLib.IL2CPP.Il2CppStringToManaged)); + [typeof(byte)] = OpCodes.Stind_I1, + [typeof(sbyte)] = OpCodes.Stind_I1, + [typeof(bool)] = OpCodes.Stind_I1, + [typeof(short)] = OpCodes.Stind_I2, + [typeof(ushort)] = OpCodes.Stind_I2, + [typeof(int)] = OpCodes.Stind_I4, + [typeof(uint)] = OpCodes.Stind_I4, + [typeof(long)] = OpCodes.Stind_I8, + [typeof(ulong)] = OpCodes.Stind_I8, + [typeof(float)] = OpCodes.Stind_R4, + [typeof(double)] = OpCodes.Stind_R8 + }; - private static readonly MethodInfo ManagedToIL2CPPStringMethodInfo - = AccessTools.Method(typeof(UnhollowerBaseLib.IL2CPP), - nameof(UnhollowerBaseLib.IL2CPP.ManagedStringToIl2Cpp)); + private static AssemblyBuilder fixedStructAssembly; + private static ModuleBuilder fixedStructModuleBuilder; + private static readonly Dictionary FixedStructCache = new(); - private static readonly MethodInfo ObjectBaseToPtrMethodInfo - = AccessTools.Method(typeof(UnhollowerBaseLib.IL2CPP), - nameof(UnhollowerBaseLib.IL2CPP.Il2CppObjectBaseToPtr)); + private bool isValid; + private INativeMethodInfoStruct modifiedNativeMethodInfo; - private static readonly MethodInfo ReportExceptionMethodInfo - = AccessTools.Method(typeof(IL2CPPDetourMethodPatcher), nameof(ReportException)); + private FastNativeDetour nativeDetour; + private INativeMethodInfoStruct originalNativeMethodInfo; - private static readonly ManualLogSource DetourLogger = Logger.CreateLogSource("Detour"); + /// + /// Constructs a new instance of method patcher. + /// + /// + public IL2CPPDetourMethodPatcher(MethodBase original) : base(original) + { + Init(); + } - // Map each value type to correctly sized store opcode to prevent memory overwrite - // Special case: bool is byte in Il2Cpp - private static readonly Dictionary StIndOpcodes = new() + private void Init() + { + try { - [typeof(byte)] = OpCodes.Stind_I1, - [typeof(sbyte)] = OpCodes.Stind_I1, - [typeof(bool)] = OpCodes.Stind_I1, - [typeof(short)] = OpCodes.Stind_I2, - [typeof(ushort)] = OpCodes.Stind_I2, - [typeof(int)] = OpCodes.Stind_I4, - [typeof(uint)] = OpCodes.Stind_I4, - [typeof(long)] = OpCodes.Stind_I8, - [typeof(ulong)] = OpCodes.Stind_I8, - [typeof(float)] = OpCodes.Stind_R4, - [typeof(double)] = OpCodes.Stind_R8 - }; + var methodField = UnhollowerUtils.GetIl2CppMethodInfoPointerFieldForGeneratedMethod(Original); - private static AssemblyBuilder fixedStructAssembly; - private static ModuleBuilder fixedStructModuleBuilder; - private static readonly Dictionary FixedStructCache = new(); - - private bool isValid; - private INativeMethodInfoStruct modifiedNativeMethodInfo; - - private FastNativeDetour nativeDetour; - - private INativeMethodInfoStruct originalNativeMethodInfo; - - /// - /// Constructs a new instance of method patcher. - /// - /// - public IL2CPPDetourMethodPatcher(MethodBase original) : base(original) - { - Init(); - } - - private void Init() - { - try + if (methodField == null) { - var methodField = UnhollowerUtils.GetIl2CppMethodInfoPointerFieldForGeneratedMethod(Original); + var fieldInfoField = + UnhollowerUtils.GetIl2CppFieldInfoPointerFieldForGeneratedFieldAccessor(Original); - if (methodField == null) - { - var fieldInfoField = - UnhollowerUtils.GetIl2CppFieldInfoPointerFieldForGeneratedFieldAccessor(Original); + if (fieldInfoField != null) + throw new + Exception($"Method {Original.FullDescription()} is a field accessor, it can't be patched."); - if (fieldInfoField != null) - throw new - Exception($"Method {Original.FullDescription()} is a field accessor, it can't be patched."); - - // Generated method is probably unstripped, it can be safely handed to IL handler - return; - } - - // Get the native MethodInfo struct for the target method - originalNativeMethodInfo = - UnityVersionHandler.Wrap((Il2CppMethodInfo*)(IntPtr)methodField.GetValue(null)); - - // Create a trampoline from the original target method - var trampolinePtr = - DetourGenerator.CreateTrampolineFromFunction(originalNativeMethodInfo.MethodPointer, out _, out _); - - // Create a modified native MethodInfo struct to point towards the trampoline - modifiedNativeMethodInfo = UnityVersionHandler.NewMethod(); - Buffer.MemoryCopy(originalNativeMethodInfo.Pointer.ToPointer(), - modifiedNativeMethodInfo.Pointer.ToPointer(), modifiedNativeMethodInfo.StructSize, - originalNativeMethodInfo.StructSize); - modifiedNativeMethodInfo.MethodPointer = trampolinePtr; - isValid = true; - } - catch (Exception e) - { - DetourLogger - .LogWarning($"Failed to init IL2CPP patch backend for {Original.FullDescription()}, using normal patch handlers: {e.Message}"); - } - } - - /// - public override DynamicMethodDefinition PrepareOriginal() => null; - - /// - public override MethodBase DetourTo(MethodBase replacement) - { - // Unpatch an existing detour if it exists - nativeDetour?.Dispose(); - - // Generate a new DMD of the modified unhollowed method, and apply harmony patches to it - var copiedDmd = CopyOriginal(); - - HarmonyManipulator.Manipulate(copiedDmd.OriginalMethod, copiedDmd.OriginalMethod.GetPatchInfo(), - new ILContext(copiedDmd.Definition)); - - // Generate the MethodInfo instances - var managedHookedMethod = copiedDmd.Generate(); - var unmanagedTrampolineMethod = GenerateNativeToManagedTrampoline(managedHookedMethod).Generate(); - - // Apply a detour from the unmanaged implementation to the patched harmony method - var unmanagedDelegateType = DelegateTypeFactory.instance.CreateDelegateType(unmanagedTrampolineMethod, - CallingConvention.Cdecl); - - var detourPtr = - Marshal.GetFunctionPointerForDelegate(unmanagedTrampolineMethod.CreateDelegate(unmanagedDelegateType)); - nativeDetour = new FastNativeDetour(originalNativeMethodInfo.MethodPointer, detourPtr); - nativeDetour.Apply(); - - // TODO: Add an ILHook for the original unhollowed method to go directly to managedHookedMethod - // Right now it goes through three times as much interop conversion as it needs to, when being called from managed side - return managedHookedMethod; - } - - /// - public override DynamicMethodDefinition CopyOriginal() - { - var dmd = new DynamicMethodDefinition(Original); - dmd.Definition.Name = "UnhollowedWrapper_" + dmd.Definition.Name; - var cursor = new ILCursor(new ILContext(dmd.Definition)); - - - // Remove il2cpp_object_get_virtual_method - if (cursor.TryGotoNext(x => x.MatchLdarg(0), - x => x.MatchCall(typeof(UnhollowerBaseLib.IL2CPP), - nameof(UnhollowerBaseLib.IL2CPP.Il2CppObjectBaseToPtr)), - x => x.MatchLdsfld(out _), - x => x.MatchCall(typeof(UnhollowerBaseLib.IL2CPP), - nameof(UnhollowerBaseLib.IL2CPP.il2cpp_object_get_virtual_method)))) - cursor.RemoveRange(4); - else - cursor.Goto(0) - .GotoNext(x => - x.MatchLdsfld(UnhollowerUtils - .GetIl2CppMethodInfoPointerFieldForGeneratedMethod(Original))) - .Remove(); - - // Replace original IL2CPPMethodInfo pointer with a modified one that points to the trampoline - cursor - .Emit(Mono.Cecil.Cil.OpCodes.Ldc_I8, modifiedNativeMethodInfo.Pointer.ToInt64()) - .Emit(Mono.Cecil.Cil.OpCodes.Conv_I); - - return dmd; - } - - /// - /// A handler for that checks if a method doesn't have a body - /// (e.g. it's icall or marked with ) and thus can be patched with - /// . - /// - /// Not used - /// Patch resolver arguments - public static void TryResolve(object sender, PatchManager.PatcherResolverEventArgs args) - { - if (args.Original.DeclaringType?.IsSubclassOf(typeof(Il2CppObjectBase)) == true) - { - var backend = new IL2CPPDetourMethodPatcher(args.Original); - if (backend.isValid) - args.MethodPatcher = backend; - } - } - - private DynamicMethodDefinition GenerateNativeToManagedTrampoline(MethodInfo targetManagedMethodInfo) - { - // managedParams are the unhollower types used on the managed side - // unmanagedParams are IntPtr references that are used by IL2CPP compiled assembly - var paramStartIndex = Original.IsStatic ? 0 : 1; - var managedParams = Original.GetParameters().Select(x => x.ParameterType).ToArray(); - var unmanagedParams = - new Type[managedParams.Length + paramStartIndex + - 1]; // +1 for thisptr if needed, +1 for methodInfo at the end - - if (!Original.IsStatic) - unmanagedParams[0] = typeof(IntPtr); - unmanagedParams[^1] = typeof(Il2CppMethodInfo*); - Array.Copy(managedParams.Select(ConvertManagedTypeToIL2CPPType).ToArray(), 0, - unmanagedParams, paramStartIndex, managedParams.Length); - - var managedReturnType = AccessTools.GetReturnedType(Original); - var unmanagedReturnType = ConvertManagedTypeToIL2CPPType(managedReturnType); - - var dmd = new DynamicMethodDefinition("(il2cpp -> managed) " + Original.Name, - unmanagedReturnType, - unmanagedParams - ); - - var il = dmd.GetILGenerator(); - il.BeginExceptionBlock(); - - // Declare a list of variables to dereference back to the original pointers. - // This is required due to the needed unhollower type conversions, so we can't directly pass some addresses as byref types - var indirectVariables = new LocalBuilder[managedParams.Length]; - - if (!Original.IsStatic) - EmitConvertArgumentToManaged(il, 0, Original.DeclaringType, out _); - for (var i = 0; i < managedParams.Length; ++i) - EmitConvertArgumentToManaged(il, i + paramStartIndex, managedParams[i], out indirectVariables[i]); - - // Run the managed method - il.Emit(OpCodes.Call, targetManagedMethodInfo); - - // Store the managed return type temporarily (if there was one) - LocalBuilder managedReturnVariable = null; - if (managedReturnType != typeof(void)) - { - managedReturnVariable = il.DeclareLocal(managedReturnType); - il.Emit(OpCodes.Stloc, managedReturnVariable); - } - - // Convert any managed byref values into their relevant IL2CPP types, and then store the values into their relevant dereferenced pointers - for (var i = 0; i < managedParams.Length; ++i) - { - if (indirectVariables[i] == null) - continue; - - il.Emit(OpCodes.Ldarg_S, i + paramStartIndex); - il.Emit(OpCodes.Ldloc, indirectVariables[i]); - var directType = managedParams[i].GetElementType(); - EmitConvertManagedTypeToIL2CPP(il, directType); - il.Emit(StIndOpcodes.TryGetValue(directType, out var stindOpCodde) ? stindOpCodde : OpCodes.Stind_I); - } - - // Handle any lingering exceptions - il.BeginCatchBlock(typeof(Exception)); - il.Emit(OpCodes.Call, ReportExceptionMethodInfo); - il.EndExceptionBlock(); - - // Convert the return value back to an IL2CPP friendly type (if there was a return value), and then return - if (managedReturnVariable != null) - { - il.Emit(OpCodes.Ldloc, managedReturnVariable); - EmitConvertManagedTypeToIL2CPP(il, managedReturnType); - } - - il.Emit(OpCodes.Ret); - - return dmd; - } - - private static void ReportException(Exception ex) => DetourLogger.LogError(ex.ToString()); - - private static Type ConvertManagedTypeToIL2CPPType(Type managedType) - { - if (managedType.IsByRef) - { - var directType = managedType.GetElementType(); - - // bool is byte in Il2Cpp, but int in CLR => force size to be correct - if (directType == typeof(bool)) - return typeof(byte).MakeByRefType(); - if (directType == typeof(string) || directType.IsSubclassOf(typeof(Il2CppObjectBase))) - return typeof(IntPtr*); - } - else if (managedType.IsSubclassOf(typeof(ValueType)) - ) // Struct that's passed on the stack => handle as general struct - { - uint align = 0; - var fixedSize = - UnhollowerBaseLib.IL2CPP.il2cpp_class_value_size(Il2CppTypeToClassPointer(managedType), ref align); - return GetFixedSizeStructType(fixedSize); - } - else if (managedType == typeof(string) || managedType.IsSubclassOf(typeof(Il2CppObjectBase)) - ) // General reference type - { - return typeof(IntPtr); - } - - return managedType; - } - - private static void EmitConvertManagedTypeToIL2CPP(ILGenerator il, Type returnType) - { - if (returnType == typeof(string)) - il.Emit(OpCodes.Call, ManagedToIL2CPPStringMethodInfo); - else if (!returnType.IsValueType && returnType.IsSubclassOf(typeof(Il2CppObjectBase))) - il.Emit(OpCodes.Call, ObjectBaseToPtrMethodInfo); - } - - private static IntPtr Il2CppTypeToClassPointer(Type type) - { - if (type == typeof(void)) - return Il2CppClassPointerStore.NativeClassPtr; - return (IntPtr)typeof(Il2CppClassPointerStore<>).MakeGenericType(type).GetField("NativeClassPtr") - .GetValue(null); - } - - private static void EmitConvertArgumentToManaged(ILGenerator il, - int argIndex, - Type managedParamType, - out LocalBuilder variable) - { - variable = null; - - // Box struct into object first before conversion - // This will likely incur struct copying down the line, but it shouldn't be a massive loss - if (managedParamType.IsSubclassOf(typeof(ValueType))) - { - il.Emit(OpCodes.Ldc_I8, Il2CppTypeToClassPointer(managedParamType).ToInt64()); - il.Emit(OpCodes.Conv_I); - il.Emit(OpCodes.Ldarga_S, argIndex); - il.Emit(OpCodes.Call, - AccessTools.Method(typeof(UnhollowerBaseLib.IL2CPP), - nameof(UnhollowerBaseLib.IL2CPP.il2cpp_value_box))); - } - else - { - il.Emit(OpCodes.Ldarg_S, argIndex); - } - - if (managedParamType.IsValueType) // don't need to convert blittable types + // Generated method is probably unstripped, it can be safely handed to IL handler return; - - void EmitCreateIl2CppObject(Type originalType) - { - var endLabel = il.DefineLabel(); - var notNullLabel = il.DefineLabel(); - - il.Emit(OpCodes.Dup); - il.Emit(OpCodes.Brtrue_S, notNullLabel); - - il.Emit(OpCodes.Pop); - il.Emit(OpCodes.Ldnull); - il.Emit(OpCodes.Br_S, endLabel); - - il.MarkLabel(notNullLabel); - il.Emit(OpCodes.Newobj, AccessTools.DeclaredConstructor(originalType, new[] { typeof(IntPtr) })); - - il.MarkLabel(endLabel); } - void HandleTypeConversion(Type originalType) - { - if (originalType == typeof(string)) - il.Emit(OpCodes.Call, IL2CPPToManagedStringMethodInfo); - else if (originalType.IsSubclassOf(typeof(Il2CppObjectBase))) - EmitCreateIl2CppObject(originalType); - } + // Get the native MethodInfo struct for the target method + originalNativeMethodInfo = + UnityVersionHandler.Wrap((Il2CppMethodInfo*) (IntPtr) methodField.GetValue(null)); - if (managedParamType.IsByRef) - { - // TODO: directType being ValueType is not handled yet (but it's not that common in games). Implement when needed. - var directType = managedParamType.GetElementType(); + // Create a trampoline from the original target method + var trampolinePtr = + DetourGenerator.CreateTrampolineFromFunction(originalNativeMethodInfo.MethodPointer, out _, out _); - variable = il.DeclareLocal(directType); - - il.Emit(OpCodes.Ldind_I); - - HandleTypeConversion(directType); - - il.Emit(OpCodes.Stloc, variable); - il.Emit(OpCodes.Ldloca, variable); - } - else - { - HandleTypeConversion(managedParamType); - } + // Create a modified native MethodInfo struct to point towards the trampoline + modifiedNativeMethodInfo = UnityVersionHandler.NewMethod(); + Buffer.MemoryCopy(originalNativeMethodInfo.Pointer.ToPointer(), + modifiedNativeMethodInfo.Pointer.ToPointer(), modifiedNativeMethodInfo.StructSize, + originalNativeMethodInfo.StructSize); + modifiedNativeMethodInfo.MethodPointer = trampolinePtr; + isValid = true; } - - private static Type GetFixedSizeStructType(int size) + catch (Exception e) { - if (FixedStructCache.TryGetValue(size, out var result)) - return result; - - fixedStructAssembly ??= - AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("FixedSizeStructAssembly"), - AssemblyBuilderAccess.RunAndCollect); - fixedStructModuleBuilder ??= fixedStructAssembly.DefineDynamicModule("FixedSizeStructAssembly"); - - var tb = fixedStructModuleBuilder.DefineType($"IL2CPPDetour_FixedSizeStruct_{size}b", - TypeAttributes.SequentialLayout, typeof(System.ValueType)); - var fb = tb.DefineField("buffer", typeof(IntPtr), FieldAttributes.Public); - fb.SetCustomAttribute(new - CustomAttributeBuilder(AccessTools.Constructor(typeof(MarshalAsAttribute), new[] { typeof(UnmanagedType) }), - new object[] { UnmanagedType.ByValArray }, - new[] - { - AccessTools.Field(typeof(MarshalAsAttribute), - nameof(MarshalAsAttribute.SizeConst)), - AccessTools.Field(typeof(MarshalAsAttribute), - nameof(MarshalAsAttribute.ArraySubType)) - }, new object[] { size, UnmanagedType.U1 })); - - var type = tb.CreateType(); - return FixedStructCache[size] = type; + DetourLogger + .LogWarning($"Failed to init IL2CPP patch backend for {Original.FullDescription()}, using normal patch handlers: {e.Message}"); } } + + /// + public override DynamicMethodDefinition PrepareOriginal() => null; + + /// + public override MethodBase DetourTo(MethodBase replacement) + { + // Unpatch an existing detour if it exists + nativeDetour?.Dispose(); + + // Generate a new DMD of the modified unhollowed method, and apply harmony patches to it + var copiedDmd = CopyOriginal(); + + HarmonyManipulator.Manipulate(copiedDmd.OriginalMethod, copiedDmd.OriginalMethod.GetPatchInfo(), + new ILContext(copiedDmd.Definition)); + + // Generate the MethodInfo instances + var managedHookedMethod = copiedDmd.Generate(); + var unmanagedTrampolineMethod = GenerateNativeToManagedTrampoline(managedHookedMethod).Generate(); + + // Apply a detour from the unmanaged implementation to the patched harmony method + var unmanagedDelegateType = DelegateTypeFactory.instance.CreateDelegateType(unmanagedTrampolineMethod, + CallingConvention.Cdecl); + + var detourPtr = + Marshal.GetFunctionPointerForDelegate(unmanagedTrampolineMethod.CreateDelegate(unmanagedDelegateType)); + nativeDetour = new FastNativeDetour(originalNativeMethodInfo.MethodPointer, detourPtr); + nativeDetour.Apply(); + + // TODO: Add an ILHook for the original unhollowed method to go directly to managedHookedMethod + // Right now it goes through three times as much interop conversion as it needs to, when being called from managed side + return managedHookedMethod; + } + + /// + public override DynamicMethodDefinition CopyOriginal() + { + var dmd = new DynamicMethodDefinition(Original); + dmd.Definition.Name = "UnhollowedWrapper_" + dmd.Definition.Name; + var cursor = new ILCursor(new ILContext(dmd.Definition)); + + + // Remove il2cpp_object_get_virtual_method + if (cursor.TryGotoNext(x => x.MatchLdarg(0), + x => x.MatchCall(typeof(UnhollowerBaseLib.IL2CPP), + nameof(UnhollowerBaseLib.IL2CPP.Il2CppObjectBaseToPtr)), + x => x.MatchLdsfld(out _), + x => x.MatchCall(typeof(UnhollowerBaseLib.IL2CPP), + nameof(UnhollowerBaseLib.IL2CPP.il2cpp_object_get_virtual_method)))) + cursor.RemoveRange(4); + else + cursor.Goto(0) + .GotoNext(x => + x.MatchLdsfld(UnhollowerUtils + .GetIl2CppMethodInfoPointerFieldForGeneratedMethod(Original))) + .Remove(); + + // Replace original IL2CPPMethodInfo pointer with a modified one that points to the trampoline + cursor + .Emit(Mono.Cecil.Cil.OpCodes.Ldc_I8, modifiedNativeMethodInfo.Pointer.ToInt64()) + .Emit(Mono.Cecil.Cil.OpCodes.Conv_I); + + return dmd; + } + + /// + /// A handler for that checks if a method doesn't have a body + /// (e.g. it's icall or marked with ) and thus can be patched with + /// . + /// + /// Not used + /// Patch resolver arguments + public static void TryResolve(object sender, PatchManager.PatcherResolverEventArgs args) + { + if (args.Original.DeclaringType?.IsSubclassOf(typeof(Il2CppObjectBase)) == true) + { + var backend = new IL2CPPDetourMethodPatcher(args.Original); + if (backend.isValid) + args.MethodPatcher = backend; + } + } + + private DynamicMethodDefinition GenerateNativeToManagedTrampoline(MethodInfo targetManagedMethodInfo) + { + // managedParams are the unhollower types used on the managed side + // unmanagedParams are IntPtr references that are used by IL2CPP compiled assembly + var paramStartIndex = Original.IsStatic ? 0 : 1; + var managedParams = Original.GetParameters().Select(x => x.ParameterType).ToArray(); + var unmanagedParams = + new Type[managedParams.Length + paramStartIndex + + 1]; // +1 for thisptr if needed, +1 for methodInfo at the end + + if (!Original.IsStatic) + unmanagedParams[0] = typeof(IntPtr); + unmanagedParams[^1] = typeof(Il2CppMethodInfo*); + Array.Copy(managedParams.Select(ConvertManagedTypeToIL2CPPType).ToArray(), 0, + unmanagedParams, paramStartIndex, managedParams.Length); + + var managedReturnType = AccessTools.GetReturnedType(Original); + var unmanagedReturnType = ConvertManagedTypeToIL2CPPType(managedReturnType); + + var dmd = new DynamicMethodDefinition("(il2cpp -> managed) " + Original.Name, + unmanagedReturnType, + unmanagedParams + ); + + var il = dmd.GetILGenerator(); + il.BeginExceptionBlock(); + + // Declare a list of variables to dereference back to the original pointers. + // This is required due to the needed unhollower type conversions, so we can't directly pass some addresses as byref types + var indirectVariables = new LocalBuilder[managedParams.Length]; + + if (!Original.IsStatic) + EmitConvertArgumentToManaged(il, 0, Original.DeclaringType, out _); + for (var i = 0; i < managedParams.Length; ++i) + EmitConvertArgumentToManaged(il, i + paramStartIndex, managedParams[i], out indirectVariables[i]); + + // Run the managed method + il.Emit(OpCodes.Call, targetManagedMethodInfo); + + // Store the managed return type temporarily (if there was one) + LocalBuilder managedReturnVariable = null; + if (managedReturnType != typeof(void)) + { + managedReturnVariable = il.DeclareLocal(managedReturnType); + il.Emit(OpCodes.Stloc, managedReturnVariable); + } + + // Convert any managed byref values into their relevant IL2CPP types, and then store the values into their relevant dereferenced pointers + for (var i = 0; i < managedParams.Length; ++i) + { + if (indirectVariables[i] == null) + continue; + + il.Emit(OpCodes.Ldarg_S, i + paramStartIndex); + il.Emit(OpCodes.Ldloc, indirectVariables[i]); + var directType = managedParams[i].GetElementType(); + EmitConvertManagedTypeToIL2CPP(il, directType); + il.Emit(StIndOpcodes.TryGetValue(directType, out var stindOpCodde) ? stindOpCodde : OpCodes.Stind_I); + } + + // Handle any lingering exceptions + il.BeginCatchBlock(typeof(Exception)); + il.Emit(OpCodes.Call, ReportExceptionMethodInfo); + il.EndExceptionBlock(); + + // Convert the return value back to an IL2CPP friendly type (if there was a return value), and then return + if (managedReturnVariable != null) + { + il.Emit(OpCodes.Ldloc, managedReturnVariable); + EmitConvertManagedTypeToIL2CPP(il, managedReturnType); + } + + il.Emit(OpCodes.Ret); + + return dmd; + } + + private static void ReportException(Exception ex) => DetourLogger.LogError(ex.ToString()); + + private static Type ConvertManagedTypeToIL2CPPType(Type managedType) + { + if (managedType.IsByRef) + { + var directType = managedType.GetElementType(); + + // bool is byte in Il2Cpp, but int in CLR => force size to be correct + if (directType == typeof(bool)) + return typeof(byte).MakeByRefType(); + if (directType == typeof(string) || directType.IsSubclassOf(typeof(Il2CppObjectBase))) + return typeof(IntPtr*); + } + else if (managedType.IsSubclassOf(typeof(ValueType)) + ) // Struct that's passed on the stack => handle as general struct + { + uint align = 0; + var fixedSize = + UnhollowerBaseLib.IL2CPP.il2cpp_class_value_size(Il2CppTypeToClassPointer(managedType), ref align); + return GetFixedSizeStructType(fixedSize); + } + else if (managedType == typeof(string) || managedType.IsSubclassOf(typeof(Il2CppObjectBase)) + ) // General reference type + { + return typeof(IntPtr); + } + + return managedType; + } + + private static void EmitConvertManagedTypeToIL2CPP(ILGenerator il, Type returnType) + { + if (returnType == typeof(string)) + il.Emit(OpCodes.Call, ManagedToIL2CPPStringMethodInfo); + else if (!returnType.IsValueType && returnType.IsSubclassOf(typeof(Il2CppObjectBase))) + il.Emit(OpCodes.Call, ObjectBaseToPtrMethodInfo); + } + + private static IntPtr Il2CppTypeToClassPointer(Type type) + { + if (type == typeof(void)) + return Il2CppClassPointerStore.NativeClassPtr; + return (IntPtr) typeof(Il2CppClassPointerStore<>).MakeGenericType(type).GetField("NativeClassPtr") + .GetValue(null); + } + + private static void EmitConvertArgumentToManaged(ILGenerator il, + int argIndex, + Type managedParamType, + out LocalBuilder variable) + { + variable = null; + + // Box struct into object first before conversion + // This will likely incur struct copying down the line, but it shouldn't be a massive loss + if (managedParamType.IsSubclassOf(typeof(ValueType))) + { + il.Emit(OpCodes.Ldc_I8, Il2CppTypeToClassPointer(managedParamType).ToInt64()); + il.Emit(OpCodes.Conv_I); + il.Emit(OpCodes.Ldarga_S, argIndex); + il.Emit(OpCodes.Call, + AccessTools.Method(typeof(UnhollowerBaseLib.IL2CPP), + nameof(UnhollowerBaseLib.IL2CPP.il2cpp_value_box))); + } + else + { + il.Emit(OpCodes.Ldarg_S, argIndex); + } + + if (managedParamType.IsValueType) // don't need to convert blittable types + return; + + void EmitCreateIl2CppObject(Type originalType) + { + var endLabel = il.DefineLabel(); + var notNullLabel = il.DefineLabel(); + + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Brtrue_S, notNullLabel); + + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Br_S, endLabel); + + il.MarkLabel(notNullLabel); + il.Emit(OpCodes.Newobj, AccessTools.DeclaredConstructor(originalType, new[] { typeof(IntPtr) })); + + il.MarkLabel(endLabel); + } + + void HandleTypeConversion(Type originalType) + { + if (originalType == typeof(string)) + il.Emit(OpCodes.Call, IL2CPPToManagedStringMethodInfo); + else if (originalType.IsSubclassOf(typeof(Il2CppObjectBase))) + EmitCreateIl2CppObject(originalType); + } + + if (managedParamType.IsByRef) + { + // TODO: directType being ValueType is not handled yet (but it's not that common in games). Implement when needed. + var directType = managedParamType.GetElementType(); + + variable = il.DeclareLocal(directType); + + il.Emit(OpCodes.Ldind_I); + + HandleTypeConversion(directType); + + il.Emit(OpCodes.Stloc, variable); + il.Emit(OpCodes.Ldloca, variable); + } + else + { + HandleTypeConversion(managedParamType); + } + } + + private static Type GetFixedSizeStructType(int size) + { + if (FixedStructCache.TryGetValue(size, out var result)) + return result; + + fixedStructAssembly ??= + AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("FixedSizeStructAssembly"), + AssemblyBuilderAccess.RunAndCollect); + fixedStructModuleBuilder ??= fixedStructAssembly.DefineDynamicModule("FixedSizeStructAssembly"); + + var tb = fixedStructModuleBuilder.DefineType($"IL2CPPDetour_FixedSizeStruct_{size}b", + TypeAttributes.SequentialLayout, typeof(System.ValueType)); + var fb = tb.DefineField("buffer", typeof(IntPtr), FieldAttributes.Public); + fb.SetCustomAttribute(new + CustomAttributeBuilder(AccessTools.Constructor(typeof(MarshalAsAttribute), new[] { typeof(UnmanagedType) }), + new object[] { UnmanagedType.ByValArray }, + new[] + { + AccessTools.Field(typeof(MarshalAsAttribute), + nameof(MarshalAsAttribute.SizeConst)), + AccessTools.Field(typeof(MarshalAsAttribute), + nameof(MarshalAsAttribute.ArraySubType)) + }, new object[] { size, UnmanagedType.U1 })); + + var type = tb.CreateType(); + return FixedStructCache[size] = type; + } } diff --git a/BepInEx.IL2CPP/Hook/UnhollowerDetourHandler.cs b/BepInEx.IL2CPP/Hook/UnhollowerDetourHandler.cs index 3adcfe2..ab68d51 100644 --- a/BepInEx.IL2CPP/Hook/UnhollowerDetourHandler.cs +++ b/BepInEx.IL2CPP/Hook/UnhollowerDetourHandler.cs @@ -1,14 +1,13 @@ using System; using UnhollowerRuntimeLib; -namespace BepInEx.IL2CPP.Hook +namespace BepInEx.IL2CPP.Hook; + +public class UnhollowerDetourHandler : IManagedDetour { - public class UnhollowerDetourHandler : IManagedDetour + public T Detour(IntPtr from, T to) where T : Delegate { - public T Detour(IntPtr from, T to) where T : Delegate - { - FastNativeDetour.CreateAndApply(from, to, out var original); - return original; - } + FastNativeDetour.CreateAndApply(from, to, out var original); + return original; } } diff --git a/BepInEx.IL2CPP/IL2CPPChainloader.cs b/BepInEx.IL2CPP/IL2CPPChainloader.cs index b5d5276..f9b4c3c 100644 --- a/BepInEx.IL2CPP/IL2CPPChainloader.cs +++ b/BepInEx.IL2CPP/IL2CPPChainloader.cs @@ -19,156 +19,155 @@ using UnhollowerRuntimeLib; using UnityEngine; using Logger = BepInEx.Logging.Logger; -namespace BepInEx.IL2CPP +namespace BepInEx.IL2CPP; + +public class IL2CPPChainloader : BaseChainloader { - public class IL2CPPChainloader : BaseChainloader + private static RuntimeInvokeDetourDelegate originalInvoke; + private static InstallUnityTlsInterfaceDelegate originalInstallUnityTlsInterface; + + private static readonly ConfigEntry ConfigUnityLogging = ConfigFile.CoreConfig.Bind( + "Logging", "UnityLogListening", + true, + "Enables showing unity log messages in the BepInEx logging system."); + + private static readonly ConfigEntry ConfigDiskWriteUnityLog = ConfigFile.CoreConfig.Bind( + "Logging.Disk", "WriteUnityLog", + false, + "Include unity log messages in log file output."); + + private static FastNativeDetour RuntimeInvokeDetour { get; set; } + private static FastNativeDetour InstallUnityTlsInterfaceDetour { 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. + /// + /// Type of the component to add. + public static T AddUnityComponent() where T : Il2CppObjectBase => AddUnityComponent(typeof(T)).Cast(); + + /// + /// 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. + /// + /// Type of the component to add + public static Il2CppObjectBase AddUnityComponent(Type t) => Il2CppUtils.AddComponent(t); + + public override void Initialize(string gameExePath = null) { - private static RuntimeInvokeDetourDelegate originalInvoke; - private static InstallUnityTlsInterfaceDelegate originalInstallUnityTlsInterface; + GeneratedDatabasesUtil.DatabasesLocationOverride = Preloader.IL2CPPUnhollowedPath; + PatchManager.ResolvePatcher += IL2CPPDetourMethodPatcher.TryResolve; - private static readonly ConfigEntry ConfigUnityLogging = ConfigFile.CoreConfig.Bind( - "Logging", "UnityLogListening", - true, - "Enables showing unity log messages in the BepInEx logging system."); + base.Initialize(gameExePath); + Instance = this; - private static readonly ConfigEntry ConfigDiskWriteUnityLog = ConfigFile.CoreConfig.Bind( - "Logging.Disk", "WriteUnityLog", - false, - "Include unity log messages in log file output."); + ClassInjector.Detour = new UnhollowerDetourHandler(); - private static FastNativeDetour RuntimeInvokeDetour { get; set; } - private static FastNativeDetour InstallUnityTlsInterfaceDetour { get; set; } + var gameAssemblyModule = Process.GetCurrentProcess().Modules.Cast() + .FirstOrDefault(x => x.ModuleName.Contains("GameAssembly") || + x.ModuleName.Contains("UserAssembly")); - 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. - /// - /// Type of the component to add. - public static T AddUnityComponent() where T : Il2CppObjectBase => AddUnityComponent(typeof(T)).Cast(); - - /// - /// 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. - /// - /// Type of the component to add - public static Il2CppObjectBase AddUnityComponent(Type t) => Il2CppUtils.AddComponent(t); - - public override void Initialize(string gameExePath = null) + if (gameAssemblyModule == null) { - GeneratedDatabasesUtil.DatabasesLocationOverride = Preloader.IL2CPPUnhollowedPath; - PatchManager.ResolvePatcher += IL2CPPDetourMethodPatcher.TryResolve; - - base.Initialize(gameExePath); - Instance = this; - - ClassInjector.Detour = new UnhollowerDetourHandler(); - - var gameAssemblyModule = Process.GetCurrentProcess().Modules.Cast() - .FirstOrDefault(x => x.ModuleName.Contains("GameAssembly") || - x.ModuleName.Contains("UserAssembly")); - - if (gameAssemblyModule == null) - { - Logger.LogFatal("Could not locate Il2Cpp game assembly (GameAssembly.dll) or (UserAssembly.dll). The game might be obfuscated or use a yet unsupported build of Unity."); - return; - } - - gameAssemblyModule.BaseAddress.TryGetFunction("il2cpp_runtime_invoke", out var runtimeInvokePtr); - PreloaderLogger.Log.LogDebug($"Runtime invoke pointer: 0x{runtimeInvokePtr.ToInt64():X}"); - RuntimeInvokeDetour = - FastNativeDetour.CreateAndApply(runtimeInvokePtr, OnInvokeMethod, out originalInvoke, - CallingConvention.Cdecl); - - if (gameAssemblyModule.BaseAddress.TryGetFunction("il2cpp_unity_install_unitytls_interface", - out var installTlsPtr)) - InstallUnityTlsInterfaceDetour = - FastNativeDetour.CreateAndApply(installTlsPtr, OnInstallUnityTlsInterface, - out originalInstallUnityTlsInterface, CallingConvention.Cdecl); - - Logger.LogDebug("Initializing TLS adapters"); - Il2CppTlsAdapter.Initialize(); - - PreloaderLogger.Log.LogDebug("Runtime invoke patched"); + Logger.LogFatal("Could not locate Il2Cpp game assembly (GameAssembly.dll) or (UserAssembly.dll). The game might be obfuscated or use a yet unsupported build of Unity."); + return; } - private void OnInstallUnityTlsInterface(IntPtr unityTlsInterfaceStruct) - { - Logger.LogDebug($"Captured UnityTls interface at {unityTlsInterfaceStruct.ToInt64():x8}"); - Il2CppTlsAdapter.Options.UnityTlsInterface = unityTlsInterfaceStruct; - originalInstallUnityTlsInterface(unityTlsInterfaceStruct); - InstallUnityTlsInterfaceDetour.Dispose(); - InstallUnityTlsInterfaceDetour = null; - } + gameAssemblyModule.BaseAddress.TryGetFunction("il2cpp_runtime_invoke", out var runtimeInvokePtr); + PreloaderLogger.Log.LogDebug($"Runtime invoke pointer: 0x{runtimeInvokePtr.ToInt64():X}"); + RuntimeInvokeDetour = + FastNativeDetour.CreateAndApply(runtimeInvokePtr, OnInvokeMethod, out originalInvoke, + CallingConvention.Cdecl); - private static IntPtr OnInvokeMethod(IntPtr method, IntPtr obj, IntPtr parameters, IntPtr exc) - { - var methodName = Marshal.PtrToStringAnsi(UnhollowerBaseLib.IL2CPP.il2cpp_method_get_name(method)); + if (gameAssemblyModule.BaseAddress.TryGetFunction("il2cpp_unity_install_unitytls_interface", + out var installTlsPtr)) + InstallUnityTlsInterfaceDetour = + FastNativeDetour.CreateAndApply(installTlsPtr, OnInstallUnityTlsInterface, + out originalInstallUnityTlsInterface, CallingConvention.Cdecl); - var unhook = false; + Logger.LogDebug("Initializing TLS adapters"); + Il2CppTlsAdapter.Initialize(); - 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 = true; - - Instance.Execute(); - } - catch (Exception ex) - { - Logger.LogFatal("Unable to execute IL2CPP chainloader"); - Logger.LogError(ex); - } - - var result = originalInvoke(method, obj, parameters, exc); - - if (unhook) - { - RuntimeInvokeDetour.Dispose(); - - PreloaderLogger.Log.LogDebug("Runtime invoke unpatched"); - } - - return result; - } - - protected override void InitializeLoggers() - { - base.InitializeLoggers(); - - if (!ConfigDiskWriteUnityLog.Value) DiskLogListener.BlacklistedSources.Add("Unity"); - - ChainloaderLogHelper.RewritePreloaderLogs(); - - Logger.Sources.Add(new IL2CPPLogSource()); - } - - public override BasePlugin LoadPlugin(PluginInfo pluginInfo, Assembly pluginAssembly) - { - var type = pluginAssembly.GetType(pluginInfo.TypeName); - - var pluginInstance = (BasePlugin)Activator.CreateInstance(type); - - pluginInstance.Load(); - - return pluginInstance; - } - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate IntPtr RuntimeInvokeDetourDelegate(IntPtr method, IntPtr obj, IntPtr parameters, IntPtr exc); - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate void InstallUnityTlsInterfaceDelegate(IntPtr unityTlsInterfaceStruct); + PreloaderLogger.Log.LogDebug("Runtime invoke patched"); } + + private void OnInstallUnityTlsInterface(IntPtr unityTlsInterfaceStruct) + { + Logger.LogDebug($"Captured UnityTls interface at {unityTlsInterfaceStruct.ToInt64():x8}"); + Il2CppTlsAdapter.Options.UnityTlsInterface = unityTlsInterfaceStruct; + originalInstallUnityTlsInterface(unityTlsInterfaceStruct); + InstallUnityTlsInterfaceDetour.Dispose(); + InstallUnityTlsInterfaceDetour = null; + } + + private static IntPtr OnInvokeMethod(IntPtr method, IntPtr obj, IntPtr parameters, IntPtr exc) + { + var methodName = Marshal.PtrToStringAnsi(UnhollowerBaseLib.IL2CPP.il2cpp_method_get_name(method)); + + var unhook = false; + + 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 = true; + + Instance.Execute(); + } + catch (Exception ex) + { + Logger.LogFatal("Unable to execute IL2CPP chainloader"); + Logger.LogError(ex); + } + + var result = originalInvoke(method, obj, parameters, exc); + + if (unhook) + { + RuntimeInvokeDetour.Dispose(); + + PreloaderLogger.Log.LogDebug("Runtime invoke unpatched"); + } + + return result; + } + + protected override void InitializeLoggers() + { + base.InitializeLoggers(); + + if (!ConfigDiskWriteUnityLog.Value) DiskLogListener.BlacklistedSources.Add("Unity"); + + ChainloaderLogHelper.RewritePreloaderLogs(); + + Logger.Sources.Add(new IL2CPPLogSource()); + } + + public override BasePlugin LoadPlugin(PluginInfo pluginInfo, Assembly pluginAssembly) + { + var type = pluginAssembly.GetType(pluginInfo.TypeName); + + var pluginInstance = (BasePlugin) Activator.CreateInstance(type); + + pluginInstance.Load(); + + return pluginInstance; + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate IntPtr RuntimeInvokeDetourDelegate(IntPtr method, IntPtr obj, IntPtr parameters, IntPtr exc); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void InstallUnityTlsInterfaceDelegate(IntPtr unityTlsInterfaceStruct); } diff --git a/BepInEx.IL2CPP/Logging/IL2CPPLogSource.cs b/BepInEx.IL2CPP/Logging/IL2CPPLogSource.cs index 9983d26..c640f32 100644 --- a/BepInEx.IL2CPP/Logging/IL2CPPLogSource.cs +++ b/BepInEx.IL2CPP/Logging/IL2CPPLogSource.cs @@ -2,25 +2,24 @@ using System.Runtime.InteropServices; using BepInEx.Logging; -namespace BepInEx.IL2CPP.Logging +namespace BepInEx.IL2CPP.Logging; + +public class IL2CPPLogSource : ILogSource { - public class IL2CPPLogSource : ILogSource + public IL2CPPLogSource() { - public IL2CPPLogSource() - { - var loggerPointer = Marshal.GetFunctionPointerForDelegate(new IL2CPPLogCallbackDelegate(IL2CPPLogCallback)); - UnhollowerBaseLib.IL2CPP.il2cpp_register_log_callback(loggerPointer); - } - - public string SourceName { get; } = "IL2CPP"; - public event EventHandler LogEvent; - - public void Dispose() { } - - private void IL2CPPLogCallback(string message) => - LogEvent?.Invoke(this, new LogEventArgs(message.Trim(), LogLevel.Message, this)); - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate void IL2CPPLogCallbackDelegate([In] [MarshalAs(UnmanagedType.LPStr)] string message); + var loggerPointer = Marshal.GetFunctionPointerForDelegate(new IL2CPPLogCallbackDelegate(IL2CPPLogCallback)); + UnhollowerBaseLib.IL2CPP.il2cpp_register_log_callback(loggerPointer); } + + public string SourceName { get; } = "IL2CPP"; + public event EventHandler LogEvent; + + public void Dispose() { } + + private void IL2CPPLogCallback(string message) => + LogEvent?.Invoke(this, new LogEventArgs(message.Trim(), LogLevel.Message, this)); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void IL2CPPLogCallbackDelegate([In] [MarshalAs(UnmanagedType.LPStr)] string message); } diff --git a/BepInEx.IL2CPP/Logging/IL2CPPUnityLogSource.cs b/BepInEx.IL2CPP/Logging/IL2CPPUnityLogSource.cs index b794b8b..7e4b512 100644 --- a/BepInEx.IL2CPP/Logging/IL2CPPUnityLogSource.cs +++ b/BepInEx.IL2CPP/Logging/IL2CPPUnityLogSource.cs @@ -2,37 +2,38 @@ using System; using BepInEx.Logging; using UnityEngine; -namespace BepInEx.IL2CPP.Logging +namespace BepInEx.IL2CPP.Logging; + +public class IL2CPPUnityLogSource : ILogSource { - public class IL2CPPUnityLogSource : ILogSource + private delegate IntPtr SetLogCallbackDefinedDelegate(bool defined); + + public IL2CPPUnityLogSource() { - private delegate IntPtr SetLogCallbackDefinedDelegate(bool defined); + Application.s_LogCallbackHandler = new Action(UnityLogCallback); - public IL2CPPUnityLogSource() + UnhollowerBaseLib.IL2CPP + .ResolveICall< + SetLogCallbackDefinedDelegate>("UnityEngine.Application::SetLogCallbackDefined")(true); + } + + public string SourceName { get; } = "Unity"; + + public event EventHandler LogEvent; + + public void Dispose() { } + + public void UnityLogCallback(string logLine, string exception, LogType type) + { + var level = type switch { - Application.s_LogCallbackHandler = new Action(UnityLogCallback); - - UnhollowerBaseLib.IL2CPP.ResolveICall("UnityEngine.Application::SetLogCallbackDefined")(true); - } - - public string SourceName { get; } = "Unity"; - - public event EventHandler LogEvent; - - public void Dispose() { } - - public void UnityLogCallback(string logLine, string exception, LogType type) - { - var level = type switch - { - LogType.Error => LogLevel.Error, - LogType.Assert => LogLevel.Debug, - LogType.Warning => LogLevel.Warning, - LogType.Log => LogLevel.Message, - LogType.Exception => LogLevel.Error, - _ => LogLevel.Message - }; - LogEvent?.Invoke(this, new LogEventArgs(logLine, level, this)); - } + LogType.Error => LogLevel.Error, + LogType.Assert => LogLevel.Debug, + LogType.Warning => LogLevel.Warning, + LogType.Log => LogLevel.Message, + LogType.Exception => LogLevel.Error, + _ => LogLevel.Message + }; + LogEvent?.Invoke(this, new LogEventArgs(logLine, level, this)); } } diff --git a/BepInEx.IL2CPP/MonoExtensions.cs b/BepInEx.IL2CPP/MonoExtensions.cs index 599ff98..69ba35d 100644 --- a/BepInEx.IL2CPP/MonoExtensions.cs +++ b/BepInEx.IL2CPP/MonoExtensions.cs @@ -2,21 +2,20 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -namespace BepInEx +namespace BepInEx; + +/// +/// Contains unofficial extensions to the underlying Mono runtime. +/// +public static class MonoExtensions { - /// - /// Contains unofficial extensions to the underlying Mono runtime. - /// - public static class MonoExtensions + [MethodImpl(MethodImplOptions.InternalCall)] + private static extern IntPtr GetFunctionPointerForDelegateInternal2(Delegate d, CallingConvention conv); + + public static IntPtr GetFunctionPointerForDelegate(Delegate d, CallingConvention conv) { - [MethodImpl(MethodImplOptions.InternalCall)] - private static extern IntPtr GetFunctionPointerForDelegateInternal2(Delegate d, CallingConvention conv); + if (d == null) throw new ArgumentNullException(nameof(d)); - public static IntPtr GetFunctionPointerForDelegate(Delegate d, CallingConvention conv) - { - if (d == null) throw new ArgumentNullException(nameof(d)); - - return GetFunctionPointerForDelegateInternal2(d, conv); - } + return GetFunctionPointerForDelegateInternal2(d, conv); } } diff --git a/BepInEx.IL2CPP/Preloader.cs b/BepInEx.IL2CPP/Preloader.cs index 279050b..dbd454a 100644 --- a/BepInEx.IL2CPP/Preloader.cs +++ b/BepInEx.IL2CPP/Preloader.cs @@ -9,138 +9,137 @@ using BepInEx.Preloader.Core.Patching; using UnhollowerBaseLib; using UnhollowerBaseLib.Runtime; -namespace BepInEx.IL2CPP +namespace BepInEx.IL2CPP; + +public static class Preloader { - public static class Preloader + #region Config + + private static readonly ConfigEntry ConfigUnityVersion = ConfigFile.CoreConfig.Bind( + "IL2CPP", "UnityVersion", + string.Empty, + "Unity version to report to Il2CppUnhollower. If empty, version is automatically determined from the game process."); + + #endregion + + public static string IL2CPPUnhollowedPath { get; internal set; } + + private static PreloaderConsoleListener PreloaderLog { get; set; } + + internal static ManualLogSource Log => PreloaderLogger.Log; + internal static ManualLogSource UnhollowerLog { get; set; } + + // TODO: This is not needed, maybe remove? (Instance is saved in IL2CPPChainloader itself) + private static IL2CPPChainloader Chainloader { get; set; } + + public static Version UnityVersion { get; private set; } + + public static void Run() { - #region Config - - private static readonly ConfigEntry ConfigUnityVersion = ConfigFile.CoreConfig.Bind( - "IL2CPP", "UnityVersion", - string.Empty, - "Unity version to report to Il2CppUnhollower. If empty, version is automatically determined from the game process."); - - #endregion - - public static string IL2CPPUnhollowedPath { get; internal set; } - - private static PreloaderConsoleListener PreloaderLog { get; set; } - - internal static ManualLogSource Log => PreloaderLogger.Log; - internal static ManualLogSource UnhollowerLog { get; set; } - - // TODO: This is not needed, maybe remove? (Instance is saved in IL2CPPChainloader itself) - private static IL2CPPChainloader Chainloader { get; set; } - - public static Version UnityVersion { get; private set; } - - public static void Run() + try { - try + ConsoleManager.Initialize(false); + + PreloaderLog = new PreloaderConsoleListener(); + Logger.Listeners.Add(PreloaderLog); + + if (ConsoleManager.ConsoleEnabled) { - ConsoleManager.Initialize(false); - - PreloaderLog = new PreloaderConsoleListener(); - Logger.Listeners.Add(PreloaderLog); - - if (ConsoleManager.ConsoleEnabled) - { - ConsoleManager.CreateConsole(); - Logger.Listeners.Add(new ConsoleLogListener()); - } - - ChainloaderLogHelper.PrintLogInfo(Log); - - Log.LogDebug($"Game executable path: {Paths.ExecutablePath}"); - Log.LogDebug($"Unhollowed assembly directory: {IL2CPPUnhollowedPath}"); - Log.LogDebug($"BepInEx root path: {Paths.BepInExRootPath}"); - - UnhollowerLog = Logger.CreateLogSource("Unhollower"); - LogSupport.InfoHandler += UnhollowerLog.LogInfo; - LogSupport.WarningHandler += UnhollowerLog.LogWarning; - LogSupport.TraceHandler += UnhollowerLog.LogDebug; - LogSupport.ErrorHandler += UnhollowerLog.LogError; - - InitializeUnityVersion(); - - if (ProxyAssemblyGenerator.CheckIfGenerationRequired()) - ProxyAssemblyGenerator.GenerateAssemblies(); - - UnityVersionHandler.Initialize(UnityVersion.Major, UnityVersion.Minor, UnityVersion.Build); - - using (var assemblyPatcher = new AssemblyPatcher()) - { - assemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath); - - Log.LogInfo($"{assemblyPatcher.PatcherContext.PatcherPlugins.Count} patcher plugin{(assemblyPatcher.PatcherContext.PatcherPlugins.Count == 1 ? "" : "s")} loaded"); - - assemblyPatcher.LoadAssemblyDirectories(IL2CPPUnhollowedPath); - - Log.LogInfo($"{assemblyPatcher.PatcherContext.PatcherPlugins.Count} assemblies discovered"); - - assemblyPatcher.PatchAndLoad(); - } - - - Logger.Listeners.Remove(PreloaderLog); - - - Chainloader = new IL2CPPChainloader(); - - Chainloader.Initialize(); + ConsoleManager.CreateConsole(); + Logger.Listeners.Add(new ConsoleLogListener()); } - catch (Exception ex) + + ChainloaderLogHelper.PrintLogInfo(Log); + + Log.LogDebug($"Game executable path: {Paths.ExecutablePath}"); + Log.LogDebug($"Unhollowed assembly directory: {IL2CPPUnhollowedPath}"); + Log.LogDebug($"BepInEx root path: {Paths.BepInExRootPath}"); + + UnhollowerLog = Logger.CreateLogSource("Unhollower"); + LogSupport.InfoHandler += UnhollowerLog.LogInfo; + LogSupport.WarningHandler += UnhollowerLog.LogWarning; + LogSupport.TraceHandler += UnhollowerLog.LogDebug; + LogSupport.ErrorHandler += UnhollowerLog.LogError; + + InitializeUnityVersion(); + + if (ProxyAssemblyGenerator.CheckIfGenerationRequired()) + ProxyAssemblyGenerator.GenerateAssemblies(); + + UnityVersionHandler.Initialize(UnityVersion.Major, UnityVersion.Minor, UnityVersion.Build); + + using (var assemblyPatcher = new AssemblyPatcher()) { - Log.LogFatal(ex); + assemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath); - throw; + Log.LogInfo($"{assemblyPatcher.PatcherContext.PatcherPlugins.Count} patcher plugin{(assemblyPatcher.PatcherContext.PatcherPlugins.Count == 1 ? "" : "s")} loaded"); + + assemblyPatcher.LoadAssemblyDirectories(IL2CPPUnhollowedPath); + + Log.LogInfo($"{assemblyPatcher.PatcherContext.PatcherPlugins.Count} assemblies discovered"); + + assemblyPatcher.PatchAndLoad(); } + + + Logger.Listeners.Remove(PreloaderLog); + + + Chainloader = new IL2CPPChainloader(); + + Chainloader.Initialize(); } - - private static void InitializeUnityVersion() + catch (Exception ex) { - if (TryInitializeUnityVersion(ConfigUnityVersion.Value)) - Log.LogWarning($"Unity version obtained from the config."); - else if (TryInitializeUnityVersion(Process.GetCurrentProcess().MainModule.FileVersionInfo.FileVersion)) - Log.LogDebug($"Unity version obtained from main application module."); - else - Log.LogError($"Running under default Unity version. UnityVersionHandler is not initialized."); + Log.LogFatal(ex); + + throw; } + } - private static bool TryInitializeUnityVersion(string version) + private static void InitializeUnityVersion() + { + if (TryInitializeUnityVersion(ConfigUnityVersion.Value)) + Log.LogWarning($"Unity version obtained from the config."); + else if (TryInitializeUnityVersion(Process.GetCurrentProcess().MainModule.FileVersionInfo.FileVersion)) + Log.LogDebug($"Unity version obtained from main application module."); + else + Log.LogError($"Running under default Unity version. UnityVersionHandler is not initialized."); + } + + private static bool TryInitializeUnityVersion(string version) + { + try { - try + if (string.IsNullOrWhiteSpace(version)) + return false; + + var parts = version.Split('.'); + var major = 0; + var minor = 0; + var build = 0; + + // Issue #229 - Don't use Version.Parse("2019.4.16.14703470L&ProductVersion") + var success = int.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out major); + if (success && parts.Length > 1) + success = int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out minor); + if (success && parts.Length > 2) + success = int.TryParse(parts[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out build); + + if (!success) { - if (string.IsNullOrWhiteSpace(version)) - return false; - - var parts = version.Split('.'); - var major = 0; - var minor = 0; - var build = 0; - - // Issue #229 - Don't use Version.Parse("2019.4.16.14703470L&ProductVersion") - bool success = int.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out major); - if (success && parts.Length > 1) - success = int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out minor); - if (success && parts.Length > 2) - success = int.TryParse(parts[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out build); - - if (!success) - { - Log.LogError($"Failed to parse Unity version: {version}"); - return false; - } - - UnityVersion = new Version(major, minor, build); - Log.LogInfo($"Running under Unity v{UnityVersion}"); - return true; - } - catch (Exception ex) - { - Log.LogError($"Failed to parse Unity version: {ex}"); + Log.LogError($"Failed to parse Unity version: {version}"); return false; } + + UnityVersion = new Version(major, minor, build); + Log.LogInfo($"Running under Unity v{UnityVersion}"); + return true; + } + catch (Exception ex) + { + Log.LogError($"Failed to parse Unity version: {ex}"); + return false; } } } diff --git a/BepInEx.IL2CPP/ProxyAssemblyGenerator.cs b/BepInEx.IL2CPP/ProxyAssemblyGenerator.cs index 129abfe..18a1371 100644 --- a/BepInEx.IL2CPP/ProxyAssemblyGenerator.cs +++ b/BepInEx.IL2CPP/ProxyAssemblyGenerator.cs @@ -16,355 +16,352 @@ using UnhollowerBaseLib; using AssemblyUnhollowerRunner = AssemblyUnhollower.Program; using Logger = BepInEx.Logging.Logger; -namespace BepInEx.IL2CPP +namespace BepInEx.IL2CPP; + +internal static class ProxyAssemblyGenerator { - internal static class ProxyAssemblyGenerator + private static readonly ConfigEntry ConfigUpdateUnhollowedAssemblies = + ConfigFile.CoreConfig.Bind("IL2CPP", + "UpdateUnhollowedAssemblies", + true, + new StringBuilder() + .AppendLine("Whether to run Il2CppAssemblyUnhollower automatically to generate Il2Cpp support assemblies when they are outdated.") + .AppendLine("If disabled assemblies in `BepInEx/unhollowed` won't be updated between game or BepInEx updates!") + .ToString()); + + private static readonly ConfigEntry ConfigUnityBaseLibrariesSource = ConfigFile.CoreConfig.Bind( + "IL2CPP", "UnityBaseLibrariesSource", + "http://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 Il2CppUnhollower to generate unhollowed Unity assemblies") + .AppendLine("The URL template MUST use HTTP.") + .AppendLine("The URL can include {VERSION} template which will be replaced with the game's Unity engine version") + .ToString()); + + private static readonly ConfigEntry ConfigIl2CppDumperType = ConfigFile.CoreConfig.Bind( + "IL2CPP", "Il2CppDumperType", + IL2CPPDumperType.Il2CppDumper, + new StringBuilder() + .AppendLine("The IL2CPP metadata dumper tool to use when generating dummy assemblies for Il2CppAssemblyUnhollower.") + .AppendLine("Il2CppDumper - Default. The traditional choice that has been used by BepInEx.") + .AppendLine("Cpp2IL - Experimental, may provide better results than Il2CppDumper. Required for use with BepInEx.MelonLoader.Loader.") + .ToString()); + + private static readonly ConfigEntry ConfigDumpDummyAssemblies = ConfigFile.CoreConfig.Bind( + "IL2CPP", "DumpDummyAssemblies", + false, + "If enabled, BepInEx will save dummy assemblies generated by an il2cpp dumper into BepInEx/dummy."); + + private static ManualLogSource Il2cppDumperLogger = null; + + public static string GameAssemblyPath => Path.Combine(Paths.GameRootPath, "GameAssembly.dll"); + + private static string HashPath => Path.Combine(Preloader.IL2CPPUnhollowedPath, "assembly-hash.txt"); + + private static string UnityBaseLibsDirectory => Path.Combine(Paths.BepInExRootPath, "unity-libs"); + + private static string ComputeHash() { - private static readonly ConfigEntry ConfigUpdateUnhollowedAssemblies = - ConfigFile.CoreConfig.Bind("IL2CPP", - "UpdateUnhollowedAssemblies", - true, - new StringBuilder() - .AppendLine("Whether to run Il2CppAssemblyUnhollower automatically to generate Il2Cpp support assemblies when they are outdated.") - .AppendLine("If disabled assemblies in `BepInEx/unhollowed` won't be updated between game or BepInEx updates!") - .ToString()); + using var md5 = MD5.Create(); - private static readonly ConfigEntry ConfigUnityBaseLibrariesSource = ConfigFile.CoreConfig.Bind( - "IL2CPP", "UnityBaseLibrariesSource", - "http://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 Il2CppUnhollower to generate unhollowed Unity assemblies") - .AppendLine("The URL template MUST use HTTP.") - .AppendLine("The URL can include {VERSION} template which will be replaced with the game's Unity engine version") - .ToString()); - - private static readonly ConfigEntry ConfigIl2CppDumperType = ConfigFile.CoreConfig.Bind( - "IL2CPP", "Il2CppDumperType", - IL2CPPDumperType.Il2CppDumper, - new StringBuilder() - .AppendLine("The IL2CPP metadata dumper tool to use when generating dummy assemblies for Il2CppAssemblyUnhollower.") - .AppendLine("Il2CppDumper - Default. The traditional choice that has been used by BepInEx.") - .AppendLine("Cpp2IL - Experimental, may provide better results than Il2CppDumper. Required for use with BepInEx.MelonLoader.Loader.") - .ToString()); - - private static readonly ConfigEntry ConfigDumpDummyAssemblies = ConfigFile.CoreConfig.Bind( - "IL2CPP", "DumpDummyAssemblies", - false, - "If enabled, BepInEx will save dummy assemblies generated by an il2cpp dumper into BepInEx/dummy."); - - private static ManualLogSource Il2cppDumperLogger = null; - - public static string GameAssemblyPath => Path.Combine(Paths.GameRootPath, "GameAssembly.dll"); - - private static string HashPath => Path.Combine(Preloader.IL2CPPUnhollowedPath, "assembly-hash.txt"); - - private static string UnityBaseLibsDirectory => Path.Combine(Paths.BepInExRootPath, "unity-libs"); - - private static string ComputeHash() + static void HashFile(ICryptoTransform hash, string file) { - using var md5 = MD5.Create(); - - static void HashFile(ICryptoTransform hash, string file) - { - const int defaultCopyBufferSize = 81920; - using var fs = File.OpenRead(file); - var buffer = new byte[defaultCopyBufferSize]; - int read; - while ((read = fs.Read(buffer)) > 0) - hash.TransformBlock(buffer, 0, read, buffer, 0); - } - - static void HashString(ICryptoTransform hash, string str) - { - var buffer = Encoding.UTF8.GetBytes(str); - hash.TransformBlock(buffer, 0, buffer.Length, buffer, 0); - } - - HashFile(md5, GameAssemblyPath); - - if (Directory.Exists(UnityBaseLibsDirectory)) - foreach (var file in Directory.EnumerateFiles(UnityBaseLibsDirectory, "*.dll", - SearchOption.TopDirectoryOnly)) - { - HashString(md5, Path.GetFileName(file)); - HashFile(md5, file); - } - - // Hash some common dependencies as they can affect output - HashString(md5, typeof(AssemblyUnhollowerRunner).Assembly.GetName().Version.ToString()); - HashString(md5, typeof(Cpp2IlApi).Assembly.GetName().Version.ToString()); - HashString(md5, typeof(Il2CppDumper.Il2CppDumper).Assembly.GetName().Version.ToString()); - - md5.TransformFinalBlock(new byte[0], 0, 0); - - return Utility.ByteArrayToString(md5.Hash); + const int defaultCopyBufferSize = 81920; + using var fs = File.OpenRead(file); + var buffer = new byte[defaultCopyBufferSize]; + int read; + while ((read = fs.Read(buffer)) > 0) + hash.TransformBlock(buffer, 0, read, buffer, 0); } - public static bool CheckIfGenerationRequired() + static void HashString(ICryptoTransform hash, string str) { - static bool NeedGenerationOrSkip() - { - if (!ConfigUpdateUnhollowedAssemblies.Value) - { - var hash = ComputeHash(); - Preloader.Log - .LogWarning($"Unhollowed assemblies are possibly out of date. To disable this message, create file {HashPath} with the following contents: {hash}"); - return false; - } - - return true; - } - - if (!Directory.Exists(Preloader.IL2CPPUnhollowedPath)) - return true; - - if (!File.Exists(HashPath)) - return NeedGenerationOrSkip(); - - if (ComputeHash() != File.ReadAllText(HashPath) && NeedGenerationOrSkip()) - { - Preloader.Log.LogInfo("Detected outdated proxy assemblies, will regenerate them now"); - return true; - } - - return false; + var buffer = Encoding.UTF8.GetBytes(str); + hash.TransformBlock(buffer, 0, buffer.Length, buffer, 0); } - public static void GenerateAssemblies() - { - Il2cppDumperLogger ??= Logger.CreateLogSource(ConfigIl2CppDumperType.Value == IL2CPPDumperType.Il2CppDumper - ? "Il2CppDumper" - : "Cpp2IL"); + HashFile(md5, GameAssemblyPath); - var domain = AppDomainHelper.CreateDomain("GeneratorDomain", new AppDomainHelper.AppDomainSetup + if (Directory.Exists(UnityBaseLibsDirectory)) + foreach (var file in Directory.EnumerateFiles(UnityBaseLibsDirectory, "*.dll", + SearchOption.TopDirectoryOnly)) { - ApplicationBase = Paths.BepInExAssemblyDirectory - }); + HashString(md5, Path.GetFileName(file)); + HashFile(md5, file); + } - var runner = - (AppDomainRunner)AppDomainHelper.CreateInstanceAndUnwrap(domain, - typeof(AppDomainRunner).Assembly.FullName, - typeof(AppDomainRunner).FullName); + // Hash some common dependencies as they can affect output + HashString(md5, typeof(AssemblyUnhollowerRunner).Assembly.GetName().Version.ToString()); + HashString(md5, typeof(Cpp2IlApi).Assembly.GetName().Version.ToString()); + HashString(md5, typeof(Il2CppDumper.Il2CppDumper).Assembly.GetName().Version.ToString()); - runner.Setup(Paths.ExecutablePath, Preloader.IL2CPPUnhollowedPath, Paths.BepInExRootPath, - Paths.ManagedPath); - runner.GenerateAssembliesInternal(new AppDomainListener(), Preloader.UnityVersion.ToString(3), - ConfigIl2CppDumperType.Value); + md5.TransformFinalBlock(new byte[0], 0, 0); - AppDomain.Unload(domain); + return Utility.ByteArrayToString(md5.Hash); + } - File.WriteAllText(HashPath, ComputeHash()); + public static bool CheckIfGenerationRequired() + { + static bool NeedGenerationOrSkip() + { + if (!ConfigUpdateUnhollowedAssemblies.Value) + { + var hash = ComputeHash(); + Preloader.Log + .LogWarning($"Unhollowed assemblies are possibly out of date. To disable this message, create file {HashPath} with the following contents: {hash}"); + return false; + } + + return true; } - internal enum IL2CPPDumperType + if (!Directory.Exists(Preloader.IL2CPPUnhollowedPath)) + return true; + + if (!File.Exists(HashPath)) + return NeedGenerationOrSkip(); + + if (ComputeHash() != File.ReadAllText(HashPath) && NeedGenerationOrSkip()) { - Il2CppDumper, - Cpp2IL + Preloader.Log.LogInfo("Detected outdated proxy assemblies, will regenerate them now"); + return true; } - private static class AppDomainHelper + return false; + } + + public static void GenerateAssemblies() + { + Il2cppDumperLogger ??= Logger.CreateLogSource(ConfigIl2CppDumperType.Value == IL2CPPDumperType.Il2CppDumper + ? "Il2CppDumper" + : "Cpp2IL"); + + var domain = AppDomainHelper.CreateDomain("GeneratorDomain", new AppDomainHelper.AppDomainSetup { - static AppDomainHelper() - { - var appDomain = typeof(AppDomain); - var evidenceType = typeof(AppDomain).Assembly.GetType("System.Security.Policy.Evidence"); - AppDomainSetupType = typeof(AppDomain).Assembly.GetType("System.AppDomainSetup"); - CreateDomainInternal = - MethodInvoker.GetHandler(AccessTools.Method(appDomain, "CreateDomain", - new[] - { - typeof(string), evidenceType, AppDomainSetupType - })); - CreateInstanceAndUnwrap = - AccessTools.MethodDelegate>(AccessTools.Method(appDomain, - nameof(CreateInstanceAndUnwrap), new[] { typeof(string), typeof(string) })); - } + ApplicationBase = Paths.BepInExAssemblyDirectory + }); - public static Func CreateInstanceAndUnwrap { get; } - private static FastInvokeHandler CreateDomainInternal { get; } - private static Type AppDomainSetupType { get; } + var runner = + (AppDomainRunner) AppDomainHelper.CreateInstanceAndUnwrap(domain, + typeof(AppDomainRunner).Assembly.FullName, + typeof(AppDomainRunner).FullName); - public static AppDomain CreateDomain(string name, AppDomainSetup setup) - { - var realSetup = AccessTools.CreateInstance(AppDomainSetupType); - Traverse.IterateProperties(setup, realSetup, (pSrc, pTgt) => pTgt.SetValue(pSrc.GetValue())); - return CreateDomainInternal(null, name, null, realSetup) as AppDomain; - } + runner.Setup(Paths.ExecutablePath, Preloader.IL2CPPUnhollowedPath, Paths.BepInExRootPath, + Paths.ManagedPath); + runner.GenerateAssembliesInternal(new AppDomainListener(), Preloader.UnityVersion.ToString(3), + ConfigIl2CppDumperType.Value); - public class AppDomainSetup - { - public string ApplicationBase { get; set; } - } + AppDomain.Unload(domain); + + File.WriteAllText(HashPath, ComputeHash()); + } + + internal enum IL2CPPDumperType + { + Il2CppDumper, + Cpp2IL + } + + private static class AppDomainHelper + { + static AppDomainHelper() + { + var appDomain = typeof(AppDomain); + var evidenceType = typeof(AppDomain).Assembly.GetType("System.Security.Policy.Evidence"); + AppDomainSetupType = typeof(AppDomain).Assembly.GetType("System.AppDomainSetup"); + CreateDomainInternal = + MethodInvoker.GetHandler(AccessTools.Method(appDomain, "CreateDomain", + new[] + { + typeof(string), evidenceType, AppDomainSetupType + })); + CreateInstanceAndUnwrap = + AccessTools.MethodDelegate>(AccessTools.Method(appDomain, + nameof(CreateInstanceAndUnwrap), new[] { typeof(string), typeof(string) })); } - [Serializable] - private class AppDomainListener : MarshalByRefObject + public static Func CreateInstanceAndUnwrap { get; } + private static FastInvokeHandler CreateDomainInternal { get; } + private static Type AppDomainSetupType { get; } + + public static AppDomain CreateDomain(string name, AppDomainSetup setup) { - public void DoPreloaderLog(object data, LogLevel level) => Preloader.Log.Log(level, data); - - public void DoDumperLog(object data, LogLevel level) => Il2cppDumperLogger.Log(level, data); - - public void DoUnhollowerLog(object data, LogLevel level) => Preloader.UnhollowerLog.Log(level, data); + var realSetup = AccessTools.CreateInstance(AppDomainSetupType); + Traverse.IterateProperties(setup, realSetup, (pSrc, pTgt) => pTgt.SetValue(pSrc.GetValue())); + return CreateDomainInternal(null, name, null, realSetup) as AppDomain; } - [Serializable] - private class AppDomainRunner : MarshalByRefObject + public class AppDomainSetup { - public void Setup(string executablePath, string unhollowedPath, string bepinPath, string managedPath) + public string ApplicationBase { get; set; } + } + } + + [Serializable] + private class AppDomainListener : MarshalByRefObject + { + public void DoPreloaderLog(object data, LogLevel level) => Preloader.Log.Log(level, data); + + public void DoDumperLog(object data, LogLevel level) => Il2cppDumperLogger.Log(level, data); + + public void DoUnhollowerLog(object data, LogLevel level) => Preloader.UnhollowerLog.Log(level, data); + } + + [Serializable] + private class AppDomainRunner : MarshalByRefObject + { + public void Setup(string executablePath, string unhollowedPath, string bepinPath, string managedPath) + { + Paths.SetExecutablePath(executablePath, bepinPath, managedPath); + Preloader.IL2CPPUnhollowedPath = unhollowedPath; + AppDomain.CurrentDomain.AddCecilPlatformAssemblies(Paths.ManagedPath); + AppDomain.CurrentDomain.AddCecilPlatformAssemblies(UnityBaseLibsDirectory); + } + + public void GenerateAssembliesInternal(AppDomainListener listener, + string unityVersion, + IL2CPPDumperType dumperType) + { + var source = + ConfigUnityBaseLibrariesSource.Value.Replace("{VERSION}", unityVersion); + + if (!string.IsNullOrEmpty(source)) { - Paths.SetExecutablePath(executablePath, bepinPath, managedPath); - Preloader.IL2CPPUnhollowedPath = unhollowedPath; - AppDomain.CurrentDomain.AddCecilPlatformAssemblies(Paths.ManagedPath); - AppDomain.CurrentDomain.AddCecilPlatformAssemblies(UnityBaseLibsDirectory); + listener.DoPreloaderLog("Downloading unity base libraries", LogLevel.Message); + + Directory.CreateDirectory(UnityBaseLibsDirectory); + Directory.EnumerateFiles(UnityBaseLibsDirectory, "*.dll").Do(File.Delete); + + using var httpClient = new HttpClient(); + using var zipStream = httpClient.GetStreamAsync(source).GetAwaiter().GetResult(); + using var zipArchive = new ZipArchive(zipStream, ZipArchiveMode.Read); + + listener.DoPreloaderLog("Extracting downloaded unity base libraries", LogLevel.Message); + zipArchive.ExtractToDirectory(UnityBaseLibsDirectory); } - public void GenerateAssembliesInternal(AppDomainListener listener, - string unityVersion, - IL2CPPDumperType dumperType) + listener.DoPreloaderLog("Generating Il2CppUnhollower assemblies", LogLevel.Message); + + Directory.CreateDirectory(Preloader.IL2CPPUnhollowedPath); + Directory.EnumerateFiles(Preloader.IL2CPPUnhollowedPath, "*.dll").Do(File.Delete); + + var metadataPath = Path.Combine(Paths.GameRootPath, + $"{Paths.ProcessName}_Data", + "il2cpp_data", + "Metadata", + "global-metadata.dat"); + + List sourceAssemblies; + + var stopwatch = new System.Diagnostics.Stopwatch(); + stopwatch.Start(); + + if (dumperType == IL2CPPDumperType.Il2CppDumper) { - var source = - ConfigUnityBaseLibrariesSource.Value.Replace("{VERSION}", unityVersion); + listener.DoPreloaderLog("Generating Il2CppDumper intermediate assemblies", LogLevel.Info); - if (!string.IsNullOrEmpty(source)) - { - listener.DoPreloaderLog("Downloading unity base libraries", LogLevel.Message); + Il2CppDumper.Il2CppDumper.Init(GameAssemblyPath, + metadataPath, + new Config + { + GenerateStruct = false, + GenerateDummyDll = true + }, + s => listener.DoDumperLog(s, LogLevel.Debug), + out var metadata, + out var il2Cpp); - Directory.CreateDirectory(UnityBaseLibsDirectory); - Directory.EnumerateFiles(UnityBaseLibsDirectory, "*.dll").Do(File.Delete); - - using var httpClient = new HttpClient(); - using var zipStream = httpClient.GetStreamAsync(source).GetAwaiter().GetResult(); - using var zipArchive = new ZipArchive(zipStream, ZipArchiveMode.Read); - - listener.DoPreloaderLog("Extracting downloaded unity base libraries", LogLevel.Message); - zipArchive.ExtractToDirectory(UnityBaseLibsDirectory); - } - - listener.DoPreloaderLog("Generating Il2CppUnhollower assemblies", LogLevel.Message); - - Directory.CreateDirectory(Preloader.IL2CPPUnhollowedPath); - Directory.EnumerateFiles(Preloader.IL2CPPUnhollowedPath, "*.dll").Do(File.Delete); - - string metadataPath = Path.Combine(Paths.GameRootPath, - $"{Paths.ProcessName}_Data", - "il2cpp_data", - "Metadata", - "global-metadata.dat"); - - List sourceAssemblies; - - var stopwatch = new System.Diagnostics.Stopwatch(); - stopwatch.Start(); - - if (dumperType == IL2CPPDumperType.Il2CppDumper) - { - listener.DoPreloaderLog("Generating Il2CppDumper intermediate assemblies", LogLevel.Info); - - Il2CppDumper.Il2CppDumper.Init(GameAssemblyPath, - metadataPath, - new Config - { - GenerateStruct = false, - GenerateDummyDll = true - }, - s => listener.DoDumperLog(s, LogLevel.Debug), - out var metadata, - out var il2Cpp); - - var executor = new Il2CppExecutor(metadata, il2Cpp); - var dummy = new DummyAssemblyGenerator(executor, true); - sourceAssemblies = dummy.Assemblies; - } - else // if (dumperType == IL2CPPDumperType.Cpp2IL) - { - Cpp2IL.Core.Logger.VerboseLog += (message, s) => - listener.DoDumperLog($"[{s}] {message.Trim()}", LogLevel.Debug); - Cpp2IL.Core.Logger.InfoLog += (message, s) => - listener.DoDumperLog($"[{s}] {message.Trim()}", LogLevel.Info); - Cpp2IL.Core.Logger.WarningLog += (message, s) => - listener.DoDumperLog($"[{s}] {message.Trim()}", LogLevel.Warning); - Cpp2IL.Core.Logger.ErrorLog += (message, s) => - listener.DoDumperLog($"[{s}] {message.Trim()}", LogLevel.Error); - - var cpp2IlUnityVersion = - Cpp2IlApi.DetermineUnityVersion(Paths.ExecutablePath, - Path.Combine(Paths.GameRootPath, $"{Paths.ProcessName}_Data")); - - Cpp2IlApi.InitializeLibCpp2Il(GameAssemblyPath, metadataPath, cpp2IlUnityVersion, false); - - sourceAssemblies = Cpp2IlApi.MakeDummyDLLs(); - } - - stopwatch.Stop(); - listener.DoDumperLog("Total time: " + stopwatch.Elapsed, LogLevel.Info); - - if (ConfigDumpDummyAssemblies.Value) - { - var dummyPath = Path.Combine(Paths.BepInExRootPath, "dummy"); - Directory.CreateDirectory(dummyPath); - - foreach (var assemblyDefinition in sourceAssemblies) - { - assemblyDefinition.Write(Path.Combine(dummyPath, assemblyDefinition.Name.Name + ".dll")); - } - } - - var unhollowerOptions = new UnhollowerOptions - { - GameAssemblyPath = GameAssemblyPath, - MscorlibPath = Path.Combine(Paths.ManagedPath, "mscorlib.dll"), - Source = sourceAssemblies, - OutputDir = Preloader.IL2CPPUnhollowedPath, - UnityBaseLibsDir = Directory.Exists(UnityBaseLibsDirectory) ? UnityBaseLibsDirectory : null, - NoCopyUnhollowerLibs = true - }; - - string renameMapLocation = Path.Combine(Paths.BepInExRootPath, "DeobfuscationMap.csv.gz"); - if (File.Exists(renameMapLocation)) - { - listener.DoPreloaderLog("Parsing deobfuscation rename mappings", LogLevel.Info); - - using var fileStream = - new FileStream(renameMapLocation, FileMode.Open, FileAccess.Read, FileShare.Read); - using var gzipStream = new GZipStream(fileStream, CompressionMode.Decompress); - - using var reader = new StreamReader(gzipStream, Encoding.UTF8, false); - while (!reader.EndOfStream) - { - var line = reader.ReadLine(); - - if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) - continue; - - var mapping = line.Split(';'); - - if (mapping.Length != 2) - continue; - - unhollowerOptions.RenameMap[mapping[0]] = mapping[1]; - } - } - - - listener.DoPreloaderLog("Executing Il2CppUnhollower generator", LogLevel.Info); - - LogSupport.InfoHandler += s => listener.DoUnhollowerLog(s.Trim(), LogLevel.Info); - LogSupport.WarningHandler += s => listener.DoUnhollowerLog(s.Trim(), LogLevel.Warning); - LogSupport.TraceHandler += s => listener.DoUnhollowerLog(s.Trim(), LogLevel.Debug); - LogSupport.ErrorHandler += s => listener.DoUnhollowerLog(s.Trim(), LogLevel.Error); - - try - { - AssemblyUnhollowerRunner.Main(unhollowerOptions); - } - catch (Exception e) - { - listener.DoUnhollowerLog($"Exception while unhollowing: {e}", LogLevel.Error); - } - - sourceAssemblies.Do(x => x.Dispose()); + var executor = new Il2CppExecutor(metadata, il2Cpp); + var dummy = new DummyAssemblyGenerator(executor, true); + sourceAssemblies = dummy.Assemblies; } + else // if (dumperType == IL2CPPDumperType.Cpp2IL) + { + Cpp2IL.Core.Logger.VerboseLog += (message, s) => + listener.DoDumperLog($"[{s}] {message.Trim()}", LogLevel.Debug); + Cpp2IL.Core.Logger.InfoLog += (message, s) => + listener.DoDumperLog($"[{s}] {message.Trim()}", LogLevel.Info); + Cpp2IL.Core.Logger.WarningLog += (message, s) => + listener.DoDumperLog($"[{s}] {message.Trim()}", LogLevel.Warning); + Cpp2IL.Core.Logger.ErrorLog += (message, s) => + listener.DoDumperLog($"[{s}] {message.Trim()}", LogLevel.Error); + + var cpp2IlUnityVersion = + Cpp2IlApi.DetermineUnityVersion(Paths.ExecutablePath, + Path.Combine(Paths.GameRootPath, $"{Paths.ProcessName}_Data")); + + Cpp2IlApi.InitializeLibCpp2Il(GameAssemblyPath, metadataPath, cpp2IlUnityVersion, false); + + sourceAssemblies = Cpp2IlApi.MakeDummyDLLs(); + } + + stopwatch.Stop(); + listener.DoDumperLog("Total time: " + stopwatch.Elapsed, LogLevel.Info); + + if (ConfigDumpDummyAssemblies.Value) + { + var dummyPath = Path.Combine(Paths.BepInExRootPath, "dummy"); + Directory.CreateDirectory(dummyPath); + + foreach (var assemblyDefinition in sourceAssemblies) + assemblyDefinition.Write(Path.Combine(dummyPath, assemblyDefinition.Name.Name + ".dll")); + } + + var unhollowerOptions = new UnhollowerOptions + { + GameAssemblyPath = GameAssemblyPath, + MscorlibPath = Path.Combine(Paths.ManagedPath, "mscorlib.dll"), + Source = sourceAssemblies, + OutputDir = Preloader.IL2CPPUnhollowedPath, + UnityBaseLibsDir = Directory.Exists(UnityBaseLibsDirectory) ? UnityBaseLibsDirectory : null, + NoCopyUnhollowerLibs = true + }; + + var renameMapLocation = Path.Combine(Paths.BepInExRootPath, "DeobfuscationMap.csv.gz"); + if (File.Exists(renameMapLocation)) + { + listener.DoPreloaderLog("Parsing deobfuscation rename mappings", LogLevel.Info); + + using var fileStream = + new FileStream(renameMapLocation, FileMode.Open, FileAccess.Read, FileShare.Read); + using var gzipStream = new GZipStream(fileStream, CompressionMode.Decompress); + + using var reader = new StreamReader(gzipStream, Encoding.UTF8, false); + while (!reader.EndOfStream) + { + var line = reader.ReadLine(); + + if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) + continue; + + var mapping = line.Split(';'); + + if (mapping.Length != 2) + continue; + + unhollowerOptions.RenameMap[mapping[0]] = mapping[1]; + } + } + + + listener.DoPreloaderLog("Executing Il2CppUnhollower generator", LogLevel.Info); + + LogSupport.InfoHandler += s => listener.DoUnhollowerLog(s.Trim(), LogLevel.Info); + LogSupport.WarningHandler += s => listener.DoUnhollowerLog(s.Trim(), LogLevel.Warning); + LogSupport.TraceHandler += s => listener.DoUnhollowerLog(s.Trim(), LogLevel.Debug); + LogSupport.ErrorHandler += s => listener.DoUnhollowerLog(s.Trim(), LogLevel.Error); + + try + { + AssemblyUnhollowerRunner.Main(unhollowerOptions); + } + catch (Exception e) + { + listener.DoUnhollowerLog($"Exception while unhollowing: {e}", LogLevel.Error); + } + + sourceAssemblies.Do(x => x.Dispose()); } } } diff --git a/BepInEx.IL2CPP/UnityEngine/Input.cs b/BepInEx.IL2CPP/UnityEngine/Input.cs index 1e779bd..22edfb1 100644 --- a/BepInEx.IL2CPP/UnityEngine/Input.cs +++ b/BepInEx.IL2CPP/UnityEngine/Input.cs @@ -1,343 +1,342 @@ -namespace BepInEx.IL2CPP.UnityEngine +namespace BepInEx.IL2CPP.UnityEngine; + +public enum KeyCode { - public enum KeyCode - { - None, - Backspace = 8, - Delete = 127, - Tab = 9, - Clear = 12, - Return, - Pause = 19, - Escape = 27, - Space = 32, - Keypad0 = 256, - Keypad1, - Keypad2, - Keypad3, - Keypad4, - Keypad5, - Keypad6, - Keypad7, - Keypad8, - Keypad9, - KeypadPeriod, - KeypadDivide, - KeypadMultiply, - KeypadMinus, - KeypadPlus, - KeypadEnter, - KeypadEquals, - UpArrow, - DownArrow, - RightArrow, - LeftArrow, - Insert, - Home, - End, - PageUp, - PageDown, - F1, - F2, - F3, - F4, - F5, - F6, - F7, - F8, - F9, - F10, - F11, - F12, - F13, - F14, - F15, - Alpha0 = 48, - Alpha1, - Alpha2, - Alpha3, - Alpha4, - Alpha5, - Alpha6, - Alpha7, - Alpha8, - Alpha9, - Exclaim = 33, - DoubleQuote, - Hash, - Dollar, - Percent, - Ampersand, - Quote, - LeftParen, - RightParen, - Asterisk, - Plus, - Comma, - Minus, - Period, - Slash, - Colon = 58, - Semicolon, - Less, - Equals, - Greater, - Question, - At, - LeftBracket = 91, - Backslash, - RightBracket, - Caret, - Underscore, - BackQuote, - A, - B, - C, - D, - E, - F, - G, - H, - I, - J, - K, - L, - M, - N, - O, - P, - Q, - R, - S, - T, - U, - V, - W, - X, - Y, - Z, - LeftCurlyBracket, - Pipe, - RightCurlyBracket, - Tilde, - Numlock = 300, - CapsLock, - ScrollLock, - RightShift, - LeftShift, - RightControl, - LeftControl, - RightAlt, - LeftAlt, - LeftCommand = 310, - LeftApple = 310, - LeftWindows, - RightCommand = 309, - RightApple = 309, - RightWindows = 312, - AltGr, - Help = 315, - Print, - SysReq, - Break, - Menu, - Mouse0 = 323, - Mouse1, - Mouse2, - Mouse3, - Mouse4, - Mouse5, - Mouse6, - JoystickButton0, - JoystickButton1, - JoystickButton2, - JoystickButton3, - JoystickButton4, - JoystickButton5, - JoystickButton6, - JoystickButton7, - JoystickButton8, - JoystickButton9, - JoystickButton10, - JoystickButton11, - JoystickButton12, - JoystickButton13, - JoystickButton14, - JoystickButton15, - JoystickButton16, - JoystickButton17, - JoystickButton18, - JoystickButton19, - Joystick1Button0, - Joystick1Button1, - Joystick1Button2, - Joystick1Button3, - Joystick1Button4, - Joystick1Button5, - Joystick1Button6, - Joystick1Button7, - Joystick1Button8, - Joystick1Button9, - Joystick1Button10, - Joystick1Button11, - Joystick1Button12, - Joystick1Button13, - Joystick1Button14, - Joystick1Button15, - Joystick1Button16, - Joystick1Button17, - Joystick1Button18, - Joystick1Button19, - Joystick2Button0, - Joystick2Button1, - Joystick2Button2, - Joystick2Button3, - Joystick2Button4, - Joystick2Button5, - Joystick2Button6, - Joystick2Button7, - Joystick2Button8, - Joystick2Button9, - Joystick2Button10, - Joystick2Button11, - Joystick2Button12, - Joystick2Button13, - Joystick2Button14, - Joystick2Button15, - Joystick2Button16, - Joystick2Button17, - Joystick2Button18, - Joystick2Button19, - Joystick3Button0, - Joystick3Button1, - Joystick3Button2, - Joystick3Button3, - Joystick3Button4, - Joystick3Button5, - Joystick3Button6, - Joystick3Button7, - Joystick3Button8, - Joystick3Button9, - Joystick3Button10, - Joystick3Button11, - Joystick3Button12, - Joystick3Button13, - Joystick3Button14, - Joystick3Button15, - Joystick3Button16, - Joystick3Button17, - Joystick3Button18, - Joystick3Button19, - Joystick4Button0, - Joystick4Button1, - Joystick4Button2, - Joystick4Button3, - Joystick4Button4, - Joystick4Button5, - Joystick4Button6, - Joystick4Button7, - Joystick4Button8, - Joystick4Button9, - Joystick4Button10, - Joystick4Button11, - Joystick4Button12, - Joystick4Button13, - Joystick4Button14, - Joystick4Button15, - Joystick4Button16, - Joystick4Button17, - Joystick4Button18, - Joystick4Button19, - Joystick5Button0, - Joystick5Button1, - Joystick5Button2, - Joystick5Button3, - Joystick5Button4, - Joystick5Button5, - Joystick5Button6, - Joystick5Button7, - Joystick5Button8, - Joystick5Button9, - Joystick5Button10, - Joystick5Button11, - Joystick5Button12, - Joystick5Button13, - Joystick5Button14, - Joystick5Button15, - Joystick5Button16, - Joystick5Button17, - Joystick5Button18, - Joystick5Button19, - Joystick6Button0, - Joystick6Button1, - Joystick6Button2, - Joystick6Button3, - Joystick6Button4, - Joystick6Button5, - Joystick6Button6, - Joystick6Button7, - Joystick6Button8, - Joystick6Button9, - Joystick6Button10, - Joystick6Button11, - Joystick6Button12, - Joystick6Button13, - Joystick6Button14, - Joystick6Button15, - Joystick6Button16, - Joystick6Button17, - Joystick6Button18, - Joystick6Button19, - Joystick7Button0, - Joystick7Button1, - Joystick7Button2, - Joystick7Button3, - Joystick7Button4, - Joystick7Button5, - Joystick7Button6, - Joystick7Button7, - Joystick7Button8, - Joystick7Button9, - Joystick7Button10, - Joystick7Button11, - Joystick7Button12, - Joystick7Button13, - Joystick7Button14, - Joystick7Button15, - Joystick7Button16, - Joystick7Button17, - Joystick7Button18, - Joystick7Button19, - Joystick8Button0, - Joystick8Button1, - Joystick8Button2, - Joystick8Button3, - Joystick8Button4, - Joystick8Button5, - Joystick8Button6, - Joystick8Button7, - Joystick8Button8, - Joystick8Button9, - Joystick8Button10, - Joystick8Button11, - Joystick8Button12, - Joystick8Button13, - Joystick8Button14, - Joystick8Button15, - Joystick8Button16, - Joystick8Button17, - Joystick8Button18, - Joystick8Button19 - } + None, + Backspace = 8, + Delete = 127, + Tab = 9, + Clear = 12, + Return, + Pause = 19, + Escape = 27, + Space = 32, + Keypad0 = 256, + Keypad1, + Keypad2, + Keypad3, + Keypad4, + Keypad5, + Keypad6, + Keypad7, + Keypad8, + Keypad9, + KeypadPeriod, + KeypadDivide, + KeypadMultiply, + KeypadMinus, + KeypadPlus, + KeypadEnter, + KeypadEquals, + UpArrow, + DownArrow, + RightArrow, + LeftArrow, + Insert, + Home, + End, + PageUp, + PageDown, + F1, + F2, + F3, + F4, + F5, + F6, + F7, + F8, + F9, + F10, + F11, + F12, + F13, + F14, + F15, + Alpha0 = 48, + Alpha1, + Alpha2, + Alpha3, + Alpha4, + Alpha5, + Alpha6, + Alpha7, + Alpha8, + Alpha9, + Exclaim = 33, + DoubleQuote, + Hash, + Dollar, + Percent, + Ampersand, + Quote, + LeftParen, + RightParen, + Asterisk, + Plus, + Comma, + Minus, + Period, + Slash, + Colon = 58, + Semicolon, + Less, + Equals, + Greater, + Question, + At, + LeftBracket = 91, + Backslash, + RightBracket, + Caret, + Underscore, + BackQuote, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q, + R, + S, + T, + U, + V, + W, + X, + Y, + Z, + LeftCurlyBracket, + Pipe, + RightCurlyBracket, + Tilde, + Numlock = 300, + CapsLock, + ScrollLock, + RightShift, + LeftShift, + RightControl, + LeftControl, + RightAlt, + LeftAlt, + LeftCommand = 310, + LeftApple = 310, + LeftWindows, + RightCommand = 309, + RightApple = 309, + RightWindows = 312, + AltGr, + Help = 315, + Print, + SysReq, + Break, + Menu, + Mouse0 = 323, + Mouse1, + Mouse2, + Mouse3, + Mouse4, + Mouse5, + Mouse6, + JoystickButton0, + JoystickButton1, + JoystickButton2, + JoystickButton3, + JoystickButton4, + JoystickButton5, + JoystickButton6, + JoystickButton7, + JoystickButton8, + JoystickButton9, + JoystickButton10, + JoystickButton11, + JoystickButton12, + JoystickButton13, + JoystickButton14, + JoystickButton15, + JoystickButton16, + JoystickButton17, + JoystickButton18, + JoystickButton19, + Joystick1Button0, + Joystick1Button1, + Joystick1Button2, + Joystick1Button3, + Joystick1Button4, + Joystick1Button5, + Joystick1Button6, + Joystick1Button7, + Joystick1Button8, + Joystick1Button9, + Joystick1Button10, + Joystick1Button11, + Joystick1Button12, + Joystick1Button13, + Joystick1Button14, + Joystick1Button15, + Joystick1Button16, + Joystick1Button17, + Joystick1Button18, + Joystick1Button19, + Joystick2Button0, + Joystick2Button1, + Joystick2Button2, + Joystick2Button3, + Joystick2Button4, + Joystick2Button5, + Joystick2Button6, + Joystick2Button7, + Joystick2Button8, + Joystick2Button9, + Joystick2Button10, + Joystick2Button11, + Joystick2Button12, + Joystick2Button13, + Joystick2Button14, + Joystick2Button15, + Joystick2Button16, + Joystick2Button17, + Joystick2Button18, + Joystick2Button19, + Joystick3Button0, + Joystick3Button1, + Joystick3Button2, + Joystick3Button3, + Joystick3Button4, + Joystick3Button5, + Joystick3Button6, + Joystick3Button7, + Joystick3Button8, + Joystick3Button9, + Joystick3Button10, + Joystick3Button11, + Joystick3Button12, + Joystick3Button13, + Joystick3Button14, + Joystick3Button15, + Joystick3Button16, + Joystick3Button17, + Joystick3Button18, + Joystick3Button19, + Joystick4Button0, + Joystick4Button1, + Joystick4Button2, + Joystick4Button3, + Joystick4Button4, + Joystick4Button5, + Joystick4Button6, + Joystick4Button7, + Joystick4Button8, + Joystick4Button9, + Joystick4Button10, + Joystick4Button11, + Joystick4Button12, + Joystick4Button13, + Joystick4Button14, + Joystick4Button15, + Joystick4Button16, + Joystick4Button17, + Joystick4Button18, + Joystick4Button19, + Joystick5Button0, + Joystick5Button1, + Joystick5Button2, + Joystick5Button3, + Joystick5Button4, + Joystick5Button5, + Joystick5Button6, + Joystick5Button7, + Joystick5Button8, + Joystick5Button9, + Joystick5Button10, + Joystick5Button11, + Joystick5Button12, + Joystick5Button13, + Joystick5Button14, + Joystick5Button15, + Joystick5Button16, + Joystick5Button17, + Joystick5Button18, + Joystick5Button19, + Joystick6Button0, + Joystick6Button1, + Joystick6Button2, + Joystick6Button3, + Joystick6Button4, + Joystick6Button5, + Joystick6Button6, + Joystick6Button7, + Joystick6Button8, + Joystick6Button9, + Joystick6Button10, + Joystick6Button11, + Joystick6Button12, + Joystick6Button13, + Joystick6Button14, + Joystick6Button15, + Joystick6Button16, + Joystick6Button17, + Joystick6Button18, + Joystick6Button19, + Joystick7Button0, + Joystick7Button1, + Joystick7Button2, + Joystick7Button3, + Joystick7Button4, + Joystick7Button5, + Joystick7Button6, + Joystick7Button7, + Joystick7Button8, + Joystick7Button9, + Joystick7Button10, + Joystick7Button11, + Joystick7Button12, + Joystick7Button13, + Joystick7Button14, + Joystick7Button15, + Joystick7Button16, + Joystick7Button17, + Joystick7Button18, + Joystick7Button19, + Joystick8Button0, + Joystick8Button1, + Joystick8Button2, + Joystick8Button3, + Joystick8Button4, + Joystick8Button5, + Joystick8Button6, + Joystick8Button7, + Joystick8Button8, + Joystick8Button9, + Joystick8Button10, + Joystick8Button11, + Joystick8Button12, + Joystick8Button13, + Joystick8Button14, + Joystick8Button15, + Joystick8Button16, + Joystick8Button17, + Joystick8Button18, + Joystick8Button19 +} - public static class Input - { - private static readonly GetKeyIntDelegate GetKeyInt_Value - = UnhollowerBaseLib.IL2CPP - .ResolveICall("UnityEngine.Input::GetKeyInt(UnityEngine.KeyCode)"); +public static class Input +{ + private static readonly GetKeyIntDelegate GetKeyInt_Value + = UnhollowerBaseLib.IL2CPP + .ResolveICall("UnityEngine.Input::GetKeyInt(UnityEngine.KeyCode)"); - public static bool GetKeyInt(KeyCode key) => GetKeyInt_Value(key); + public static bool GetKeyInt(KeyCode key) => GetKeyInt_Value(key); - private delegate bool GetKeyIntDelegate(KeyCode key); - } + private delegate bool GetKeyIntDelegate(KeyCode key); } diff --git a/BepInEx.IL2CPP/Utils/Collections/CollectionExtensions.cs b/BepInEx.IL2CPP/Utils/Collections/CollectionExtensions.cs index a1b72c5..482de95 100644 --- a/BepInEx.IL2CPP/Utils/Collections/CollectionExtensions.cs +++ b/BepInEx.IL2CPP/Utils/Collections/CollectionExtensions.cs @@ -1,27 +1,18 @@ using Il2CppSystem.Collections; -namespace BepInEx.IL2CPP.Utils.Collections +namespace BepInEx.IL2CPP.Utils.Collections; + +public static class CollectionExtensions { - public static class CollectionExtensions - { - public static IEnumerator WrapToIl2Cpp(this System.Collections.IEnumerator self) - { - return new Il2CppManagedEnumerator(self).Cast(); - } + public static IEnumerator WrapToIl2Cpp(this System.Collections.IEnumerator self) => + new Il2CppManagedEnumerator(self).Cast(); - public static System.Collections.IEnumerator WrapToManaged(this IEnumerator self) - { - return new ManagedIl2CppEnumerator(self); - } + public static System.Collections.IEnumerator WrapToManaged(this IEnumerator self) => + new ManagedIl2CppEnumerator(self); - public static IEnumerable WrapToIl2Cpp(this System.Collections.IEnumerable self) - { - return new Il2CppManagedEnumerable(self).Cast(); - } + public static IEnumerable WrapToIl2Cpp(this System.Collections.IEnumerable self) => + new Il2CppManagedEnumerable(self).Cast(); - public static System.Collections.IEnumerable WrapToManaged(this IEnumerable self) - { - return new ManagedIl2CppEnumerable(self); - } - } + public static System.Collections.IEnumerable WrapToManaged(this IEnumerable self) => + new ManagedIl2CppEnumerable(self); } diff --git a/BepInEx.IL2CPP/Utils/Collections/Il2CppManagedEnumerable.cs b/BepInEx.IL2CPP/Utils/Collections/Il2CppManagedEnumerable.cs index 41373bd..6a0e070 100644 --- a/BepInEx.IL2CPP/Utils/Collections/Il2CppManagedEnumerable.cs +++ b/BepInEx.IL2CPP/Utils/Collections/Il2CppManagedEnumerable.cs @@ -5,30 +5,29 @@ using ArgumentNullException = System.ArgumentNullException; using IEnumerator = Il2CppSystem.Collections.IEnumerator; using IntPtr = System.IntPtr; -namespace BepInEx.IL2CPP.Utils.Collections +namespace BepInEx.IL2CPP.Utils.Collections; + +public class Il2CppManagedEnumerable : Object { - public class Il2CppManagedEnumerable : Object + private readonly IEnumerable enumerable; + + static Il2CppManagedEnumerable() { - private readonly IEnumerable enumerable; - - static Il2CppManagedEnumerable() + ClassInjector.RegisterTypeInIl2Cpp(new RegisterTypeOptions { - ClassInjector.RegisterTypeInIl2Cpp(new RegisterTypeOptions - { - Interfaces = new[] { typeof(Il2CppSystem.Collections.IEnumerable) } - }); - } - - public Il2CppManagedEnumerable(IntPtr ptr) : base(ptr) { } - - public Il2CppManagedEnumerable(IEnumerable enumerable) - : base(ClassInjector.DerivedConstructorPointer()) - { - this.enumerable = enumerable ?? throw new ArgumentNullException(nameof(enumerable)); - ClassInjector.DerivedConstructorBody(this); - } - - public IEnumerator GetEnumerator() => - new Il2CppManagedEnumerator(enumerable.GetEnumerator()).Cast(); + Interfaces = new[] { typeof(Il2CppSystem.Collections.IEnumerable) } + }); } + + public Il2CppManagedEnumerable(IntPtr ptr) : base(ptr) { } + + public Il2CppManagedEnumerable(IEnumerable enumerable) + : base(ClassInjector.DerivedConstructorPointer()) + { + this.enumerable = enumerable ?? throw new ArgumentNullException(nameof(enumerable)); + ClassInjector.DerivedConstructorBody(this); + } + + public IEnumerator GetEnumerator() => + new Il2CppManagedEnumerator(enumerable.GetEnumerator()).Cast(); } diff --git a/BepInEx.IL2CPP/Utils/Collections/Il2CppManagedEnumerator.cs b/BepInEx.IL2CPP/Utils/Collections/Il2CppManagedEnumerator.cs index bb9e490..aa62c7c 100644 --- a/BepInEx.IL2CPP/Utils/Collections/Il2CppManagedEnumerator.cs +++ b/BepInEx.IL2CPP/Utils/Collections/Il2CppManagedEnumerator.cs @@ -11,79 +11,78 @@ using IntPtr = System.IntPtr; using NotSupportedException = System.NotSupportedException; using Type = System.Type; -namespace BepInEx.IL2CPP.Utils.Collections +namespace BepInEx.IL2CPP.Utils.Collections; + +public class Il2CppManagedEnumerator : Object { - public class Il2CppManagedEnumerator : Object + private static readonly Dictionary> boxers = new(); + + private readonly IEnumerator enumerator; + + static Il2CppManagedEnumerator() { - private static readonly Dictionary> boxers = new(); - - private readonly IEnumerator enumerator; - - static Il2CppManagedEnumerator() + ClassInjector.RegisterTypeInIl2Cpp(new RegisterTypeOptions { - ClassInjector.RegisterTypeInIl2Cpp(new RegisterTypeOptions - { - Interfaces = new[] { typeof(Il2CppIEnumerator) } - }); - } + Interfaces = new[] { typeof(Il2CppIEnumerator) } + }); + } - public Il2CppManagedEnumerator(IntPtr ptr) : base(ptr) { } + public Il2CppManagedEnumerator(IntPtr ptr) : base(ptr) { } - public Il2CppManagedEnumerator(IEnumerator enumerator) - : base(ClassInjector.DerivedConstructorPointer()) - { - this.enumerator = enumerator ?? throw new ArgumentNullException(nameof(enumerator)); - ClassInjector.DerivedConstructorBody(this); - } + public Il2CppManagedEnumerator(IEnumerator enumerator) + : base(ClassInjector.DerivedConstructorPointer()) + { + this.enumerator = enumerator ?? throw new ArgumentNullException(nameof(enumerator)); + ClassInjector.DerivedConstructorBody(this); + } - public Object Current => enumerator.Current switch - { - Il2CppIEnumerator i => i.Cast(), - IEnumerator e => new Il2CppManagedEnumerator(e), - Object oo => oo, - { } obj => ManagedToIl2CppObject(obj), - null => null - }; + public Object Current => enumerator.Current switch + { + Il2CppIEnumerator i => i.Cast(), + IEnumerator e => new Il2CppManagedEnumerator(e), + Object oo => oo, + { } obj => ManagedToIl2CppObject(obj), + null => null + }; - public bool MoveNext() => enumerator.MoveNext(); + public bool MoveNext() => enumerator.MoveNext(); - public void Reset() => enumerator.Reset(); + public void Reset() => enumerator.Reset(); - private static System.Func GetValueBoxer(Type t) - { - if (boxers.TryGetValue(t, out var conv)) - return conv; + private static System.Func GetValueBoxer(Type t) + { + if (boxers.TryGetValue(t, out var conv)) + return conv; - var dm = new DynamicMethod($"Il2CppUnbox_{t.FullDescription()}", typeof(Object), - new[] { typeof(object) }); - var il = dm.GetILGenerator(); - var loc = il.DeclareLocal(t); - var classField = typeof(Il2CppClassPointerStore<>).MakeGenericType(t) - .GetField(nameof(Il2CppClassPointerStore - .NativeClassPtr)); - il.Emit(OpCodes.Ldsfld, classField); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Unbox_Any, t); - il.Emit(OpCodes.Stloc, loc); - il.Emit(OpCodes.Ldloca, loc); - il.Emit(OpCodes.Call, - typeof(UnhollowerBaseLib.IL2CPP).GetMethod(nameof(UnhollowerBaseLib.IL2CPP.il2cpp_value_box))); - il.Emit(OpCodes.Newobj, typeof(Object).GetConstructor(new[] { typeof(IntPtr) })); - il.Emit(OpCodes.Ret); + var dm = new DynamicMethod($"Il2CppUnbox_{t.FullDescription()}", typeof(Object), + new[] { typeof(object) }); + var il = dm.GetILGenerator(); + var loc = il.DeclareLocal(t); + var classField = typeof(Il2CppClassPointerStore<>).MakeGenericType(t) + .GetField(nameof(Il2CppClassPointerStore + .NativeClassPtr)); + il.Emit(OpCodes.Ldsfld, classField); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Unbox_Any, t); + il.Emit(OpCodes.Stloc, loc); + il.Emit(OpCodes.Ldloca, loc); + il.Emit(OpCodes.Call, + typeof(UnhollowerBaseLib.IL2CPP).GetMethod(nameof(UnhollowerBaseLib.IL2CPP.il2cpp_value_box))); + il.Emit(OpCodes.Newobj, typeof(Object).GetConstructor(new[] { typeof(IntPtr) })); + il.Emit(OpCodes.Ret); - var converter = dm.CreateDelegate(typeof(System.Func)) as System.Func; - boxers[t] = converter; - return converter; - } + var converter = dm.CreateDelegate(typeof(System.Func)) as System.Func; + boxers[t] = converter; + return converter; + } - private static Object ManagedToIl2CppObject(object obj) - { - var t = obj.GetType(); - if (obj is string s) - return new Object(UnhollowerBaseLib.IL2CPP.ManagedStringToIl2Cpp(s)); - if (t.IsPrimitive) - return GetValueBoxer(t)(obj); - throw new NotSupportedException($"Type {t} cannot be converted directly to an Il2Cpp object"); - } + private static Object ManagedToIl2CppObject(object obj) + { + var t = obj.GetType(); + if (obj is string s) + return new Object(UnhollowerBaseLib.IL2CPP.ManagedStringToIl2Cpp(s)); + if (t.IsPrimitive) + return GetValueBoxer(t)(obj); + throw new NotSupportedException($"Type {t} cannot be converted directly to an Il2Cpp object"); } } diff --git a/BepInEx.IL2CPP/Utils/Collections/ManagedIl2CppEnumerable.cs b/BepInEx.IL2CPP/Utils/Collections/ManagedIl2CppEnumerable.cs index 0789adb..aa8377e 100644 --- a/BepInEx.IL2CPP/Utils/Collections/ManagedIl2CppEnumerable.cs +++ b/BepInEx.IL2CPP/Utils/Collections/ManagedIl2CppEnumerable.cs @@ -1,17 +1,16 @@ using System; using System.Collections; -namespace BepInEx.IL2CPP.Utils.Collections +namespace BepInEx.IL2CPP.Utils.Collections; + +public class ManagedIl2CppEnumerable : IEnumerable { - public class ManagedIl2CppEnumerable : IEnumerable + private Il2CppSystem.Collections.IEnumerable enumerable; + + public ManagedIl2CppEnumerable(Il2CppSystem.Collections.IEnumerable enumerable) { - private Il2CppSystem.Collections.IEnumerable enumerable; - - public ManagedIl2CppEnumerable(Il2CppSystem.Collections.IEnumerable enumerable) - { - this.enumerable = enumerable ?? throw new ArgumentNullException(nameof(enumerable)); - } - - public IEnumerator GetEnumerator() => new ManagedIl2CppEnumerator(enumerable.GetEnumerator()); + this.enumerable = enumerable ?? throw new ArgumentNullException(nameof(enumerable)); } + + public IEnumerator GetEnumerator() => new ManagedIl2CppEnumerator(enumerable.GetEnumerator()); } diff --git a/BepInEx.IL2CPP/Utils/Collections/ManagedIl2CppEnumerator.cs b/BepInEx.IL2CPP/Utils/Collections/ManagedIl2CppEnumerator.cs index 4f5edba..f3e4114 100644 --- a/BepInEx.IL2CPP/Utils/Collections/ManagedIl2CppEnumerator.cs +++ b/BepInEx.IL2CPP/Utils/Collections/ManagedIl2CppEnumerator.cs @@ -3,29 +3,28 @@ using System.Collections; using HarmonyLib; using MonoMod.Utils; -namespace BepInEx.IL2CPP.Utils.Collections +namespace BepInEx.IL2CPP.Utils.Collections; + +public class ManagedIl2CppEnumerator : IEnumerator { - public class ManagedIl2CppEnumerator : IEnumerator + private static readonly Func moveNext = AccessTools + .Method(typeof(Il2CppSystem.Collections.IEnumerator), "MoveNext") + ?.CreateDelegate>(); + + private static readonly Action reset = AccessTools + .Method(typeof(Il2CppSystem.Collections.IEnumerator), "Reset") + ?.CreateDelegate>(); + + private readonly Il2CppSystem.Collections.IEnumerator enumerator; + + public ManagedIl2CppEnumerator(Il2CppSystem.Collections.IEnumerator enumerator) { - private static readonly Func moveNext = AccessTools - .Method(typeof(Il2CppSystem.Collections.IEnumerator), "MoveNext") - ?.CreateDelegate>(); - - private static readonly Action reset = AccessTools - .Method(typeof(Il2CppSystem.Collections.IEnumerator), "Reset") - ?.CreateDelegate>(); - - private readonly Il2CppSystem.Collections.IEnumerator enumerator; - - public ManagedIl2CppEnumerator(Il2CppSystem.Collections.IEnumerator enumerator) - { - this.enumerator = enumerator ?? throw new ArgumentNullException(nameof(enumerator)); - } - - public bool MoveNext() => moveNext?.Invoke(enumerator) ?? false; - - public void Reset() => reset?.Invoke(enumerator); - - public object Current => enumerator.Current; + this.enumerator = enumerator ?? throw new ArgumentNullException(nameof(enumerator)); } + + public bool MoveNext() => moveNext?.Invoke(enumerator) ?? false; + + public void Reset() => reset?.Invoke(enumerator); + + public object Current => enumerator.Current; } diff --git a/BepInEx.IL2CPP/Utils/Il2CppUtils.cs b/BepInEx.IL2CPP/Utils/Il2CppUtils.cs index 1855ff1..7061619 100644 --- a/BepInEx.IL2CPP/Utils/Il2CppUtils.cs +++ b/BepInEx.IL2CPP/Utils/Il2CppUtils.cs @@ -3,22 +3,21 @@ using UnhollowerBaseLib; using UnhollowerRuntimeLib; using UnityEngine; -namespace BepInEx.IL2CPP.Utils +namespace BepInEx.IL2CPP.Utils; + +internal static class Il2CppUtils { - internal static class Il2CppUtils + // TODO: Check if we can safely initialize this in Chainloader instead + private static GameObject managerGo; + + public static Il2CppObjectBase AddComponent(Type t) { - // TODO: Check if we can safely initialize this in Chainloader instead - private static GameObject managerGo; + if (managerGo == null) + managerGo = new GameObject { hideFlags = HideFlags.HideAndDontSave }; - public static Il2CppObjectBase AddComponent(Type t) - { - if (managerGo == null) - managerGo = new GameObject { hideFlags = HideFlags.HideAndDontSave }; + if (!ClassInjector.IsTypeRegisteredInIl2Cpp(t)) + ClassInjector.RegisterTypeInIl2Cpp(t); - if (!ClassInjector.IsTypeRegisteredInIl2Cpp(t)) - ClassInjector.RegisterTypeInIl2Cpp(t); - - return managerGo.AddComponent(Il2CppType.From(t)); - } + return managerGo.AddComponent(Il2CppType.From(t)); } } diff --git a/BepInEx.IL2CPP/Utils/MonoBehaviourExtensions.cs b/BepInEx.IL2CPP/Utils/MonoBehaviourExtensions.cs index 61782a8..539635a 100644 --- a/BepInEx.IL2CPP/Utils/MonoBehaviourExtensions.cs +++ b/BepInEx.IL2CPP/Utils/MonoBehaviourExtensions.cs @@ -2,13 +2,10 @@ using BepInEx.IL2CPP.Utils.Collections; using UnityEngine; -namespace BepInEx.IL2CPP.Utils +namespace BepInEx.IL2CPP.Utils; + +public static class MonoBehaviourExtensions { - public static class MonoBehaviourExtensions - { - public static Coroutine StartCoroutine(this MonoBehaviour self, IEnumerator coroutine) - { - return self.StartCoroutine(coroutine.WrapToIl2Cpp()); - } - } + public static Coroutine StartCoroutine(this MonoBehaviour self, IEnumerator coroutine) => + self.StartCoroutine(coroutine.WrapToIl2Cpp()); } diff --git a/BepInEx.IL2CPP/app.config b/BepInEx.IL2CPP/app.config index c3012eb..4cb6f5e 100644 --- a/BepInEx.IL2CPP/app.config +++ b/BepInEx.IL2CPP/app.config @@ -1,11 +1,11 @@  - - - - - - - - - \ No newline at end of file + + + + + + + + + diff --git a/BepInEx.NetLauncher/App.config b/BepInEx.NetLauncher/App.config index 88fa402..8671a25 100644 --- a/BepInEx.NetLauncher/App.config +++ b/BepInEx.NetLauncher/App.config @@ -1,6 +1,6 @@  - - + + - \ No newline at end of file + diff --git a/BepInEx.NetLauncher/BasePlugin.cs b/BepInEx.NetLauncher/BasePlugin.cs index c7f6493..c2329d7 100644 --- a/BepInEx.NetLauncher/BasePlugin.cs +++ b/BepInEx.NetLauncher/BasePlugin.cs @@ -2,29 +2,28 @@ using BepInEx.Logging; using HarmonyLib; -namespace BepInEx.NetLauncher +namespace BepInEx.NetLauncher; + +public abstract class BasePlugin { - public abstract class BasePlugin + protected BasePlugin() { - protected BasePlugin() - { - var metadata = MetadataHelper.GetMetadata(this); + var metadata = MetadataHelper.GetMetadata(this); - HarmonyInstance = new Harmony("BepInEx.Plugin." + metadata.GUID); + HarmonyInstance = new Harmony("BepInEx.Plugin." + metadata.GUID); - Log = Logger.CreateLogSource(metadata.Name); + Log = Logger.CreateLogSource(metadata.Name); - Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, metadata.GUID + ".cfg"), false, metadata); - } - - public ManualLogSource Log { get; } - - public ConfigFile Config { get; } - - public Harmony HarmonyInstance { get; set; } - - public abstract void Load(); - - public virtual bool Unload() => false; + Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, metadata.GUID + ".cfg"), false, metadata); } + + public ManualLogSource Log { get; } + + public ConfigFile Config { get; } + + public Harmony HarmonyInstance { get; set; } + + public abstract void Load(); + + public virtual bool Unload() => false; } diff --git a/BepInEx.NetLauncher/BepInEx.NetLauncher.csproj b/BepInEx.NetLauncher/BepInEx.NetLauncher.csproj index 9cd3e35..9a9d72f 100644 --- a/BepInEx.NetLauncher/BepInEx.NetLauncher.csproj +++ b/BepInEx.NetLauncher/BepInEx.NetLauncher.csproj @@ -6,7 +6,7 @@ true BepInEx.NetLauncher BepInEx.NetLauncher - 9 + 10 false diff --git a/BepInEx.NetLauncher/NetChainloader.cs b/BepInEx.NetLauncher/NetChainloader.cs index 92e7a60..273597d 100644 --- a/BepInEx.NetLauncher/NetChainloader.cs +++ b/BepInEx.NetLauncher/NetChainloader.cs @@ -3,26 +3,25 @@ using System.Reflection; using BepInEx.Bootstrap; using BepInEx.Preloader.Core.Logging; -namespace BepInEx.NetLauncher +namespace BepInEx.NetLauncher; + +public class NetChainloader : BaseChainloader { - public class NetChainloader : BaseChainloader + public override BasePlugin LoadPlugin(PluginInfo pluginInfo, Assembly pluginAssembly) { - public override BasePlugin LoadPlugin(PluginInfo pluginInfo, Assembly pluginAssembly) - { - var type = pluginAssembly.GetType(pluginInfo.TypeName); + var type = pluginAssembly.GetType(pluginInfo.TypeName); - var pluginInstance = (BasePlugin) Activator.CreateInstance(type); + var pluginInstance = (BasePlugin) Activator.CreateInstance(type); - pluginInstance.Load(); + pluginInstance.Load(); - return pluginInstance; - } + return pluginInstance; + } - protected override void InitializeLoggers() - { - base.InitializeLoggers(); + protected override void InitializeLoggers() + { + base.InitializeLoggers(); - ChainloaderLogHelper.RewritePreloaderLogs(); - } + ChainloaderLogHelper.RewritePreloaderLogs(); } } diff --git a/BepInEx.NetLauncher/NetPreloader.cs b/BepInEx.NetLauncher/NetPreloader.cs index 00cb53b..490f67c 100644 --- a/BepInEx.NetLauncher/NetPreloader.cs +++ b/BepInEx.NetLauncher/NetPreloader.cs @@ -14,151 +14,151 @@ using BepInEx.Preloader.Core.Patching; using HarmonyLib; using MonoMod.Utils; -namespace BepInEx.NetLauncher +namespace BepInEx.NetLauncher; + +public static class NetPreloader { - public static class NetPreloader + private static readonly ManualLogSource Log = PreloaderLogger.Log; + + [DllImport("Kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern bool SetDllDirectory(string lpPathName); + + [DllImport("Kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern bool AddDllDirectory(string lpPathName); + + + public static void Start(string[] args) { - private static readonly ManualLogSource Log = PreloaderLogger.Log; + var preloaderListener = new PreloaderConsoleListener(); + Logger.Listeners.Add(preloaderListener); - [DllImport("Kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - private static extern bool SetDllDirectory(string lpPathName); - - [DllImport("Kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - private static extern bool AddDllDirectory(string lpPathName); - - - public static void Start(string[] args) + if (string.IsNullOrEmpty(ConfigEntrypointExecutable.Value)) { - var preloaderListener = new PreloaderConsoleListener(); - Logger.Listeners.Add(preloaderListener); + Log.LogFatal($"Entry executable was not set. Please set this in your config before launching the application"); + Program.ReadExit(); + return; + } - if (string.IsNullOrEmpty(ConfigEntrypointExecutable.Value)) + var executablePath = Path.GetFullPath(ConfigEntrypointExecutable.Value); + + if (!File.Exists(executablePath)) + { + Log.LogFatal($"Unable to locate executable: {ConfigEntrypointExecutable.Value}"); + Program.ReadExit(); + return; + } + + + Paths.SetExecutablePath(executablePath); + Program.ResolveDirectories.Add(Paths.GameRootPath); + + foreach (var searchDir in Program.ResolveDirectories) + TypeLoader.SearchDirectories.Add(searchDir); + + if (PlatformHelper.Is(Platform.Windows)) + { + AddDllDirectory(Paths.GameRootPath); + SetDllDirectory(Paths.GameRootPath); + } + + + Logger.Sources.Add(TraceLogSource.CreateSource()); + + ChainloaderLogHelper.PrintLogInfo(Log); + + Log.LogInfo($"CLR runtime version: {Environment.Version}"); + + Log.LogMessage("Preloader started"); + + Assembly entrypointAssembly; + + using (var assemblyPatcher = new AssemblyPatcher()) + { + assemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath); + + Log.LogInfo($"{assemblyPatcher.PatcherContext.PatchDefinitions.Count} patcher definition(s) loaded"); + + assemblyPatcher.LoadAssemblyDirectories(new[] { Paths.GameRootPath }, new[] { "dll", "exe" }); + + Log.LogInfo($"{assemblyPatcher.PatcherContext.AvailableAssemblies.Count} assemblies discovered"); + + assemblyPatcher.PatchAndLoad(); + + + var assemblyName = AssemblyName.GetAssemblyName(executablePath); + + entrypointAssembly = + assemblyPatcher.PatcherContext.LoadedAssemblies.Values.FirstOrDefault(x => x.FullName == + assemblyName.FullName); + + foreach (var loadedAssembly in assemblyPatcher.PatcherContext.LoadedAssemblies) { - Log.LogFatal($"Entry executable was not set. Please set this in your config before launching the application"); - Program.ReadExit(); - return; + // TODO: Need full paths for loaded assemblies + var assemblyPath = Path.Combine(Paths.GameRootPath, loadedAssembly.Key); + + Log.LogDebug($"Registering '{assemblyPath}' as a loaded assembly"); + AssemblyFixes.AssemblyLocations[loadedAssembly.Value.FullName] = assemblyPath; } - var executablePath = Path.GetFullPath(ConfigEntrypointExecutable.Value); - - if (!File.Exists(executablePath)) + if (entrypointAssembly != null) { - Log.LogFatal($"Unable to locate executable: {ConfigEntrypointExecutable.Value}"); - Program.ReadExit(); - return; + Log.LogDebug("Found patched entrypoint assembly! Using it"); } - - - Paths.SetExecutablePath(executablePath); - Program.ResolveDirectories.Add(Paths.GameRootPath); - - foreach (var searchDir in Program.ResolveDirectories) - TypeLoader.SearchDirectories.Add(searchDir); - - if (PlatformHelper.Is(Platform.Windows)) + else { - AddDllDirectory(Paths.GameRootPath); - SetDllDirectory(Paths.GameRootPath); - } - - - Logger.Sources.Add(TraceLogSource.CreateSource()); - - ChainloaderLogHelper.PrintLogInfo(Log); - - Log.LogInfo($"CLR runtime version: {Environment.Version}"); - - Log.LogMessage("Preloader started"); - - Assembly entrypointAssembly; - - using (var assemblyPatcher = new AssemblyPatcher()) - { - assemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath); - - Log.LogInfo($"{assemblyPatcher.PatcherContext.PatchDefinitions.Count} patcher definition(s) loaded"); - - assemblyPatcher.LoadAssemblyDirectories(new[] { Paths.GameRootPath }, new[] { "dll", "exe" }); - - Log.LogInfo($"{assemblyPatcher.PatcherContext.AvailableAssemblies.Count} assemblies discovered"); - - assemblyPatcher.PatchAndLoad(); - - - var assemblyName = AssemblyName.GetAssemblyName(executablePath); - - entrypointAssembly = - assemblyPatcher.PatcherContext.LoadedAssemblies.Values.FirstOrDefault(x => x.FullName == assemblyName.FullName); - - foreach (var loadedAssembly in assemblyPatcher.PatcherContext.LoadedAssemblies) - { - // TODO: Need full paths for loaded assemblies - var assemblyPath = Path.Combine(Paths.GameRootPath, loadedAssembly.Key); - - Log.LogDebug($"Registering '{assemblyPath}' as a loaded assembly"); - AssemblyFixes.AssemblyLocations[loadedAssembly.Value.FullName] = assemblyPath; - } - - if (entrypointAssembly != null) - { - Log.LogDebug("Found patched entrypoint assembly! Using it"); - } - else - { - Log.LogDebug("Using entrypoint assembly from disk"); - entrypointAssembly = Assembly.LoadFrom(executablePath); - } - } - - Log.LogMessage("Preloader finished"); - - Logger.Listeners.Remove(preloaderListener); - - var chainloader = new NetChainloader(); - chainloader.Initialize(); - chainloader.Execute(); - - - AssemblyFixes.Execute(entrypointAssembly); - - try - { - var argList = new List(); - - var paramTypes = entrypointAssembly.EntryPoint.GetParameters(); - - if (paramTypes.Length == 1 && paramTypes[0].ParameterType == typeof(string[])) - { - argList.Add(args); - } - else if (paramTypes.Length == 1 && paramTypes[0].ParameterType == typeof(string)) - { - argList.Add(string.Join(" ", args)); - } - else if (paramTypes.Length != 0) - { - // Only other entrypoint signatures I can think of that .NET supports is Task / Task - // async entrypoints. That's a can of worms for another time though - - Log.LogFatal($"Could not figure out how to handle entrypoint method with this signature: {entrypointAssembly.EntryPoint.FullDescription()}"); - return; - } - - entrypointAssembly.EntryPoint.Invoke(null, argList.ToArray()); - } - catch (Exception ex) - { - Log.LogFatal($"Unhandled exception: {ex}"); + Log.LogDebug("Using entrypoint assembly from disk"); + entrypointAssembly = Assembly.LoadFrom(executablePath); } } - #region Config + Log.LogMessage("Preloader finished"); - private static readonly ConfigEntry ConfigEntrypointExecutable = ConfigFile.CoreConfig.Bind( - "Preloader.Entrypoint", "Assembly", - null, - "The local filename of the .NET executable to target."); + Logger.Listeners.Remove(preloaderListener); - #endregion + var chainloader = new NetChainloader(); + chainloader.Initialize(); + chainloader.Execute(); + + + AssemblyFixes.Execute(entrypointAssembly); + + try + { + var argList = new List(); + + var paramTypes = entrypointAssembly.EntryPoint.GetParameters(); + + if (paramTypes.Length == 1 && paramTypes[0].ParameterType == typeof(string[])) + { + argList.Add(args); + } + else if (paramTypes.Length == 1 && paramTypes[0].ParameterType == typeof(string)) + { + argList.Add(string.Join(" ", args)); + } + else if (paramTypes.Length != 0) + { + // Only other entrypoint signatures I can think of that .NET supports is Task / Task + // async entrypoints. That's a can of worms for another time though + + Log.LogFatal($"Could not figure out how to handle entrypoint method with this signature: {entrypointAssembly.EntryPoint.FullDescription()}"); + return; + } + + entrypointAssembly.EntryPoint.Invoke(null, argList.ToArray()); + } + catch (Exception ex) + { + Log.LogFatal($"Unhandled exception: {ex}"); + } } + + #region Config + + private static readonly ConfigEntry ConfigEntrypointExecutable = ConfigFile.CoreConfig.Bind( + "Preloader.Entrypoint", "Assembly", + null, + "The local filename of the .NET executable to target."); + + #endregion } diff --git a/BepInEx.NetLauncher/Program.cs b/BepInEx.NetLauncher/Program.cs index 77fedf7..1ab0c0d 100644 --- a/BepInEx.NetLauncher/Program.cs +++ b/BepInEx.NetLauncher/Program.cs @@ -7,143 +7,142 @@ using System.Reflection; using BepInEx.Logging; using BepInEx.Preloader.Core; -namespace BepInEx.NetLauncher +namespace BepInEx.NetLauncher; + +internal static class NetPreloaderRunner { - internal static class NetPreloaderRunner + internal static void PreloaderMain(string[] args) { - internal static void PreloaderMain(string[] args) + try { - try - { - Logger.Listeners.Add(new ConsoleLogListener()); + Logger.Listeners.Add(new ConsoleLogListener()); - ConsoleManager.Initialize(true); + ConsoleManager.Initialize(true); - NetPreloader.Start(args); - } - catch (Exception ex) - { - PreloaderLogger.Log.LogFatal("Unhandled exception"); - PreloaderLogger.Log.LogFatal(ex); - Program.ReadExit(); - } + NetPreloader.Start(args); } - - internal static void OuterMain(string[] args, string filename) + catch (Exception ex) { - Paths.SetExecutablePath(filename); - - AppDomain.CurrentDomain.AssemblyResolve += LocalResolve; - - PreloaderMain(args); - } - - private static Assembly LocalResolve(object sender, ResolveEventArgs args) - { - var assemblyName = new AssemblyName(args.Name); - - var foundAssembly = AppDomain.CurrentDomain.GetAssemblies() - .FirstOrDefault(x => x.GetName().Name == assemblyName.Name); - - if (foundAssembly != null) - return foundAssembly; - - if (LocalUtility.TryResolveDllAssembly(assemblyName, Paths.BepInExAssemblyDirectory, out foundAssembly) - || LocalUtility.TryResolveDllAssembly(assemblyName, Paths.PatcherPluginPath, out foundAssembly) - || LocalUtility.TryResolveDllAssembly(assemblyName, Paths.PluginPath, out foundAssembly)) - return foundAssembly; - - return null; + PreloaderLogger.Log.LogFatal("Unhandled exception"); + PreloaderLogger.Log.LogFatal(ex); + Program.ReadExit(); } } - internal class Program + internal static void OuterMain(string[] args, string filename) { - public static List ResolveDirectories { get; set; } = new() - { - "C:\\Windows\\Microsoft.NET\\assembly\\GAC_32\\Microsoft.Xna.Framework.Game\\v4.0_4.0.0.0__842cf8be1de50553\\" - }; + Paths.SetExecutablePath(filename); - internal static void ReadExit() - { - Console.WriteLine("Press enter to exit..."); - Console.ReadLine(); - Environment.Exit(-1); - } + AppDomain.CurrentDomain.AssemblyResolve += LocalResolve; - private static void Main(string[] args) + PreloaderMain(args); + } + + private static Assembly LocalResolve(object sender, ResolveEventArgs args) + { + var assemblyName = new AssemblyName(args.Name); + + var foundAssembly = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(x => x.GetName().Name == assemblyName.Name); + + if (foundAssembly != null) + return foundAssembly; + + if (LocalUtility.TryResolveDllAssembly(assemblyName, Paths.BepInExAssemblyDirectory, out foundAssembly) + || LocalUtility.TryResolveDllAssembly(assemblyName, Paths.PatcherPluginPath, out foundAssembly) + || LocalUtility.TryResolveDllAssembly(assemblyName, Paths.PluginPath, out foundAssembly)) + return foundAssembly; + + return null; + } +} + +internal class Program +{ + public static List ResolveDirectories { get; set; } = new() + { + "C:\\Windows\\Microsoft.NET\\assembly\\GAC_32\\Microsoft.Xna.Framework.Game\\v4.0_4.0.0.0__842cf8be1de50553\\" + }; + + internal static void ReadExit() + { + Console.WriteLine("Press enter to exit..."); + Console.ReadLine(); + Environment.Exit(-1); + } + + private static void Main(string[] args) + { + try { - try - { - string filename; + string filename; #if DEBUG - filename = - Path.Combine(Directory.GetCurrentDirectory(), - Path.GetFileName(Process.GetCurrentProcess().MainModule.FileName)); - ResolveDirectories.Add(Path.GetDirectoryName(filename)); + filename = + Path.Combine(Directory.GetCurrentDirectory(), + Path.GetFileName(Process.GetCurrentProcess().MainModule.FileName)); + ResolveDirectories.Add(Path.GetDirectoryName(filename)); - // for debugging within VS - ResolveDirectories.Add(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName)); + // for debugging within VS + ResolveDirectories.Add(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName)); #else filename = Process.GetCurrentProcess().MainModule.FileName; ResolveDirectories.Add(Path.Combine(Path.GetDirectoryName(filename), "BepInEx", "core")); #endif - AppDomain.CurrentDomain.AssemblyResolve += RemoteResolve; + AppDomain.CurrentDomain.AssemblyResolve += RemoteResolve; - NetPreloaderRunner.OuterMain(args, filename); - } - catch (Exception ex) - { - Console.WriteLine("Unhandled exception"); - Console.WriteLine(ex); - ReadExit(); - } + NetPreloaderRunner.OuterMain(args, filename); } - - private static Assembly RemoteResolve(object sender, ResolveEventArgs reference) + catch (Exception ex) { - var assemblyName = new AssemblyName(reference.Name); - - foreach (var directory in ResolveDirectories) - { - if (!Directory.Exists(directory)) - continue; - - var potentialDirectories = new List { directory }; - - potentialDirectories.AddRange(Directory.GetDirectories(directory, "*", SearchOption.AllDirectories)); - - var potentialFiles = potentialDirectories.Select(x => Path.Combine(x, $"{assemblyName.Name}.dll")) - .Concat(potentialDirectories.Select(x => - Path - .Combine(x, - $"{assemblyName.Name}.exe"))); - - foreach (var path in potentialFiles) - { - if (!File.Exists(path)) - continue; - - Assembly assembly; - - try - { - assembly = Assembly.LoadFrom(path); - } - catch (Exception ex) - { - continue; - } - - if (assembly.GetName().Name == assemblyName.Name) - return assembly; - } - } - - return null; + Console.WriteLine("Unhandled exception"); + Console.WriteLine(ex); + ReadExit(); } } + + private static Assembly RemoteResolve(object sender, ResolveEventArgs reference) + { + var assemblyName = new AssemblyName(reference.Name); + + foreach (var directory in ResolveDirectories) + { + if (!Directory.Exists(directory)) + continue; + + var potentialDirectories = new List { directory }; + + potentialDirectories.AddRange(Directory.GetDirectories(directory, "*", SearchOption.AllDirectories)); + + var potentialFiles = potentialDirectories.Select(x => Path.Combine(x, $"{assemblyName.Name}.dll")) + .Concat(potentialDirectories.Select(x => + Path + .Combine(x, + $"{assemblyName.Name}.exe"))); + + foreach (var path in potentialFiles) + { + if (!File.Exists(path)) + continue; + + Assembly assembly; + + try + { + assembly = Assembly.LoadFrom(path); + } + catch (Exception ex) + { + continue; + } + + if (assembly.GetName().Name == assemblyName.Name) + return assembly; + } + } + + return null; + } } diff --git a/BepInEx.NetLauncher/RuntimeFixes/AssemblyFixes.cs b/BepInEx.NetLauncher/RuntimeFixes/AssemblyFixes.cs index 6e4d6ec..898aeda 100644 --- a/BepInEx.NetLauncher/RuntimeFixes/AssemblyFixes.cs +++ b/BepInEx.NetLauncher/RuntimeFixes/AssemblyFixes.cs @@ -5,73 +5,72 @@ using System.Reflection; using BepInEx.Logging; using HarmonyLib; -namespace BepInEx.NetLauncher.RuntimeFixes +namespace BepInEx.NetLauncher.RuntimeFixes; + +internal class AssemblyFixes { - internal class AssemblyFixes + private static Assembly EntryAssembly { get; set; } + + public static Dictionary AssemblyLocations { get; } = + new(StringComparer.InvariantCultureIgnoreCase); + + public static void Execute(Assembly entryAssembly) { - private static Assembly EntryAssembly { get; set; } + EntryAssembly = entryAssembly; + Harmony.CreateAndPatchAll(typeof(AssemblyFixes), "io.bepinex.assemblyfix"); + } - public static Dictionary AssemblyLocations { get; } = - new(StringComparer.InvariantCultureIgnoreCase); + [HarmonyPrefix] + [HarmonyPatch(typeof(Assembly), nameof(Assembly.GetEntryAssembly))] + public static bool GetEntryAssemblyPrefix(ref Assembly __result) + { + Logger.LogDebug($"Ran GetEntryAssembly"); - public static void Execute(Assembly entryAssembly) + __result = EntryAssembly; + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(Assembly), nameof(Assembly.Load), typeof(string))] + public static bool LoadByName(ref Assembly __result, string assemblyString) + { + __result = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(x => x.GetName().FullName == assemblyString); + + + Logger.LogDebug($"LoadByName: {assemblyString} : {__result != null}"); + + return __result != null; + } + + [HarmonyPrefix] + [HarmonyPatch("System.Reflection.RuntimeAssembly", nameof(Assembly.Location), MethodType.Getter)] + public static bool GetLocation(ref string __result, Assembly __instance) + { + Logger.LogDebug($"GetLocation: {__instance.FullName}"); + + if (AssemblyLocations.TryGetValue(__instance.FullName, out var location)) { - EntryAssembly = entryAssembly; - Harmony.CreateAndPatchAll(typeof(AssemblyFixes), "io.bepinex.assemblyfix"); - } - - [HarmonyPrefix] - [HarmonyPatch(typeof(Assembly), nameof(Assembly.GetEntryAssembly))] - public static bool GetEntryAssemblyPrefix(ref Assembly __result) - { - Logger.LogDebug($"Ran GetEntryAssembly"); - - __result = EntryAssembly; + __result = location; return false; } - [HarmonyPrefix] - [HarmonyPatch(typeof(Assembly), nameof(Assembly.Load), typeof(string))] - public static bool LoadByName(ref Assembly __result, string assemblyString) - { - __result = AppDomain.CurrentDomain.GetAssemblies() - .FirstOrDefault(x => x.GetName().FullName == assemblyString); - - - Logger.LogDebug($"LoadByName: {assemblyString} : {__result != null}"); - - return __result != null; - } - - [HarmonyPrefix] - [HarmonyPatch("System.Reflection.RuntimeAssembly", nameof(Assembly.Location), MethodType.Getter)] - public static bool GetLocation(ref string __result, Assembly __instance) - { - Logger.LogDebug($"GetLocation: {__instance.FullName}"); - - if (AssemblyLocations.TryGetValue(__instance.FullName, out var location)) - { - __result = location; - return false; - } - - return true; - } - - // This is commented out because it should be implemented alongside GetLocation, but seems to crash the runtime - // Might be because the target type should be System.Reflection.RuntimeAssembly - - //[HarmonyPrefix] - //[HarmonyPatch(typeof(Assembly), nameof(Assembly.CodeBase), MethodType.Getter)] - //public static bool GetCodeBase(ref string __result, Assembly __instance) - //{ - // if (AssemblyLocations.TryGetValue(__instance.FullName, out var location)) - // { - // __result = $"file://{location.Replace('\\', '/')}"; - // return false; - // } - - // return true; - //} + return true; } + + // This is commented out because it should be implemented alongside GetLocation, but seems to crash the runtime + // Might be because the target type should be System.Reflection.RuntimeAssembly + + //[HarmonyPrefix] + //[HarmonyPatch(typeof(Assembly), nameof(Assembly.CodeBase), MethodType.Getter)] + //public static bool GetCodeBase(ref string __result, Assembly __instance) + //{ + // if (AssemblyLocations.TryGetValue(__instance.FullName, out var location)) + // { + // __result = $"file://{location.Replace('\\', '/')}"; + // return false; + // } + + // return true; + //} } diff --git a/BepInEx.NetLauncher/Utility.cs b/BepInEx.NetLauncher/Utility.cs index 2fd9098..a062eed 100644 --- a/BepInEx.NetLauncher/Utility.cs +++ b/BepInEx.NetLauncher/Utility.cs @@ -3,64 +3,63 @@ using System.Collections.Generic; using System.IO; using System.Reflection; -namespace BepInEx.NetLauncher +namespace BepInEx.NetLauncher; + +/// +/// Generic helper properties and methods. +/// +public static class LocalUtility { /// - /// Generic helper properties and methods. + /// Try to resolve and load the given assembly DLL. /// - public static class LocalUtility + /// 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. + private static bool TryResolveDllAssembly(AssemblyName assemblyName, + string directory, + Func loader, + out T assembly) where T : class { - /// - /// Try to resolve and load the given assembly DLL. - /// - /// Name of the assembly, of the type . - /// Directory to search the assembly from. - /// The loaded assembly. - /// True, if the assembly was found and loaded. Otherwise, false. - private static bool TryResolveDllAssembly(AssemblyName assemblyName, - string directory, - Func loader, - out T assembly) where T : class + assembly = null; + + if (!Directory.Exists(directory)) + return false; + + var potentialDirectories = new List { directory }; + + potentialDirectories.AddRange(Directory.GetDirectories(directory, "*", SearchOption.AllDirectories)); + + foreach (var subDirectory in potentialDirectories) { - assembly = null; + var path = Path.Combine(subDirectory, $"{assemblyName.Name}.dll"); - if (!Directory.Exists(directory)) - return false; + if (!File.Exists(path)) + continue; - var potentialDirectories = new List { directory }; - - potentialDirectories.AddRange(Directory.GetDirectories(directory, "*", SearchOption.AllDirectories)); - - foreach (var subDirectory in potentialDirectories) + try { - var path = Path.Combine(subDirectory, $"{assemblyName.Name}.dll"); - - if (!File.Exists(path)) - continue; - - try - { - assembly = loader(path); - } - catch (Exception) - { - continue; - } - - return true; + assembly = loader(path); + } + catch (Exception) + { + continue; } - return false; + return true; } - /// - /// Try to resolve and load the given assembly DLL. - /// - /// Name of the assembly, of the type . - /// Directory to search the assembly from. - /// The loaded assembly. - /// True, if the assembly was found and loaded. Otherwise, false. - public static bool TryResolveDllAssembly(AssemblyName assemblyName, string directory, out Assembly assembly) => - TryResolveDllAssembly(assemblyName, directory, Assembly.LoadFile, out assembly); + return false; } + + /// + /// Try to resolve and load the given assembly DLL. + /// + /// Name of the assembly, of the type . + /// Directory to search the assembly from. + /// The loaded assembly. + /// True, if the assembly was found and loaded. Otherwise, false. + public static bool TryResolveDllAssembly(AssemblyName assemblyName, string directory, out Assembly assembly) => + TryResolveDllAssembly(assemblyName, directory, Assembly.LoadFile, out assembly); } diff --git a/BepInEx.Preloader.Core/BepInEx.Preloader.Core.csproj b/BepInEx.Preloader.Core/BepInEx.Preloader.Core.csproj index 9d155c3..b84bc54 100644 --- a/BepInEx.Preloader.Core/BepInEx.Preloader.Core.csproj +++ b/BepInEx.Preloader.Core/BepInEx.Preloader.Core.csproj @@ -6,7 +6,7 @@ BepInEx.Preloader.Core ..\bin\Core $(OutputPath)\$(TargetFramework)\BepInEx.Preloader.Core.xml - 9 + 10 full diff --git a/BepInEx.Preloader.Core/EnvVars.cs b/BepInEx.Preloader.Core/EnvVars.cs index c150d8d..180b687 100644 --- a/BepInEx.Preloader.Core/EnvVars.cs +++ b/BepInEx.Preloader.Core/EnvVars.cs @@ -1,43 +1,42 @@ using System; using System.IO; -namespace BepInEx.Preloader.Core +namespace BepInEx.Preloader.Core; + +/// +/// Doorstop environment variables, passed into the BepInEx preloader. +/// https://github.com/NeighTools/UnityDoorstop/wiki#environment-variables +/// +public static class EnvVars { /// - /// Doorstop environment variables, passed into the BepInEx preloader. - /// https://github.com/NeighTools/UnityDoorstop/wiki#environment-variables + /// Path to the assembly that was invoked via Doorstop. Contains the same value as in "targetAssembly" configuration + /// option in the config file. /// - public static class EnvVars + public static string DOORSTOP_INVOKE_DLL_PATH { get; private set; } + + /// + /// Full path to the game's "Managed" folder that contains all the game's managed assemblies + /// + public static string DOORSTOP_MANAGED_FOLDER_DIR { get; private set; } + + /// + /// Full path to the game executable currently running. + /// + public static string DOORSTOP_PROCESS_PATH { get; private set; } + + /// + /// Array of paths where Mono searches DLLs from before assembly resolvers are invoked. + /// + public static string[] DOORSTOP_DLL_SEARCH_DIRS { get; private set; } + + internal static void LoadVars() { - /// - /// Path to the assembly that was invoked via Doorstop. Contains the same value as in "targetAssembly" configuration - /// option in the config file. - /// - public static string DOORSTOP_INVOKE_DLL_PATH { get; private set; } - - /// - /// Full path to the game's "Managed" folder that contains all the game's managed assemblies - /// - public static string DOORSTOP_MANAGED_FOLDER_DIR { get; private set; } - - /// - /// Full path to the game executable currently running. - /// - public static string DOORSTOP_PROCESS_PATH { get; private set; } - - /// - /// Array of paths where Mono searches DLLs from before assembly resolvers are invoked. - /// - public static string[] DOORSTOP_DLL_SEARCH_DIRS { get; private set; } - - internal static void LoadVars() - { - DOORSTOP_INVOKE_DLL_PATH = Environment.GetEnvironmentVariable("DOORSTOP_INVOKE_DLL_PATH"); - DOORSTOP_MANAGED_FOLDER_DIR = Environment.GetEnvironmentVariable("DOORSTOP_MANAGED_FOLDER_DIR"); - DOORSTOP_PROCESS_PATH = Environment.GetEnvironmentVariable("DOORSTOP_PROCESS_PATH"); - DOORSTOP_DLL_SEARCH_DIRS = - Environment.GetEnvironmentVariable("DOORSTOP_DLL_SEARCH_DIRS")?.Split(Path.PathSeparator) ?? - new string[0]; - } + DOORSTOP_INVOKE_DLL_PATH = Environment.GetEnvironmentVariable("DOORSTOP_INVOKE_DLL_PATH"); + DOORSTOP_MANAGED_FOLDER_DIR = Environment.GetEnvironmentVariable("DOORSTOP_MANAGED_FOLDER_DIR"); + DOORSTOP_PROCESS_PATH = Environment.GetEnvironmentVariable("DOORSTOP_PROCESS_PATH"); + DOORSTOP_DLL_SEARCH_DIRS = + Environment.GetEnvironmentVariable("DOORSTOP_DLL_SEARCH_DIRS")?.Split(Path.PathSeparator) ?? + new string[0]; } } diff --git a/BepInEx.Preloader.Core/InternalPreloaderLogger.cs b/BepInEx.Preloader.Core/InternalPreloaderLogger.cs index 78addf0..2fa3cd0 100644 --- a/BepInEx.Preloader.Core/InternalPreloaderLogger.cs +++ b/BepInEx.Preloader.Core/InternalPreloaderLogger.cs @@ -1,9 +1,8 @@ using BepInEx.Logging; -namespace BepInEx.Preloader.Core +namespace BepInEx.Preloader.Core; + +public static class PreloaderLogger { - public static class PreloaderLogger - { - public static ManualLogSource Log { get; } = Logger.CreateLogSource("Preloader"); - } + public static ManualLogSource Log { get; } = Logger.CreateLogSource("Preloader"); } diff --git a/BepInEx.Preloader.Core/Logging/ChainloaderLogHelper.cs b/BepInEx.Preloader.Core/Logging/ChainloaderLogHelper.cs index 789b402..a0cec86 100644 --- a/BepInEx.Preloader.Core/Logging/ChainloaderLogHelper.cs +++ b/BepInEx.Preloader.Core/Logging/ChainloaderLogHelper.cs @@ -3,46 +3,45 @@ using BepInEx.Logging; using BepInEx.Shared; using MonoMod.Utils; -namespace BepInEx.Preloader.Core.Logging +namespace BepInEx.Preloader.Core.Logging; + +public static class ChainloaderLogHelper { - public static class ChainloaderLogHelper + public static void PrintLogInfo(ManualLogSource log) { - public static void PrintLogInfo(ManualLogSource log) + var consoleTitle = $"BepInEx {Paths.BepInExVersion} - {Paths.ProcessName}"; + log.LogMessage(consoleTitle); + + if (ConsoleManager.ConsoleActive) + ConsoleManager.SetConsoleTitle(consoleTitle); + + //See BuildInfoAttribute for more information about this section. + var attributes = typeof(BuildInfoAttribute).Assembly.GetCustomAttributes(typeof(BuildInfoAttribute), false); + + if (attributes.Length > 0) { - var consoleTitle = $"BepInEx {Paths.BepInExVersion} - {Paths.ProcessName}"; - log.LogMessage(consoleTitle); - - if (ConsoleManager.ConsoleActive) - ConsoleManager.SetConsoleTitle(consoleTitle); - - //See BuildInfoAttribute for more information about this section. - var attributes = typeof(BuildInfoAttribute).Assembly.GetCustomAttributes(typeof(BuildInfoAttribute), false); - - if (attributes.Length > 0) - { - var attribute = (BuildInfoAttribute) attributes[0]; - log.LogMessage(attribute.Info); - } - - Logger.LogInfo($"System platform: {PlatformHelper.Current}"); + var attribute = (BuildInfoAttribute) attributes[0]; + log.LogMessage(attribute.Info); } - public static void RewritePreloaderLogs() - { - if (PreloaderConsoleListener.LogEvents == null || PreloaderConsoleListener.LogEvents.Count == 0) - return; + Logger.LogInfo($"System platform: {PlatformHelper.Current}"); + } - // Temporarily disable the console log listener (if there is one from preloader) as we replay the preloader logs - var logListener = Logger.Listeners.FirstOrDefault(logger => logger is ConsoleLogListener); + public static void RewritePreloaderLogs() + { + if (PreloaderConsoleListener.LogEvents == null || PreloaderConsoleListener.LogEvents.Count == 0) + return; - if (logListener != null) - Logger.Listeners.Remove(logListener); + // Temporarily disable the console log listener (if there is one from preloader) as we replay the preloader logs + var logListener = Logger.Listeners.FirstOrDefault(logger => logger is ConsoleLogListener); - foreach (var preloaderLogEvent in PreloaderConsoleListener.LogEvents) - Logger.InternalLogEvent(PreloaderLogger.Log, preloaderLogEvent); + if (logListener != null) + Logger.Listeners.Remove(logListener); - if (logListener != null) - Logger.Listeners.Add(logListener); - } + foreach (var preloaderLogEvent in PreloaderConsoleListener.LogEvents) + Logger.InternalLogEvent(PreloaderLogger.Log, preloaderLogEvent); + + if (logListener != null) + Logger.Listeners.Add(logListener); } } diff --git a/BepInEx.Preloader.Core/Logging/PreloaderConsoleListener.cs b/BepInEx.Preloader.Core/Logging/PreloaderConsoleListener.cs index b454eb6..e535277 100644 --- a/BepInEx.Preloader.Core/Logging/PreloaderConsoleListener.cs +++ b/BepInEx.Preloader.Core/Logging/PreloaderConsoleListener.cs @@ -2,33 +2,32 @@ using BepInEx.Configuration; using BepInEx.Logging; -namespace BepInEx.Preloader.Core.Logging +namespace BepInEx.Preloader.Core.Logging; + +/// +/// Log listener that listens to logs during preloading time and buffers messages for output in Unity logs later. +/// +public class PreloaderConsoleListener : ILogListener { + private static readonly ConfigEntry ConfigConsoleDisplayedLevel = ConfigFile.CoreConfig.Bind( + "Logging.Console", "LogLevels", + LogLevel.Fatal | LogLevel.Error | LogLevel.Warning | LogLevel.Message | LogLevel.Info, + "Which log levels to show in the console output."); + /// - /// Log listener that listens to logs during preloading time and buffers messages for output in Unity logs later. + /// A list of all objects that this listener has received. /// - public class PreloaderConsoleListener : ILogListener + public static List LogEvents { get; } = new(); + + /// + public void LogEvent(object sender, LogEventArgs eventArgs) { - private static readonly ConfigEntry ConfigConsoleDisplayedLevel = ConfigFile.CoreConfig.Bind( - "Logging.Console", "LogLevels", - LogLevel.Fatal | LogLevel.Error | LogLevel.Warning | LogLevel.Message | LogLevel.Info, - "Which log levels to show in the console output."); + if ((eventArgs.Level & ConfigConsoleDisplayedLevel.Value) == 0) + return; - /// - /// A list of all objects that this listener has received. - /// - public static List LogEvents { get; } = new(); - - /// - public void LogEvent(object sender, LogEventArgs eventArgs) - { - if ((eventArgs.Level & ConfigConsoleDisplayedLevel.Value) == 0) - return; - - LogEvents.Add(eventArgs); - } - - /// - public void Dispose() { } + LogEvents.Add(eventArgs); } + + /// + public void Dispose() { } } diff --git a/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs b/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs index ccd05cd..fd25ae9 100644 --- a/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs +++ b/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs @@ -12,517 +12,518 @@ using BepInEx.Logging; using HarmonyLib; using Mono.Cecil; -namespace BepInEx.Preloader.Core.Patching +namespace BepInEx.Preloader.Core.Patching; + +/// +/// Worker class which is used for loading and patching entire folders of assemblies, or alternatively patching and +/// loading assemblies one at a time. +/// +public class AssemblyPatcher : IDisposable { + private static readonly string CurrentAssemblyName = Assembly.GetExecutingAssembly().GetName().Name; + /// - /// Worker class which is used for loading and patching entire folders of assemblies, or alternatively patching and - /// loading assemblies one at a time. + /// The context of this assembly patcher instance that is passed to all patcher plugins. /// - public class AssemblyPatcher : IDisposable + public PatcherContext PatcherContext { get; } = new() { - private static readonly string CurrentAssemblyName = Assembly.GetExecutingAssembly().GetName().Name; + DumpedAssembliesPath = Utility.CombinePaths(Paths.BepInExRootPath, "DumpedAssemblies", Paths.ProcessName) + }; - /// - /// The context of this assembly patcher instance that is passed to all patcher plugins. - /// - public PatcherContext PatcherContext { get; } = new() + /// + /// A cloned version of to ensure that any foreach loops do not break when the collection + /// gets modified. + /// + private IEnumerable PatcherPluginsSafe => PatcherContext.PatcherPlugins.ToList(); + + private ManualLogSource Logger { get; } = BepInEx.Logging.Logger.CreateLogSource("AssemblyPatcher"); + + private static Regex allowedGuidRegex { get; } = new(@"^[a-zA-Z0-9\._\-]+$"); + + /// + /// Performs work to dispose collection objects. + /// + public void Dispose() + { + foreach (var assembly in PatcherContext.AvailableAssemblies) + assembly.Value.Dispose(); + + PatcherContext.AvailableAssemblies.Clear(); + + // Clear to allow GC collection. + PatcherContext.PatcherPlugins.Clear(); + } + + private PatcherPluginMetadata ToPatcherPlugin(TypeDefinition type, string assemblyPath) + { + if (type.IsInterface || type.IsAbstract && !type.IsSealed) + return null; + + try { - DumpedAssembliesPath = Utility.CombinePaths(Paths.BepInExRootPath, "DumpedAssemblies", Paths.ProcessName) + if (!type.IsSubtypeOf(typeof(BasePatcher))) + return null; + } + catch (AssemblyResolutionException) + { + // Can happen if this type inherits a type from an assembly that can't be found. Safe to assume it's not a plugin. + return null; + } + + var metadata = PatcherPluginInfoAttribute.FromCecilType(type); + + // Perform checks that will prevent the plugin from being loaded in ALL cases + if (metadata == null) + { + Logger.LogWarning($"Skipping over type [{type.FullName}] as no metadata attribute is specified"); + return null; + } + + if (string.IsNullOrEmpty(metadata.GUID) || !allowedGuidRegex.IsMatch(metadata.GUID)) + { + Logger.LogWarning($"Skipping type [{type.FullName}] because its GUID [{metadata.GUID}] is of an illegal format"); + return null; + } + + if (metadata.Version == null) + { + Logger.LogWarning($"Skipping type [{type.FullName}] because its version is invalid"); + return null; + } + + if (metadata.Name == null) + { + Logger.LogWarning($"Skipping type [{type.FullName}] because its name is null"); + return null; + } + + return new PatcherPluginMetadata + { + TypeName = type.FullName }; + } - /// - /// A cloned version of to ensure that any foreach loops do not break when the collection - /// gets modified. - /// - private IEnumerable PatcherPluginsSafe => PatcherContext.PatcherPlugins.ToList(); + 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(BasePatcher).FullName)) + return false; - private ManualLogSource Logger { get; } = BepInEx.Logging.Logger.CreateLogSource("AssemblyPatcher"); + return true; + } - private static Regex allowedGuidRegex { get; } = new(@"^[a-zA-Z0-9\._\-]+$"); + /// + /// Adds all patchers from all managed assemblies specified in a directory. + /// + /// Directory to search patcher DLLs from. + public void AddPatchersFromDirectory(string directory) + { + if (!Directory.Exists(directory)) + return; - /// - /// Performs work to dispose collection objects. - /// - public void Dispose() + var sortedPatchers = new List(); + + var patchers = TypeLoader.FindPluginTypes(directory, ToPatcherPlugin, HasPatcherPlugins); + + // TODO: Add dependency ordering and process attribute filtering + + foreach (var keyValuePair in patchers) { - foreach (var assembly in PatcherContext.AvailableAssemblies) - assembly.Value.Dispose(); + var assemblyPath = keyValuePair.Key; + var patcherCollection = keyValuePair.Value; - PatcherContext.AvailableAssemblies.Clear(); + if (patcherCollection.Count == 0) + continue; - // Clear to allow GC collection. - PatcherContext.PatcherPlugins.Clear(); - } + var ass = Assembly.LoadFrom(assemblyPath); - private PatcherPluginMetadata ToPatcherPlugin(TypeDefinition type, string assemblyPath) - { - if (type.IsInterface || type.IsAbstract && !type.IsSealed) - return null; - - try - { - if (!type.IsSubtypeOf(typeof(BasePatcher))) - return null; - } - catch (AssemblyResolutionException) - { - // Can happen if this type inherits a type from an assembly that can't be found. Safe to assume it's not a plugin. - return null; - } - - var metadata = PatcherPluginInfoAttribute.FromCecilType(type); - - // Perform checks that will prevent the plugin from being loaded in ALL cases - if (metadata == null) - { - Logger.LogWarning($"Skipping over type [{type.FullName}] as no metadata attribute is specified"); - return null; - } - - if (string.IsNullOrEmpty(metadata.GUID) || !allowedGuidRegex.IsMatch(metadata.GUID)) - { - Logger.LogWarning($"Skipping type [{type.FullName}] because its GUID [{metadata.GUID}] is of an illegal format"); - return null; - } - - if (metadata.Version == null) - { - Logger.LogWarning($"Skipping type [{type.FullName}] because its version is invalid"); - return null; - } - - if (metadata.Name == null) - { - Logger.LogWarning($"Skipping type [{type.FullName}] because its name is null"); - return null; - } - - return new PatcherPluginMetadata - { - TypeName = type.FullName - }; - } - - 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(BasePatcher).FullName)) - return false; - - return true; - } - - /// - /// Adds all patchers from all managed assemblies specified in a directory. - /// - /// Directory to search patcher DLLs from. - public void AddPatchersFromDirectory(string directory) - { - if (!Directory.Exists(directory)) - return; - - var sortedPatchers = new List(); - - var patchers = TypeLoader.FindPluginTypes(directory, ToPatcherPlugin, HasPatcherPlugins); - - // TODO: Add dependency ordering and process attribute filtering - - foreach (var keyValuePair in patchers) - { - 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 = (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 - .LogWarning($"Skipping method [{method.FullDescription()}] as it is not a valid patcher method"); - continue; - } - - void AddDefinition(PatchDefinition definition) - { - Logger.LogDebug($"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.LogError($"Failed to load patchers from type [{patcherPlugin.TypeName}]: {(e is ReflectionTypeLoadException re ? TypeLoader.TypeLoadExceptionToString(re) : e)}"); - } - - var assName = ass.GetName(); - Logger.Log(patcherCollection.Any() ? LogLevel.Info : LogLevel.Debug, - $"Loaded {patcherCollection.Count} patcher type{(patcherCollection.Count == 1 ? "" : "s")} from [{assName.Name} {assName.Version}]"); - } - - PatcherContext.PatchDefinitions.AddRange(sortedPatchers); - } - - - /// - /// Adds all .dll assemblies in given directories to be patched and loaded by this patcher instance. Non-managed assemblies - /// are skipped. - /// - /// The directories to search. - public void LoadAssemblyDirectories(params string[] directories) => - LoadAssemblyDirectories(directories, new[] { "dll" }); - - /// - /// Adds all assemblies in given directories to be patched and loaded by this patcher instance. Non-managed assemblies are - /// skipped. - /// - /// The directory to search. - /// The file extensions to attempt to load. - public void LoadAssemblyDirectories(IEnumerable directories, IEnumerable assemblyExtensions) - { - var filesToSearch = assemblyExtensions - .SelectMany(ext => Utility.GetUniqueFilesInDirectories(directories, $"*.{ext}")); - - foreach (var assemblyPath in filesToSearch) - { - if (!TryLoadAssembly(assemblyPath, out var assembly)) - continue; - - // NOTE: this is special cased here because the dependency handling for System.dll is a bit wonky - // System has an assembly reference to itself, and it also has a reference to Mono.Security causing a circular dependency - // It's also generally dangerous to change system.dll since so many things rely on it, - // and it's already loaded into the appdomain since this loader references it, so we might as well skip it - if (assembly.Name.Name == "System" - || assembly.Name.Name == - "mscorlib") //mscorlib is already loaded into the appdomain so it can't be patched - { - assembly.Dispose(); - continue; - } - - PatcherContext.AvailableAssemblies.Add(Path.GetFileName(assemblyPath), assembly); - - Logger.LogDebug($"Assembly loaded: {Path.GetFileName(assemblyPath)}"); - - //if (UnityPatches.AssemblyLocations.ContainsKey(assembly.FullName)) - //{ - // Logger.LogWarning($"Tried to load duplicate assembly {Path.GetFileName(assemblyPath)} from Managed folder! Skipping..."); - // continue; - //} - - //assemblies.Add(Path.GetFileName(assemblyPath), assembly); - //UnityPatches.AssemblyLocations.Add(assembly.FullName, Path.GetFullPath(assemblyPath)); - } - } - - /// - /// Attempts to load a managed assembly as an . Returns true if successful. - /// - /// The path of the assembly. - /// The loaded assembly. Null if not successful in loading. - public static bool TryLoadAssembly(string path, out AssemblyDefinition assembly) - { - try - { - assembly = AssemblyDefinition.ReadAssembly(path, TypeLoader.ReaderParameters); - return true; - } - catch (BadImageFormatException) - { - // Not a managed assembly - assembly = null; - return false; - } - } - - /// - /// Applies patchers to all assemblies loaded into this assembly patcher and then loads patched assemblies into memory. - /// - public void PatchAndLoad() - { - // First, create a copy of the assembly dictionary as the initializer can change them - var assemblies = - new Dictionary(PatcherContext.AvailableAssemblies, - StringComparer.InvariantCultureIgnoreCase); - - // Next, initialize all the patchers - foreach (var assemblyPatcher in PatcherPluginsSafe) + foreach (var patcherPlugin in patcherCollection) try { - assemblyPatcher.Initialize(); - } - catch (Exception ex) - { - Logger.LogError($"Failed to run initializer of {assemblyPatcher.Info.GUID}: {ex}"); - } + var type = ass.GetType(patcherPlugin.TypeName); - // Then, perform the actual patching + var instance = (BasePatcher) Activator.CreateInstance(type); + instance.Context = PatcherContext; - var patchedAssemblies = new HashSet(StringComparer.InvariantCultureIgnoreCase); - var resolvedAssemblies = new Dictionary(); + PatcherContext.PatcherPlugins.Add(instance); - // TODO: Maybe instead reload the assembly and repatch with other valid patchers? - var invalidAssemblies = new HashSet(StringComparer.InvariantCultureIgnoreCase); + var methods = + type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - Logger.LogMessage($"Executing {PatcherContext.PatchDefinitions.Count} patch(es)"); - - foreach (var patchDefinition in PatcherContext.PatchDefinitions.ToList()) - { - string targetDll = patchDefinition.TargetAssembly?.TargetAssembly ?? - patchDefinition.TargetType.TargetAssembly; - - bool isAssemblyPatch = patchDefinition.TargetAssembly != null; - - if (targetDll == TargetAssemblyAttribute.AllAssemblies) - { - foreach (var kv in PatcherContext.AvailableAssemblies.ToList()) + foreach (var method in methods) { - if (invalidAssemblies.Contains(kv.Key)) + var targetAssemblies = MetadataHelper.GetAttributes(method); + var targetTypes = MetadataHelper.GetAttributes(method); + + if (targetAssemblies.Length == 0 && targetTypes.Length == 0) continue; - RunPatcher(kv.Value, kv.Key); - } - } - else - { - if (!PatcherContext.AvailableAssemblies.TryGetValue(targetDll, out var assembly) - || invalidAssemblies.Contains(targetDll)) - continue; + var parameters = method.GetParameters(); - RunPatcher(assembly, targetDll); - } - - - bool RunPatcher(AssemblyDefinition assembly, string targetDll) - { - try - { - var arguments = new object[patchDefinition.MethodInfo.GetParameters().Length]; - - if (!isAssemblyPatch) + 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) + ) { - var targetType = - assembly.MainModule.Types.FirstOrDefault(x => x.FullName == - patchDefinition.TargetType.TargetType); - - if (targetType == null) - { - Logger - .LogWarning($"Unable to find type [{patchDefinition.TargetType.TargetType}] defined in {patchDefinition.MethodInfo.Name}. Skipping patcher"); //TODO: Proper name - return false; - } - - arguments[0] = targetType; - } - else - { - arguments[0] = assembly; - } - - if (arguments.Length > 1) - arguments[1] = targetDll; - - var result = patchDefinition.MethodInfo.Invoke(patchDefinition.Instance, arguments); - - if (patchDefinition.MethodInfo.ReturnType == typeof(void) - || patchDefinition.MethodInfo.ReturnType == typeof(bool) && (bool)result) - { - if (isAssemblyPatch) - { - assembly = (AssemblyDefinition)arguments[0]; - PatcherContext.AvailableAssemblies[targetDll] = assembly; - } - - patchedAssemblies.Add(targetDll); - } - - return true; - } - catch (Exception e) - { - Logger.LogError($"Failed to run [{patchDefinition.FullName}] when patching [{assembly.Name.Name}]. This assembly will not be patched. Error: {e}"); - patchedAssemblies.Remove(targetDll); - invalidAssemblies.Add(targetDll); - return false; - } - } - - - foreach (var resolvedAss in AppDomain.CurrentDomain.GetAssemblies()) - { - var name = Utility.TryParseAssemblyName(resolvedAss.FullName, out var assName) - ? assName.Name - : resolvedAss.FullName; - - // Report only the first type that caused the assembly to load, because any subsequent ones can be false positives - if (!resolvedAssemblies.ContainsKey(name)) - resolvedAssemblies[name] = patchDefinition.MethodInfo.DeclaringType.ToString(); - } - } - - // Check if any patched assemblies have been already resolved by the CLR - // If there are any, they cannot be loaded by the preloader - var patchedAssemblyNames = - new - HashSet(assemblies.Where(kv => patchedAssemblies.Contains(kv.Key)).Select(kv => kv.Value.Name.Name), - StringComparer.InvariantCultureIgnoreCase); - var earlyLoadAssemblies = resolvedAssemblies.Where(kv => patchedAssemblyNames.Contains(kv.Key)).ToList(); - - if (earlyLoadAssemblies.Count != 0) - Logger.LogWarning(new StringBuilder() - .AppendLine("The following assemblies have been loaded too early and will not be patched by preloader:") - .AppendLine(string.Join(Environment.NewLine, - earlyLoadAssemblies - .Select(kv => - $"* [{kv.Key}] (first loaded by [{kv.Value}])") - .ToArray())) - .AppendLine("Expect unexpected behavior and issues with plugins and patchers not being loaded.") - .ToString()); - - var dumpedAssemblyPaths = new Dictionary(); - // Finally, load patched assemblies into memory - if (ConfigDumpAssemblies.Value || ConfigLoadDumpedAssemblies.Value) - { - if (!Directory.Exists(PatcherContext.DumpedAssembliesPath)) - Directory.CreateDirectory(PatcherContext.DumpedAssembliesPath); - - foreach (var kv in assemblies) - { - var filename = kv.Key; - var name = Path.GetFileNameWithoutExtension(filename); - var ext = Path.GetExtension(filename); - var assembly = kv.Value; - - if (!patchedAssemblies.Contains(filename)) - continue; - for (var i = 0;; i++) - { - var postfix = i > 0 ? $"_{i}" : ""; - var path = Path.Combine(PatcherContext.DumpedAssembliesPath, $"{name}{postfix}{ext}"); - if (!Utility.TryOpenFileStream(path, FileMode.Create, out var fs)) + Logger + .LogWarning($"Skipping method [{method.FullDescription()}] as it is not a valid patcher method"); continue; - assembly.Write(fs); - fs.Dispose(); - dumpedAssemblyPaths[filename] = path; - break; + } + + void AddDefinition(PatchDefinition definition) + { + Logger.LogDebug($"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.LogError($"Failed to load patchers from type [{patcherPlugin.TypeName}]: {(e is ReflectionTypeLoadException re ? TypeLoader.TypeLoadExceptionToString(re) : e)}"); + } + + var assName = ass.GetName(); + Logger.Log(patcherCollection.Any() ? LogLevel.Info : LogLevel.Debug, + $"Loaded {patcherCollection.Count} patcher type{(patcherCollection.Count == 1 ? "" : "s")} from [{assName.Name} {assName.Version}]"); + } + + PatcherContext.PatchDefinitions.AddRange(sortedPatchers); + } + + + /// + /// Adds all .dll assemblies in given directories to be patched and loaded by this patcher instance. Non-managed assemblies + /// are skipped. + /// + /// The directories to search. + public void LoadAssemblyDirectories(params string[] directories) => + LoadAssemblyDirectories(directories, new[] { "dll" }); + + /// + /// Adds all assemblies in given directories to be patched and loaded by this patcher instance. Non-managed assemblies are + /// skipped. + /// + /// The directory to search. + /// The file extensions to attempt to load. + public void LoadAssemblyDirectories(IEnumerable directories, IEnumerable assemblyExtensions) + { + var filesToSearch = assemblyExtensions + .SelectMany(ext => Utility.GetUniqueFilesInDirectories(directories, $"*.{ext}")); + + foreach (var assemblyPath in filesToSearch) + { + if (!TryLoadAssembly(assemblyPath, out var assembly)) + continue; + + // NOTE: this is special cased here because the dependency handling for System.dll is a bit wonky + // System has an assembly reference to itself, and it also has a reference to Mono.Security causing a circular dependency + // It's also generally dangerous to change system.dll since so many things rely on it, + // and it's already loaded into the appdomain since this loader references it, so we might as well skip it + if (assembly.Name.Name == "System" + || assembly.Name.Name == + "mscorlib") //mscorlib is already loaded into the appdomain so it can't be patched + { + assembly.Dispose(); + continue; + } + + PatcherContext.AvailableAssemblies.Add(Path.GetFileName(assemblyPath), assembly); + + Logger.LogDebug($"Assembly loaded: {Path.GetFileName(assemblyPath)}"); + + //if (UnityPatches.AssemblyLocations.ContainsKey(assembly.FullName)) + //{ + // Logger.LogWarning($"Tried to load duplicate assembly {Path.GetFileName(assemblyPath)} from Managed folder! Skipping..."); + // continue; + //} + + //assemblies.Add(Path.GetFileName(assemblyPath), assembly); + //UnityPatches.AssemblyLocations.Add(assembly.FullName, Path.GetFullPath(assemblyPath)); + } + } + + /// + /// Attempts to load a managed assembly as an . Returns true if successful. + /// + /// The path of the assembly. + /// The loaded assembly. Null if not successful in loading. + public static bool TryLoadAssembly(string path, out AssemblyDefinition assembly) + { + try + { + assembly = AssemblyDefinition.ReadAssembly(path, TypeLoader.ReaderParameters); + return true; + } + catch (BadImageFormatException) + { + // Not a managed assembly + assembly = null; + return false; + } + } + + /// + /// Applies patchers to all assemblies loaded into this assembly patcher and then loads patched assemblies into memory. + /// + public void PatchAndLoad() + { + // First, create a copy of the assembly dictionary as the initializer can change them + var assemblies = + new Dictionary(PatcherContext.AvailableAssemblies, + StringComparer.InvariantCultureIgnoreCase); + + // Next, initialize all the patchers + foreach (var assemblyPatcher in PatcherPluginsSafe) + try + { + assemblyPatcher.Initialize(); + } + catch (Exception ex) + { + Logger.LogError($"Failed to run initializer of {assemblyPatcher.Info.GUID}: {ex}"); + } + + // Then, perform the actual patching + + var patchedAssemblies = new HashSet(StringComparer.InvariantCultureIgnoreCase); + var resolvedAssemblies = new Dictionary(); + + // TODO: Maybe instead reload the assembly and repatch with other valid patchers? + var invalidAssemblies = new HashSet(StringComparer.InvariantCultureIgnoreCase); + + Logger.LogMessage($"Executing {PatcherContext.PatchDefinitions.Count} patch(es)"); + + foreach (var patchDefinition in PatcherContext.PatchDefinitions.ToList()) + { + var targetDll = patchDefinition.TargetAssembly?.TargetAssembly ?? + patchDefinition.TargetType.TargetAssembly; + + var isAssemblyPatch = patchDefinition.TargetAssembly != null; + + if (targetDll == TargetAssemblyAttribute.AllAssemblies) + { + foreach (var kv in PatcherContext.AvailableAssemblies.ToList()) + { + if (invalidAssemblies.Contains(kv.Key)) + continue; + + RunPatcher(kv.Value, kv.Key); + } + } + else + { + if (!PatcherContext.AvailableAssemblies.TryGetValue(targetDll, out var assembly) + || invalidAssemblies.Contains(targetDll)) + continue; + + RunPatcher(assembly, targetDll); + } + + + bool RunPatcher(AssemblyDefinition assembly, string targetDll) + { + try + { + var arguments = new object[patchDefinition.MethodInfo.GetParameters().Length]; + + if (!isAssemblyPatch) + { + var targetType = + assembly.MainModule.Types.FirstOrDefault(x => x.FullName == + patchDefinition.TargetType.TargetType); + + if (targetType == null) + { + Logger + .LogWarning($"Unable to find type [{patchDefinition.TargetType.TargetType}] defined in {patchDefinition.MethodInfo.Name}. Skipping patcher"); //TODO: Proper name + return false; + } + + arguments[0] = targetType; + } + else + { + arguments[0] = assembly; + } + + if (arguments.Length > 1) + arguments[1] = targetDll; + + var result = patchDefinition.MethodInfo.Invoke(patchDefinition.Instance, arguments); + + if (patchDefinition.MethodInfo.ReturnType == typeof(void) + || patchDefinition.MethodInfo.ReturnType == typeof(bool) && (bool) result) + { + if (isAssemblyPatch) + { + assembly = (AssemblyDefinition) arguments[0]; + PatcherContext.AvailableAssemblies[targetDll] = assembly; + } + + patchedAssemblies.Add(targetDll); + } + + return true; + } + catch (Exception e) + { + Logger.LogError($"Failed to run [{patchDefinition.FullName}] when patching [{assembly.Name.Name}]. This assembly will not be patched. Error: {e}"); + patchedAssemblies.Remove(targetDll); + invalidAssemblies.Add(targetDll); + return false; + } } - if (ConfigBreakBeforeLoadAssemblies.Value) + + foreach (var resolvedAss in AppDomain.CurrentDomain.GetAssemblies()) { - Logger.LogInfo($"BepInEx is about load the following assemblies:\n{string.Join("\n", patchedAssemblies.ToArray())}"); - Logger.LogInfo($"The assemblies were dumped into {PatcherContext.DumpedAssembliesPath}"); - Logger.LogInfo("Load any assemblies into the debugger, set breakpoints and continue execution."); - Debugger.Break(); + var name = Utility.TryParseAssemblyName(resolvedAss.FullName, out var assName) + ? assName.Name + : resolvedAss.FullName; + + // Report only the first type that caused the assembly to load, because any subsequent ones can be false positives + if (!resolvedAssemblies.ContainsKey(name)) + resolvedAssemblies[name] = patchDefinition.MethodInfo.DeclaringType.ToString(); } + } + + // Check if any patched assemblies have been already resolved by the CLR + // If there are any, they cannot be loaded by the preloader + var patchedAssemblyNames = + new + HashSet(assemblies.Where(kv => patchedAssemblies.Contains(kv.Key)).Select(kv => kv.Value.Name.Name), + StringComparer.InvariantCultureIgnoreCase); + var earlyLoadAssemblies = resolvedAssemblies.Where(kv => patchedAssemblyNames.Contains(kv.Key)).ToList(); + + if (earlyLoadAssemblies.Count != 0) + Logger.LogWarning(new StringBuilder() + .AppendLine("The following assemblies have been loaded too early and will not be patched by preloader:") + .AppendLine(string.Join(Environment.NewLine, + earlyLoadAssemblies + .Select(kv => + $"* [{kv.Key}] (first loaded by [{kv.Value}])") + .ToArray())) + .AppendLine("Expect unexpected behavior and issues with plugins and patchers not being loaded.") + .ToString()); + + var dumpedAssemblyPaths = new Dictionary(); + // Finally, load patched assemblies into memory + if (ConfigDumpAssemblies.Value || ConfigLoadDumpedAssemblies.Value) + { + if (!Directory.Exists(PatcherContext.DumpedAssembliesPath)) + Directory.CreateDirectory(PatcherContext.DumpedAssembliesPath); foreach (var kv in assemblies) { var filename = kv.Key; + var name = Path.GetFileNameWithoutExtension(filename); + var ext = Path.GetExtension(filename); var assembly = kv.Value; - // Note that since we only *load* assemblies, they shouldn't trigger dependency loading - // Not loading all assemblies is very important not only because of memory reasons, - // but because some games *rely* on that because of messed up internal dependencies. - if (patchedAssemblies.Contains(filename)) + if (!patchedAssemblies.Contains(filename)) + continue; + for (var i = 0;; i++) { - Assembly loadedAssembly; - - if (ConfigLoadDumpedAssemblies.Value && - dumpedAssemblyPaths.TryGetValue(filename, out var dumpedAssemblyPath)) - loadedAssembly = Assembly.LoadFrom(dumpedAssemblyPath); - else - { - using var assemblyStream = new MemoryStream(); - assembly.Write(assemblyStream); - loadedAssembly = Assembly.Load(assemblyStream.ToArray()); - } - - PatcherContext.LoadedAssemblies.Add(filename, loadedAssembly); - - Logger.LogDebug($"Loaded '{assembly.FullName}' into memory"); + var postfix = i > 0 ? $"_{i}" : ""; + var path = Path.Combine(PatcherContext.DumpedAssembliesPath, $"{name}{postfix}{ext}"); + if (!Utility.TryOpenFileStream(path, FileMode.Create, out var fs)) + continue; + assembly.Write(fs); + fs.Dispose(); + dumpedAssemblyPaths[filename] = path; + break; } - - // Though we have to dispose of all assemblies regardless of them being patched or not - assembly.Dispose(); } - - // Finally, run all finalizers - foreach (var assemblyPatcher in PatcherPluginsSafe) - try - { - assemblyPatcher.Finalizer(); - } - catch (Exception ex) - { - Logger.LogError($"Failed to run finalizer of {assemblyPatcher.Info.GUID}: {ex}"); - } } - #region Config + if (ConfigBreakBeforeLoadAssemblies.Value) + { + Logger.LogInfo($"BepInEx is about load the following assemblies:\n{string.Join("\n", patchedAssemblies.ToArray())}"); + Logger.LogInfo($"The assemblies were dumped into {PatcherContext.DumpedAssembliesPath}"); + Logger.LogInfo("Load any assemblies into the debugger, set breakpoints and continue execution."); + Debugger.Break(); + } - private static readonly ConfigEntry ConfigDumpAssemblies = ConfigFile.CoreConfig.Bind( - "Preloader", "DumpAssemblies", - false, - "If enabled, BepInEx will save patched assemblies into BepInEx/DumpedAssemblies.\nThis can be used by developers to inspect and debug preloader patchers."); + foreach (var kv in assemblies) + { + var filename = kv.Key; + var assembly = kv.Value; - private static readonly ConfigEntry ConfigLoadDumpedAssemblies = ConfigFile.CoreConfig.Bind( - "Preloader", "LoadDumpedAssemblies", - false, - "If enabled, BepInEx will load patched assemblies from BepInEx/DumpedAssemblies instead of memory.\nThis can be used to be able to load patched assemblies into debuggers like dnSpy.\nIf set to true, will override DumpAssemblies."); + // Note that since we only *load* assemblies, they shouldn't trigger dependency loading + // Not loading all assemblies is very important not only because of memory reasons, + // but because some games *rely* on that because of messed up internal dependencies. + if (patchedAssemblies.Contains(filename)) + { + Assembly loadedAssembly; - private static readonly ConfigEntry ConfigBreakBeforeLoadAssemblies = ConfigFile.CoreConfig.Bind( - "Preloader", "BreakBeforeLoadAssemblies", - false, - "If enabled, BepInEx will call Debugger.Break() once before loading patched assemblies.\nThis can be used with debuggers like dnSpy to install breakpoints into patched assemblies before they are loaded."); + if (ConfigLoadDumpedAssemblies.Value && + dumpedAssemblyPaths.TryGetValue(filename, out var dumpedAssemblyPath)) + { + loadedAssembly = Assembly.LoadFrom(dumpedAssemblyPath); + } + else + { + using var assemblyStream = new MemoryStream(); + assembly.Write(assemblyStream); + loadedAssembly = Assembly.Load(assemblyStream.ToArray()); + } - #endregion + PatcherContext.LoadedAssemblies.Add(filename, loadedAssembly); + + Logger.LogDebug($"Loaded '{assembly.FullName}' into memory"); + } + + // Though we have to dispose of all assemblies regardless of them being patched or not + assembly.Dispose(); + } + + // Finally, run all finalizers + foreach (var assemblyPatcher in PatcherPluginsSafe) + try + { + assemblyPatcher.Finalizer(); + } + catch (Exception ex) + { + Logger.LogError($"Failed to run finalizer of {assemblyPatcher.Info.GUID}: {ex}"); + } } + + #region Config + + private static readonly ConfigEntry ConfigDumpAssemblies = ConfigFile.CoreConfig.Bind( + "Preloader", "DumpAssemblies", + false, + "If enabled, BepInEx will save patched assemblies into BepInEx/DumpedAssemblies.\nThis can be used by developers to inspect and debug preloader patchers."); + + private static readonly ConfigEntry ConfigLoadDumpedAssemblies = ConfigFile.CoreConfig.Bind( + "Preloader", "LoadDumpedAssemblies", + false, + "If enabled, BepInEx will load patched assemblies from BepInEx/DumpedAssemblies instead of memory.\nThis can be used to be able to load patched assemblies into debuggers like dnSpy.\nIf set to true, will override DumpAssemblies."); + + private static readonly ConfigEntry ConfigBreakBeforeLoadAssemblies = ConfigFile.CoreConfig.Bind( + "Preloader", "BreakBeforeLoadAssemblies", + false, + "If enabled, BepInEx will call Debugger.Break() once before loading patched assemblies.\nThis can be used with debuggers like dnSpy to install breakpoints into patched assemblies before they are loaded."); + + #endregion } diff --git a/BepInEx.Preloader.Core/Patching/Attributes.cs b/BepInEx.Preloader.Core/Patching/Attributes.cs index d676457..b9a5c4c 100644 --- a/BepInEx.Preloader.Core/Patching/Attributes.cs +++ b/BepInEx.Preloader.Core/Patching/Attributes.cs @@ -3,127 +3,126 @@ using System.Linq; using Mono.Cecil; using Version = SemanticVersioning.Version; -namespace BepInEx.Preloader.Core.Patching +namespace BepInEx.Preloader.Core.Patching; + +/// +/// This attribute denotes that a class is a patcher plugin, and specifies the required metadata. +/// +[AttributeUsage(AttributeTargets.Class)] +public class PatcherPluginInfoAttribute : Attribute { - /// - /// This attribute denotes that a class is a patcher plugin, and specifies the required metadata. - /// - [AttributeUsage(AttributeTargets.Class)] - public class PatcherPluginInfoAttribute : Attribute + /// 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 PatcherPluginInfoAttribute(string GUID, string Name, string Version) { - /// 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 PatcherPluginInfoAttribute(string GUID, string Name, string Version) + this.GUID = GUID; + this.Name = Name; + this.Version = TryParseLongVersion(Version); + } + + /// + /// The unique identifier of the plugin. Should not change between plugin versions. + /// + public string GUID { get; protected set; } + + + /// + /// The user friendly name of the plugin. Is able to be changed between versions. + /// + public string Name { get; protected set; } + + + /// + /// The specific version of the plugin. + /// + public Version Version { get; protected set; } + + private static Version TryParseLongVersion(string version) + { + if (Version.TryParse(version, out var v)) + return v; + + // no System.Version.TryParse() on .NET 3.5 + try { - this.GUID = GUID; - this.Name = Name; - this.Version = TryParseLongVersion(Version); + var longVersion = new System.Version(version); + + return new Version(longVersion.Major, longVersion.Minor, + longVersion.Build != -1 ? longVersion.Build : 0); } + catch { } - /// - /// The unique identifier of the plugin. Should not change between plugin versions. - /// - public string GUID { get; protected set; } + return null; + } + internal static PatcherPluginInfoAttribute FromCecilType(TypeDefinition td) + { + var attr = MetadataHelper.GetCustomAttributes(td, false).FirstOrDefault(); - /// - /// The user friendly name of the plugin. Is able to be changed between versions. - /// - public string Name { get; protected set; } - - - /// - /// The specific version of the plugin. - /// - public Version Version { get; protected set; } - - private static Version TryParseLongVersion(string version) - { - if (Version.TryParse(version, out var v)) - return v; - - // no System.Version.TryParse() on .NET 3.5 - try - { - var longVersion = new System.Version(version); - - return new Version(longVersion.Major, longVersion.Minor, - longVersion.Build != -1 ? longVersion.Build : 0); - } - catch { } - + if (attr == null) return null; - } - internal static PatcherPluginInfoAttribute FromCecilType(TypeDefinition td) - { - var attr = MetadataHelper.GetCustomAttributes(td, false).FirstOrDefault(); - - if (attr == null) - return null; - - return new PatcherPluginInfoAttribute((string)attr.ConstructorArguments[0].Value, - (string)attr.ConstructorArguments[1].Value, - (string)attr.ConstructorArguments[2].Value); - } - - internal static PatcherPluginInfoAttribute FromType(Type type) - { - var attributes = type.GetCustomAttributes(typeof(PatcherPluginInfoAttribute), false); - - if (attributes.Length == 0) - return null; - - return (PatcherPluginInfoAttribute)attributes[0]; - } + return new PatcherPluginInfoAttribute((string) attr.ConstructorArguments[0].Value, + (string) attr.ConstructorArguments[1].Value, + (string) attr.ConstructorArguments[2].Value); } - /// - /// Defines an assembly that a patch method will target. - /// - [AttributeUsage(AttributeTargets.Method)] - public class TargetAssemblyAttribute : Attribute + internal static PatcherPluginInfoAttribute FromType(Type type) { - /// - /// Marker used to indicate all possible assemblies to be targeted by a patch method. - /// - public const string AllAssemblies = "_all"; + var attributes = type.GetCustomAttributes(typeof(PatcherPluginInfoAttribute), false); - /// The short filename of the assembly. Use to mark all possible assemblies as targets. - public TargetAssemblyAttribute(string targetAssembly) - { - TargetAssembly = targetAssembly; - } + if (attributes.Length == 0) + return null; - /// - /// The short filename of the assembly to target. - /// - public string TargetAssembly { get; } - } - - /// - /// Defines a type that a patch method will target. - /// - [AttributeUsage(AttributeTargets.Method)] - public class TargetTypeAttribute : Attribute - { - /// The short filename of the assembly of which belongs to. - /// The full name of the type to target for patching. - public TargetTypeAttribute(string targetAssembly, string targetType) - { - TargetAssembly = targetAssembly; - TargetType = targetType; - } - - /// - /// The short filename of the assembly to target. - /// - public string TargetAssembly { get; } - - /// - /// The full name of the type to target for patching. - /// - public string TargetType { get; } + return (PatcherPluginInfoAttribute) attributes[0]; } } + +/// +/// Defines an assembly that a patch method will target. +/// +[AttributeUsage(AttributeTargets.Method)] +public class TargetAssemblyAttribute : Attribute +{ + /// + /// Marker used to indicate all possible assemblies to be targeted by a patch method. + /// + public const string AllAssemblies = "_all"; + + /// The short filename of the assembly. Use to mark all possible assemblies as targets. + public TargetAssemblyAttribute(string targetAssembly) + { + TargetAssembly = targetAssembly; + } + + /// + /// The short filename of the assembly to target. + /// + public string TargetAssembly { get; } +} + +/// +/// Defines a type that a patch method will target. +/// +[AttributeUsage(AttributeTargets.Method)] +public class TargetTypeAttribute : Attribute +{ + /// The short filename of the assembly of which belongs to. + /// The full name of the type to target for patching. + public TargetTypeAttribute(string targetAssembly, string targetType) + { + TargetAssembly = targetAssembly; + TargetType = targetType; + } + + /// + /// The short filename of the assembly to target. + /// + public string TargetAssembly { get; } + + /// + /// The full name of the type to target for patching. + /// + public string TargetType { get; } +} diff --git a/BepInEx.Preloader.Core/Patching/BasePatcher.cs b/BepInEx.Preloader.Core/Patching/BasePatcher.cs index 6910402..bc16cd0 100644 --- a/BepInEx.Preloader.Core/Patching/BasePatcher.cs +++ b/BepInEx.Preloader.Core/Patching/BasePatcher.cs @@ -1,51 +1,50 @@ using BepInEx.Configuration; using BepInEx.Logging; -namespace BepInEx.Preloader.Core.Patching +namespace BepInEx.Preloader.Core.Patching; + +/// +/// A patcher that can contain multiple methods for patching assemblies. +/// +public abstract class BasePatcher { /// - /// A patcher that can contain multiple methods for patching assemblies. + /// A instance created for use by this patcher plugin. /// - public abstract class BasePatcher + public ManualLogSource Log { get; } + + /// + /// A configuration file binding created with the of this plugin as the filename. + /// + public ConfigFile Config { get; } + + /// + /// Metadata associated with this patcher plugin. + /// + public PatcherPluginInfoAttribute Info { get; } + + /// + /// The context of the this BasePatcher is associated with. + /// + public PatcherContext Context { get; set; } + + protected BasePatcher() { - /// - /// A instance created for use by this patcher plugin. - /// - public ManualLogSource Log { get; } - - /// - /// A configuration file binding created with the of this plugin as the filename. - /// - public ConfigFile Config { get; } + Info = PatcherPluginInfoAttribute.FromType(GetType()); - /// - /// Metadata associated with this patcher plugin. - /// - public PatcherPluginInfoAttribute Info { get; } + Log = Logger.CreateLogSource(Info.Name); - /// - /// The context of the this BasePatcher is associated with. - /// - public PatcherContext Context { get; set; } - - protected BasePatcher() - { - 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())); - } - - /// - /// Executed before any patches from any plugin are applied. - /// - public virtual void Initialize() { } - - /// - /// Executed after all patches from all plugins have been applied. - /// - public virtual void Finalizer() { } + Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, Info.GUID + ".cfg"), false, + new BepInPlugin(Info.GUID, Info.Name, Info.Version.ToString())); } + + /// + /// Executed before any patches from any plugin are applied. + /// + public virtual void Initialize() { } + + /// + /// Executed after all patches from all plugins have been applied. + /// + public virtual void Finalizer() { } } diff --git a/BepInEx.Preloader.Core/Patching/PatcherContext.cs b/BepInEx.Preloader.Core/Patching/PatcherContext.cs index 6579855..1331bea 100644 --- a/BepInEx.Preloader.Core/Patching/PatcherContext.cs +++ b/BepInEx.Preloader.Core/Patching/PatcherContext.cs @@ -2,97 +2,96 @@ using System.Collections.Generic; using System.Reflection; using Mono.Cecil; -namespace BepInEx.Preloader.Core.Patching +namespace BepInEx.Preloader.Core.Patching; + +/// +/// A definition of an individual patch for use by . +/// +public class PatchDefinition +{ + public PatchDefinition(TargetAssemblyAttribute targetAssembly, BasePatcher instance, MethodInfo methodInfo) + { + TargetAssembly = targetAssembly; + Instance = instance; + MethodInfo = methodInfo; + + FullName = $"{MethodInfo.DeclaringType.FullName}/{MethodInfo.Name} -> {TargetAssembly.TargetAssembly}"; + } + + public PatchDefinition(TargetTypeAttribute targetType, BasePatcher instance, MethodInfo methodInfo) + { + TargetType = targetType; + Instance = instance; + MethodInfo = methodInfo; + + FullName = + $"{MethodInfo.DeclaringType.FullName}/{MethodInfo.Name} -> {TargetType.TargetAssembly}/{TargetType.TargetType}"; + } + + /// + /// The assembly / assemblies this patch will target, if there any. + /// + public TargetAssemblyAttribute TargetAssembly { get; } = null; + + /// + /// The type / types this patch will target, if there are any. + /// + public TargetTypeAttribute TargetType { get; } = null; + + /// + /// The instance of the this originates from. + /// + public BasePatcher Instance { get; } + + /// + /// The method that will perform the patching logic defined by this instance. + /// + public MethodInfo MethodInfo { get; } + + /// + /// A friendly name for this patch definition, for use in logging and error tracking. + /// + public string FullName { get; } +} + +/// +/// Context provided to patcher plugins from the associated patcher engine. +/// +public class PatcherContext { /// - /// A definition of an individual patch for use by . + /// Contains a list of assemblies that will be patched and loaded into the runtime. + /// + /// The dictionary has the name of the file, without any directories. These are used by the dumping + /// functionality, and as such, these are also required to be unique. They do not have to be exactly the same as + /// the real filename, however they have to be mapped deterministically. + /// + /// Order is not respected, as it will be sorted by dependencies. /// - public class PatchDefinition - { - public PatchDefinition(TargetAssemblyAttribute targetAssembly, BasePatcher instance, MethodInfo methodInfo) - { - TargetAssembly = targetAssembly; - Instance = instance; - MethodInfo = methodInfo; - - FullName = $"{MethodInfo.DeclaringType.FullName}/{MethodInfo.Name} -> {TargetAssembly.TargetAssembly}"; - } - - public PatchDefinition(TargetTypeAttribute targetType, BasePatcher instance, MethodInfo methodInfo) - { - TargetType = targetType; - Instance = instance; - MethodInfo = methodInfo; - - FullName = - $"{MethodInfo.DeclaringType.FullName}/{MethodInfo.Name} -> {TargetType.TargetAssembly}/{TargetType.TargetType}"; - } - - /// - /// The assembly / assemblies this patch will target, if there any. - /// - public TargetAssemblyAttribute TargetAssembly { get; } = null; - - /// - /// The type / types this patch will target, if there are any. - /// - public TargetTypeAttribute TargetType { get; } = null; - - /// - /// The instance of the this originates from. - /// - public BasePatcher Instance { get; } - - /// - /// The method that will perform the patching logic defined by this instance. - /// - public MethodInfo MethodInfo { get; } - - /// - /// A friendly name for this patch definition, for use in logging and error tracking. - /// - public string FullName { get; } - } + public Dictionary AvailableAssemblies { get; } = new(); /// - /// Context provided to patcher plugins from the associated patcher engine. + /// Contains a dictionary of assemblies that have been loaded as part of executing this assembly patcher. + /// + /// The key is the same key as used in , while the value is the actual assembly + /// itself. + /// /// - public class PatcherContext - { - /// - /// Contains a list of assemblies that will be patched and loaded into the runtime. - /// - /// The dictionary has the name of the file, without any directories. These are used by the dumping - /// functionality, and as such, these are also required to be unique. They do not have to be exactly the same as - /// the real filename, however they have to be mapped deterministically. - /// - /// Order is not respected, as it will be sorted by dependencies. - /// - public Dictionary AvailableAssemblies { get; } = new(); + public Dictionary LoadedAssemblies { get; } = new(); - /// - /// Contains a dictionary of assemblies that have been loaded as part of executing this assembly patcher. - /// - /// The key is the same key as used in , while the value is the actual assembly - /// itself. - /// - /// - public Dictionary LoadedAssemblies { get; } = new(); + /// + /// A list of plugins that will be initialized and executed, in the order of the list. + /// + public List PatcherPlugins { get; } = new(); - /// - /// A list of plugins that will be initialized and executed, in the order of the list. - /// - public List PatcherPlugins { get; } = new(); + /// + /// A list of individual patches that will execute, generated by parsing . + /// + public List PatchDefinitions { get; } = new(); - /// - /// A list of individual patches that will execute, generated by parsing . - /// - public List PatchDefinitions { get; } = new(); - - /// - /// The directory location as to where patched assemblies will be saved to and loaded from disk, for debugging - /// purposes. Defaults to BepInEx/DumpedAssemblies/ - /// - public string DumpedAssembliesPath { get; internal set; } - } + /// + /// The directory location as to where patched assemblies will be saved to and loaded from disk, for debugging + /// purposes. Defaults to BepInEx/DumpedAssemblies/ + /// + public string DumpedAssembliesPath { get; internal set; } } diff --git a/BepInEx.Preloader.Core/Patching/PatcherPluginMetadata.cs b/BepInEx.Preloader.Core/Patching/PatcherPluginMetadata.cs index fcdc63c..6994dea 100644 --- a/BepInEx.Preloader.Core/Patching/PatcherPluginMetadata.cs +++ b/BepInEx.Preloader.Core/Patching/PatcherPluginMetadata.cs @@ -1,22 +1,21 @@ using System.IO; using BepInEx.Bootstrap; -namespace BepInEx.Preloader.Core.Patching +namespace BepInEx.Preloader.Core.Patching; + +/// +/// A single cached assembly patcher. +/// +internal class PatcherPluginMetadata : ICacheable { /// - /// A single cached assembly patcher. + /// Type name of the patcher. /// - internal class PatcherPluginMetadata : ICacheable - { - /// - /// Type name of the patcher. - /// - public string TypeName { get; set; } = string.Empty; + public string TypeName { get; set; } = string.Empty; - /// - public void Save(BinaryWriter bw) => bw.Write(TypeName); + /// + public void Save(BinaryWriter bw) => bw.Write(TypeName); - /// - public void Load(BinaryReader br) => TypeName = br.ReadString(); - } + /// + public void Load(BinaryReader br) => TypeName = br.ReadString(); } diff --git a/BepInEx.Preloader.Core/PlatformUtils.cs b/BepInEx.Preloader.Core/PlatformUtils.cs index b4a2aba..21a0252 100644 --- a/BepInEx.Preloader.Core/PlatformUtils.cs +++ b/BepInEx.Preloader.Core/PlatformUtils.cs @@ -4,133 +4,132 @@ using System.Reflection; using System.Runtime.InteropServices; using MonoMod.Utils; -namespace BepInEx.Preloader.Core +namespace BepInEx.Preloader.Core; + +internal static class PlatformUtils { - internal static class PlatformUtils + [DllImport("libc.so.6", EntryPoint = "uname", CallingConvention = CallingConvention.Cdecl, + CharSet = CharSet.Ansi)] + private static extern IntPtr uname_linux(ref utsname_linux utsname); + + [DllImport("/usr/lib/libSystem.dylib", EntryPoint = "uname", CallingConvention = CallingConvention.Cdecl, + CharSet = CharSet.Ansi)] + private static extern IntPtr uname_osx(ref utsname_osx utsname); + + /// + /// Recreation of MonoMod's PlatformHelper.DeterminePlatform method, but with libc calls instead of creating processes. + /// + public static void SetPlatform() { - [DllImport("libc.so.6", EntryPoint = "uname", CallingConvention = CallingConvention.Cdecl, - CharSet = CharSet.Ansi)] - private static extern IntPtr uname_linux(ref utsname_linux utsname); + var current = Platform.Unknown; - [DllImport("/usr/lib/libSystem.dylib", EntryPoint = "uname", CallingConvention = CallingConvention.Cdecl, - CharSet = CharSet.Ansi)] - private static extern IntPtr uname_osx(ref utsname_osx utsname); + // For old Mono, get from a private property to accurately get the platform. + // static extern PlatformID Platform + var p_Platform = typeof(Environment).GetProperty("Platform", BindingFlags.NonPublic | BindingFlags.Static); + string platID; + if (p_Platform != null) + platID = p_Platform.GetValue(null, new object[0]).ToString(); + else + // For .NET and newer Mono, use the usual value. + platID = Environment.OSVersion.Platform.ToString(); + platID = platID.ToLowerInvariant(); - /// - /// Recreation of MonoMod's PlatformHelper.DeterminePlatform method, but with libc calls instead of creating processes. - /// - public static void SetPlatform() + if (platID.Contains("win")) + current = Platform.Windows; + else if (platID.Contains("mac") || platID.Contains("osx")) + current = Platform.MacOS; + else if (platID.Contains("lin") || platID.Contains("unix")) current = Platform.Linux; + + if (Is(current, Platform.Linux) && Directory.Exists("/data") && File.Exists("/system/build.prop")) + current = Platform.Android; + else if (Is(current, Platform.Unix) && Directory.Exists("/System/Library/AccessibilityBundles")) + current = Platform.iOS; + + // Is64BitOperatingSystem has been added in .NET Framework 4.0 + var m_get_Is64BitOperatingSystem = + typeof(Environment).GetProperty("Is64BitOperatingSystem")?.GetGetMethod(); + if (m_get_Is64BitOperatingSystem != null) + current |= (bool) m_get_Is64BitOperatingSystem.Invoke(null, new object[0]) ? Platform.Bits64 : 0; + else + current |= IntPtr.Size >= 8 ? Platform.Bits64 : 0; + + if ((Is(current, Platform.MacOS) || Is(current, Platform.Linux)) && Type.GetType("Mono.Runtime") != null) { - var current = Platform.Unknown; + string arch; + IntPtr result; - // For old Mono, get from a private property to accurately get the platform. - // static extern PlatformID Platform - var p_Platform = typeof(Environment).GetProperty("Platform", BindingFlags.NonPublic | BindingFlags.Static); - string platID; - if (p_Platform != null) - platID = p_Platform.GetValue(null, new object[0]).ToString(); - else - // For .NET and newer Mono, use the usual value. - platID = Environment.OSVersion.Platform.ToString(); - platID = platID.ToLowerInvariant(); - - if (platID.Contains("win")) - current = Platform.Windows; - else if (platID.Contains("mac") || platID.Contains("osx")) - current = Platform.MacOS; - else if (platID.Contains("lin") || platID.Contains("unix")) current = Platform.Linux; - - if (Is(current, Platform.Linux) && Directory.Exists("/data") && File.Exists("/system/build.prop")) - current = Platform.Android; - else if (Is(current, Platform.Unix) && Directory.Exists("/System/Library/AccessibilityBundles")) - current = Platform.iOS; - - // Is64BitOperatingSystem has been added in .NET Framework 4.0 - var m_get_Is64BitOperatingSystem = - typeof(Environment).GetProperty("Is64BitOperatingSystem")?.GetGetMethod(); - if (m_get_Is64BitOperatingSystem != null) - current |= (bool) m_get_Is64BitOperatingSystem.Invoke(null, new object[0]) ? Platform.Bits64 : 0; - else - current |= IntPtr.Size >= 8 ? Platform.Bits64 : 0; - - if ((Is(current, Platform.MacOS) || Is(current, Platform.Linux)) && Type.GetType("Mono.Runtime") != null) + if (Is(current, Platform.MacOS)) { - string arch; - IntPtr result; - - if (Is(current, Platform.MacOS)) - { - var utsname_osx = new utsname_osx(); - result = uname_osx(ref utsname_osx); - arch = utsname_osx.machine; - } - else - { - // Linux - var utsname_linux = new utsname_linux(); - result = uname_linux(ref utsname_linux); - arch = utsname_linux.machine; - } - - if (result == IntPtr.Zero && (arch.StartsWith("aarch") || arch.StartsWith("arm"))) - current |= Platform.ARM; + var utsname_osx = new utsname_osx(); + result = uname_osx(ref utsname_osx); + arch = utsname_osx.machine; } else { - // Detect ARM based on PE info or uname. - typeof(object).Module.GetPEKind(out var peKind, out var machine); - if (machine == (ImageFileMachine) 0x01C4 /* ARM, .NET Framework 4.5 */) - current |= Platform.ARM; + // Linux + var utsname_linux = new utsname_linux(); + result = uname_linux(ref utsname_linux); + arch = utsname_linux.machine; } - PlatformHelper.Current = current; + if (result == IntPtr.Zero && (arch.StartsWith("aarch") || arch.StartsWith("arm"))) + current |= Platform.ARM; } - - private static bool Is(Platform current, Platform expected) => (current & expected) == expected; - - [StructLayout(LayoutKind.Sequential, Pack = 1)] - public struct utsname_osx + else { - private const int osx_utslen = 256; - - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = osx_utslen)] - public string sysname; - - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = osx_utslen)] - public string nodename; - - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = osx_utslen)] - public string release; - - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = osx_utslen)] - public string version; - - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = osx_utslen)] - public string machine; + // Detect ARM based on PE info or uname. + typeof(object).Module.GetPEKind(out var peKind, out var machine); + if (machine == (ImageFileMachine) 0x01C4 /* ARM, .NET Framework 4.5 */) + current |= Platform.ARM; } - [StructLayout(LayoutKind.Sequential, Pack = 1)] - public struct utsname_linux - { - private const int linux_utslen = 65; + PlatformHelper.Current = current; + } - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = linux_utslen)] - public string sysname; + private static bool Is(Platform current, Platform expected) => (current & expected) == expected; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = linux_utslen)] - public string nodename; + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct utsname_osx + { + private const int osx_utslen = 256; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = linux_utslen)] - public string release; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = osx_utslen)] + public string sysname; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = linux_utslen)] - public string version; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = osx_utslen)] + public string nodename; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = linux_utslen)] - public string machine; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = osx_utslen)] + public string release; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = linux_utslen)] - public string domainname; - } + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = osx_utslen)] + public string version; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = osx_utslen)] + public string machine; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct utsname_linux + { + private const int linux_utslen = 65; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = linux_utslen)] + public string sysname; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = linux_utslen)] + public string nodename; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = linux_utslen)] + public string release; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = linux_utslen)] + public string version; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = linux_utslen)] + public string machine; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = linux_utslen)] + public string domainname; } } diff --git a/BepInEx.Preloader.Core/RuntimeFixes/ConsoleSetOutFix.cs b/BepInEx.Preloader.Core/RuntimeFixes/ConsoleSetOutFix.cs index f5ae728..194b104 100644 --- a/BepInEx.Preloader.Core/RuntimeFixes/ConsoleSetOutFix.cs +++ b/BepInEx.Preloader.Core/RuntimeFixes/ConsoleSetOutFix.cs @@ -4,47 +4,46 @@ using System.Text; using BepInEx.Logging; using HarmonyLib; -namespace BepInEx.Preloader.RuntimeFixes +namespace BepInEx.Preloader.RuntimeFixes; + +public static class ConsoleSetOutFix { - public static class ConsoleSetOutFix + private static LoggedTextWriter loggedTextWriter; + internal static ManualLogSource ConsoleLogSource = Logger.CreateLogSource("Console"); + + public static void Apply() { - private static LoggedTextWriter loggedTextWriter; - internal static ManualLogSource ConsoleLogSource = Logger.CreateLogSource("Console"); - - public static void Apply() - { - loggedTextWriter = new LoggedTextWriter { Parent = Console.Out }; - Console.SetOut(loggedTextWriter); - Harmony.CreateAndPatchAll(typeof(ConsoleSetOutFix)); - } - - [HarmonyPatch(typeof(Console), nameof(Console.SetOut))] - [HarmonyPrefix] - private static bool OnSetOut(TextWriter newOut) - { - loggedTextWriter.Parent = newOut; - return false; - } + loggedTextWriter = new LoggedTextWriter { Parent = Console.Out }; + Console.SetOut(loggedTextWriter); + Harmony.CreateAndPatchAll(typeof(ConsoleSetOutFix)); } - internal class LoggedTextWriter : TextWriter + [HarmonyPatch(typeof(Console), nameof(Console.SetOut))] + [HarmonyPrefix] + private static bool OnSetOut(TextWriter newOut) { - public override Encoding Encoding { get; } = Encoding.UTF8; + loggedTextWriter.Parent = newOut; + return false; + } +} - public TextWriter Parent { get; set; } +internal class LoggedTextWriter : TextWriter +{ + public override Encoding Encoding { get; } = Encoding.UTF8; - public override void Flush() => Parent.Flush(); + public TextWriter Parent { get; set; } - public override void Write(string value) - { - ConsoleSetOutFix.ConsoleLogSource.LogInfo(value); - Parent.Write(value); - } + public override void Flush() => Parent.Flush(); - public override void WriteLine(string value) - { - ConsoleSetOutFix.ConsoleLogSource.LogInfo(value); - Parent.WriteLine(value); - } + public override void Write(string value) + { + ConsoleSetOutFix.ConsoleLogSource.LogInfo(value); + Parent.Write(value); + } + + public override void WriteLine(string value) + { + ConsoleSetOutFix.ConsoleLogSource.LogInfo(value); + Parent.WriteLine(value); } } diff --git a/BepInEx.Preloader.Unity/BepInEx.Preloader.Unity.csproj b/BepInEx.Preloader.Unity/BepInEx.Preloader.Unity.csproj index 8556f1e..34fcfc7 100644 --- a/BepInEx.Preloader.Unity/BepInEx.Preloader.Unity.csproj +++ b/BepInEx.Preloader.Unity/BepInEx.Preloader.Unity.csproj @@ -3,7 +3,7 @@ net35;netstandard2.0 BepInEx.Preloader.Unity BepInEx.Preloader.Unity - 9 + 10 diff --git a/BepInEx.Preloader.Unity/DoorstopEntrypoint.cs b/BepInEx.Preloader.Unity/DoorstopEntrypoint.cs index 270f90e..30cfe11 100644 --- a/BepInEx.Preloader.Unity/DoorstopEntrypoint.cs +++ b/BepInEx.Preloader.Unity/DoorstopEntrypoint.cs @@ -5,158 +5,157 @@ using System.Reflection; using BepInEx.Preloader.Core; using BepInEx.Preloader.RuntimeFixes; -namespace BepInEx.Preloader.Unity +namespace BepInEx.Preloader.Unity; + +internal static class UnityPreloaderRunner { - internal static class UnityPreloaderRunner + // This is a list of important assemblies in BepInEx core folder that should be force-loaded + // Some games can ship these assemblies in Managed folder, in which case assembly resolving bypasses our LocalResolve + // On the other hand, renaming these assemblies is not viable because 3rd party assemblies + // that we don't build (e.g. MonoMod, Harmony, many plugins) depend on them + // As such, we load them early so that the game uses our version instead + // These assemblies should be known to be rarely edited and are known to be shipped as-is with Unity assets + private static readonly string[] CriticalAssemblies = { - // This is a list of important assemblies in BepInEx core folder that should be force-loaded - // Some games can ship these assemblies in Managed folder, in which case assembly resolving bypasses our LocalResolve - // On the other hand, renaming these assemblies is not viable because 3rd party assemblies - // that we don't build (e.g. MonoMod, Harmony, many plugins) depend on them - // As such, we load them early so that the game uses our version instead - // These assemblies should be known to be rarely edited and are known to be shipped as-is with Unity assets - private static readonly string[] CriticalAssemblies = - { - "Mono.Cecil.dll", - "Mono.Cecil.Mdb.dll", - "Mono.Cecil.Pdb.dll", - "Mono.Cecil.Rocks.dll" - }; + "Mono.Cecil.dll", + "Mono.Cecil.Mdb.dll", + "Mono.Cecil.Pdb.dll", + "Mono.Cecil.Rocks.dll" + }; - private static void LoadCriticalAssemblies() - { - foreach (var criticalAssembly in CriticalAssemblies) - try - { - Assembly.LoadFile(Path.Combine(Paths.BepInExAssemblyDirectory, criticalAssembly)); - } - catch (Exception) - { - // Suppress error for now - // TODO: Should we crash here if load fails? Can't use logging at this point - } - } - - public static void PreloaderPreMain() - { - PlatformUtils.SetPlatform(); - - var bepinPath = Utility.ParentDirectory(Path.GetFullPath(EnvVars.DOORSTOP_INVOKE_DLL_PATH), 2); - - Paths.SetExecutablePath(EnvVars.DOORSTOP_PROCESS_PATH, bepinPath, - EnvVars.DOORSTOP_MANAGED_FOLDER_DIR, - EnvVars.DOORSTOP_DLL_SEARCH_DIRS); - - LoadCriticalAssemblies(); - AppDomain.CurrentDomain.AssemblyResolve += LocalResolve; - // Remove temporary resolver early so it won't override local resolver - AppDomain.CurrentDomain.AssemblyResolve -= DoorstopEntrypoint.ResolveCurrentDirectory; - PreloaderMain(); - } - - private static void PreloaderMain() - { - if (UnityPreloader.ConfigApplyRuntimePatches.Value) - { - XTermFix.Apply(); - ConsoleSetOutFix.Apply(); - } - - UnityPreloader.Run(); - } - - private static Assembly LocalResolve(object sender, ResolveEventArgs args) - { - if (!Utility.TryParseAssemblyName(args.Name, out var assemblyName)) - return null; - - // Use parse assembly name on managed side because native GetName() can fail on some locales - // if the game path has "exotic" characters - - var validAssemblies = AppDomain.CurrentDomain - .GetAssemblies() - .Select(a => new - { - assembly = a, - name = Utility.TryParseAssemblyName(a.FullName, out var name) - ? name - : null - }) - .Where(a => a.name != null && a.name.Name == assemblyName.Name) - .OrderByDescending(a => a.name.Version) - .ToList(); - - // First try to match by version, then just pick the best match (generally highest) - // This should mainly affect cases where the game itself loads some assembly (like Mono.Cecil) - var foundMatch = validAssemblies.FirstOrDefault(a => a.name.Version == assemblyName.Version) ?? - validAssemblies.FirstOrDefault(); - var foundAssembly = foundMatch?.assembly; - - if (foundAssembly != null) - return foundAssembly; - - if (Utility.TryResolveDllAssembly(assemblyName, Paths.BepInExAssemblyDirectory, out foundAssembly) - || Utility.TryResolveDllAssembly(assemblyName, Paths.PatcherPluginPath, out foundAssembly) - || Utility.TryResolveDllAssembly(assemblyName, Paths.PluginPath, out foundAssembly)) - return foundAssembly; - - return null; - } - } - - internal static class DoorstopEntrypoint + private static void LoadCriticalAssemblies() { - private static string preloaderPath; - - /// - /// The main entrypoint of BepInEx, called from Doorstop. - /// - public static void Main() - { - // We set it to the current directory first as a fallback, but try to use the same location as the .exe file. - var silentExceptionLog = $"preloader_{DateTime.Now:yyyyMMdd_HHmmss_fff}.log"; - + foreach (var criticalAssembly in CriticalAssemblies) try { - EnvVars.LoadVars(); - - var gamePath = Path.GetDirectoryName(EnvVars.DOORSTOP_PROCESS_PATH) ?? "."; - silentExceptionLog = Path.Combine(gamePath, silentExceptionLog); - - // Get the path of this DLL via Doorstop env var because Assembly.Location mangles non-ASCII characters on some versions of Mono for unknown reasons - preloaderPath = Path.GetDirectoryName(Path.GetFullPath(EnvVars.DOORSTOP_INVOKE_DLL_PATH)); - - AppDomain.CurrentDomain.AssemblyResolve += ResolveCurrentDirectory; - - // In some versions of Unity 4, Mono tries to resolve BepInEx.dll prematurely because of the call to Paths.SetExecutablePath - // To prevent that, we have to use reflection and a separate startup class so that we can install required assembly resolvers before the main code - typeof(DoorstopEntrypoint).Assembly.GetType($"BepInEx.Preloader.Unity.{nameof(UnityPreloaderRunner)}") - ?.GetMethod(nameof(UnityPreloaderRunner.PreloaderPreMain)) - ?.Invoke(null, null); - } - catch (Exception ex) - { - File.WriteAllText(silentExceptionLog, ex.ToString()); - } - finally - { - AppDomain.CurrentDomain.AssemblyResolve -= ResolveCurrentDirectory; - } - } - - internal static Assembly ResolveCurrentDirectory(object sender, ResolveEventArgs args) - { - // Can't use Utils here because it's not yet resolved - var name = new AssemblyName(args.Name); - - try - { - return Assembly.LoadFile(Path.Combine(preloaderPath, $"{name.Name}.dll")); + Assembly.LoadFile(Path.Combine(Paths.BepInExAssemblyDirectory, criticalAssembly)); } catch (Exception) { - return null; + // Suppress error for now + // TODO: Should we crash here if load fails? Can't use logging at this point } + } + + public static void PreloaderPreMain() + { + PlatformUtils.SetPlatform(); + + var bepinPath = Utility.ParentDirectory(Path.GetFullPath(EnvVars.DOORSTOP_INVOKE_DLL_PATH), 2); + + Paths.SetExecutablePath(EnvVars.DOORSTOP_PROCESS_PATH, bepinPath, + EnvVars.DOORSTOP_MANAGED_FOLDER_DIR, + EnvVars.DOORSTOP_DLL_SEARCH_DIRS); + + LoadCriticalAssemblies(); + AppDomain.CurrentDomain.AssemblyResolve += LocalResolve; + // Remove temporary resolver early so it won't override local resolver + AppDomain.CurrentDomain.AssemblyResolve -= DoorstopEntrypoint.ResolveCurrentDirectory; + PreloaderMain(); + } + + private static void PreloaderMain() + { + if (UnityPreloader.ConfigApplyRuntimePatches.Value) + { + XTermFix.Apply(); + ConsoleSetOutFix.Apply(); + } + + UnityPreloader.Run(); + } + + private static Assembly LocalResolve(object sender, ResolveEventArgs args) + { + if (!Utility.TryParseAssemblyName(args.Name, out var assemblyName)) + return null; + + // Use parse assembly name on managed side because native GetName() can fail on some locales + // if the game path has "exotic" characters + + var validAssemblies = AppDomain.CurrentDomain + .GetAssemblies() + .Select(a => new + { + assembly = a, + name = Utility.TryParseAssemblyName(a.FullName, out var name) + ? name + : null + }) + .Where(a => a.name != null && a.name.Name == assemblyName.Name) + .OrderByDescending(a => a.name.Version) + .ToList(); + + // First try to match by version, then just pick the best match (generally highest) + // This should mainly affect cases where the game itself loads some assembly (like Mono.Cecil) + var foundMatch = validAssemblies.FirstOrDefault(a => a.name.Version == assemblyName.Version) ?? + validAssemblies.FirstOrDefault(); + var foundAssembly = foundMatch?.assembly; + + if (foundAssembly != null) + return foundAssembly; + + if (Utility.TryResolveDllAssembly(assemblyName, Paths.BepInExAssemblyDirectory, out foundAssembly) + || Utility.TryResolveDllAssembly(assemblyName, Paths.PatcherPluginPath, out foundAssembly) + || Utility.TryResolveDllAssembly(assemblyName, Paths.PluginPath, out foundAssembly)) + return foundAssembly; + + return null; + } +} + +internal static class DoorstopEntrypoint +{ + private static string preloaderPath; + + /// + /// The main entrypoint of BepInEx, called from Doorstop. + /// + public static void Main() + { + // We set it to the current directory first as a fallback, but try to use the same location as the .exe file. + var silentExceptionLog = $"preloader_{DateTime.Now:yyyyMMdd_HHmmss_fff}.log"; + + try + { + EnvVars.LoadVars(); + + var gamePath = Path.GetDirectoryName(EnvVars.DOORSTOP_PROCESS_PATH) ?? "."; + silentExceptionLog = Path.Combine(gamePath, silentExceptionLog); + + // Get the path of this DLL via Doorstop env var because Assembly.Location mangles non-ASCII characters on some versions of Mono for unknown reasons + preloaderPath = Path.GetDirectoryName(Path.GetFullPath(EnvVars.DOORSTOP_INVOKE_DLL_PATH)); + + AppDomain.CurrentDomain.AssemblyResolve += ResolveCurrentDirectory; + + // In some versions of Unity 4, Mono tries to resolve BepInEx.dll prematurely because of the call to Paths.SetExecutablePath + // To prevent that, we have to use reflection and a separate startup class so that we can install required assembly resolvers before the main code + typeof(DoorstopEntrypoint).Assembly.GetType($"BepInEx.Preloader.Unity.{nameof(UnityPreloaderRunner)}") + ?.GetMethod(nameof(UnityPreloaderRunner.PreloaderPreMain)) + ?.Invoke(null, null); + } + catch (Exception ex) + { + File.WriteAllText(silentExceptionLog, ex.ToString()); + } + finally + { + AppDomain.CurrentDomain.AssemblyResolve -= ResolveCurrentDirectory; + } + } + + internal static Assembly ResolveCurrentDirectory(object sender, ResolveEventArgs args) + { + // Can't use Utils here because it's not yet resolved + var name = new AssemblyName(args.Name); + + try + { + return Assembly.LoadFile(Path.Combine(preloaderPath, $"{name.Name}.dll")); + } + catch (Exception) + { + return null; } } } diff --git a/BepInEx.Preloader.Unity/EntrypointPatcher.cs b/BepInEx.Preloader.Unity/EntrypointPatcher.cs index 90f2618..866c58d 100644 --- a/BepInEx.Preloader.Unity/EntrypointPatcher.cs +++ b/BepInEx.Preloader.Unity/EntrypointPatcher.cs @@ -8,126 +8,124 @@ using BepInEx.Preloader.Core.Patching; using Mono.Cecil; using Mono.Cecil.Cil; -namespace BepInEx.Preloader.Unity +namespace BepInEx.Preloader.Unity; + +[PatcherPluginInfo("io.bepinex.entrypointpatcher", "BepInEx Entrypoint", "1.0")] +internal class EntrypointPatcher : BasePatcher { - [PatcherPluginInfo("io.bepinex.entrypointpatcher", "BepInEx Entrypoint", "1.0")] - internal class EntrypointPatcher : BasePatcher + private bool HasLoaded { get; set; } = false; + + [TargetAssembly(TargetAssemblyAttribute.AllAssemblies)] + public bool PatchEntrypoint(ref AssemblyDefinition assembly, string filename) { - private bool HasLoaded { get; set; } = false; + if (HasLoaded || filename != ConfigEntrypointAssembly.Value) + return false; - [TargetAssembly(TargetAssemblyAttribute.AllAssemblies)] - public bool PatchEntrypoint(ref AssemblyDefinition assembly, string filename) + if (assembly.MainModule.AssemblyReferences.Any(x => x.Name.Contains("BepInEx"))) + throw new + Exception("BepInEx has been detected to be patched! Please unpatch before using a patchless variant!"); + + var entrypointType = ConfigEntrypointType.Value; + var entrypointMethod = ConfigEntrypointMethod.Value; + + Log.LogDebug($"Hooking chainloader into {entrypointType}::{entrypointMethod}"); + + var isCctor = entrypointMethod.IsNullOrWhiteSpace() || entrypointMethod == ".cctor"; + + + var entryType = assembly.MainModule.Types.FirstOrDefault(x => x.Name == entrypointType); + + if (entryType == null) + throw new Exception("The entrypoint type is invalid! Please check your config/BepInEx.cfg file"); + + var chainloaderAssemblyPath = Path.Combine(Paths.BepInExAssemblyDirectory, "BepInEx.Unity.dll"); + + var readerParameters = new ReaderParameters { - if (HasLoaded || filename != ConfigEntrypointAssembly.Value) - return false; + AssemblyResolver = TypeLoader.CecilResolver + }; - if (assembly.MainModule.AssemblyReferences.Any(x => x.Name.Contains("BepInEx"))) - throw new - Exception("BepInEx has been detected to be patched! Please unpatch before using a patchless variant!"); + using (var chainloaderAssemblyDefinition = + AssemblyDefinition.ReadAssembly(chainloaderAssemblyPath, readerParameters)) + { + var chainloaderType = + chainloaderAssemblyDefinition.MainModule.Types.First(x => x.Name == "UnityChainloader"); - var entrypointType = ConfigEntrypointType.Value; - var entrypointMethod = ConfigEntrypointMethod.Value; + var originalStartMethod = chainloaderType.EnumerateAllMethods().First(x => x.Name == "StaticStart"); - Log.LogDebug($"Hooking chainloader into {entrypointType}::{entrypointMethod}"); + var startMethod = assembly.MainModule.ImportReference(originalStartMethod); - var isCctor = entrypointMethod.IsNullOrWhiteSpace() || entrypointMethod == ".cctor"; + var methods = new List(); - - var entryType = assembly.MainModule.Types.FirstOrDefault(x => x.Name == entrypointType); - - if (entryType == null) - throw new Exception("The entrypoint type is invalid! Please check your config/BepInEx.cfg file"); - - var chainloaderAssemblyPath = Path.Combine(Paths.BepInExAssemblyDirectory, "BepInEx.Unity.dll"); - - var readerParameters = new ReaderParameters + if (isCctor) { - AssemblyResolver = TypeLoader.CecilResolver - }; + var cctor = entryType.Methods.FirstOrDefault(m => m.IsConstructor && m.IsStatic); - using (var chainloaderAssemblyDefinition = - AssemblyDefinition.ReadAssembly(chainloaderAssemblyPath, readerParameters)) + if (cctor == null) + { + cctor = new MethodDefinition(".cctor", + MethodAttributes.Static | MethodAttributes.Private + | MethodAttributes.HideBySig | + MethodAttributes.SpecialName + | MethodAttributes.RTSpecialName, + assembly.MainModule.ImportReference(typeof(void))); + + entryType.Methods.Add(cctor); + var il = cctor.Body.GetILProcessor(); + il.Append(il.Create(OpCodes.Ret)); + } + + methods.Add(cctor); + } + else { - var chainloaderType = - chainloaderAssemblyDefinition.MainModule.Types.First(x => x.Name == "UnityChainloader"); - - var originalStartMethod = chainloaderType.EnumerateAllMethods().First(x => x.Name == "StaticStart"); - - var startMethod = assembly.MainModule.ImportReference(originalStartMethod); - - var methods = new List(); - - if (isCctor) - { - var cctor = entryType.Methods.FirstOrDefault(m => m.IsConstructor && m.IsStatic); - - if (cctor == null) - { - cctor = new MethodDefinition(".cctor", - MethodAttributes.Static | MethodAttributes.Private - | MethodAttributes.HideBySig | MethodAttributes.SpecialName - | MethodAttributes.RTSpecialName, - assembly.MainModule.ImportReference(typeof(void))); - - entryType.Methods.Add(cctor); - var il = cctor.Body.GetILProcessor(); - il.Append(il.Create(OpCodes.Ret)); - } - - methods.Add(cctor); - } - else - { - methods.AddRange(entryType.Methods.Where(x => x.Name == entrypointMethod)); - } - - if (!methods.Any()) - throw new Exception("The entrypoint method is invalid! Please check your config.ini"); - - foreach (var method in methods) - { - var il = method.Body.GetILProcessor(); - - var ins = il.Body.Instructions.First(); - - il.InsertBefore(ins, - il.Create(OpCodes.Ldnull)); // gameExePath (always null, we initialize the Paths class in Entrypoint) - - il.InsertBefore(ins, - il.Create(OpCodes.Call, - startMethod)); // UnityChainloader.StaticStart(string gameExePath) - } + methods.AddRange(entryType.Methods.Where(x => x.Name == entrypointMethod)); } - HasLoaded = true; + if (!methods.Any()) + throw new Exception("The entrypoint method is invalid! Please check your config.ini"); - return true; - } - - public override void Finalizer() - { - if (!HasLoaded) + foreach (var method in methods) { - Log.LogFatal($"Failed to patch BepInEx chainloader into assembly '{ConfigEntrypointAssembly.Value}', either due to error or not being able to find it. Is it spelled correctly?"); + var il = method.Body.GetILProcessor(); + + var ins = il.Body.Instructions.First(); + + il.InsertBefore(ins, + il.Create(OpCodes + .Ldnull)); // gameExePath (always null, we initialize the Paths class in Entrypoint) + + il.InsertBefore(ins, + il.Create(OpCodes.Call, + startMethod)); // UnityChainloader.StaticStart(string gameExePath) } } - private static readonly ConfigEntry ConfigEntrypointAssembly = ConfigFile.CoreConfig.Bind( - "Preloader.Entrypoint", "Assembly", - UnityPreloader.IsPostUnity2017 - ? "UnityEngine.CoreModule.dll" - : "UnityEngine.dll", - "The local filename of the assembly to target."); - - private static readonly ConfigEntry ConfigEntrypointType = ConfigFile.CoreConfig.Bind( - "Preloader.Entrypoint", "Type", - "Application", - "The name of the type in the entrypoint assembly to search for the entrypoint method."); - - private static readonly ConfigEntry ConfigEntrypointMethod = ConfigFile.CoreConfig.Bind( - "Preloader.Entrypoint", "Method", - ".cctor", - "The name of the method in the specified entrypoint assembly and type to hook and load Chainloader from."); + HasLoaded = true; + return true; } + + public override void Finalizer() + { + if (!HasLoaded) + Log.LogFatal($"Failed to patch BepInEx chainloader into assembly '{ConfigEntrypointAssembly.Value}', either due to error or not being able to find it. Is it spelled correctly?"); + } + + private static readonly ConfigEntry ConfigEntrypointAssembly = ConfigFile.CoreConfig.Bind( + "Preloader.Entrypoint", "Assembly", + UnityPreloader.IsPostUnity2017 + ? "UnityEngine.CoreModule.dll" + : "UnityEngine.dll", + "The local filename of the assembly to target."); + + private static readonly ConfigEntry ConfigEntrypointType = ConfigFile.CoreConfig.Bind( + "Preloader.Entrypoint", "Type", + "Application", + "The name of the type in the entrypoint assembly to search for the entrypoint method."); + + private static readonly ConfigEntry ConfigEntrypointMethod = ConfigFile.CoreConfig.Bind( + "Preloader.Entrypoint", "Method", + ".cctor", + "The name of the method in the specified entrypoint assembly and type to hook and load Chainloader from."); } diff --git a/BepInEx.Preloader.Unity/RuntimeFixes/TraceFix.cs b/BepInEx.Preloader.Unity/RuntimeFixes/TraceFix.cs index 6cfc343..e6f5446 100644 --- a/BepInEx.Preloader.Unity/RuntimeFixes/TraceFix.cs +++ b/BepInEx.Preloader.Unity/RuntimeFixes/TraceFix.cs @@ -4,71 +4,70 @@ using System.Linq; using System.Reflection; using HarmonyLib; -namespace BepInEx.Preloader.RuntimeFixes +namespace BepInEx.Preloader.RuntimeFixes; + +/// +/// This exists because the Mono implementation of is/was broken, and would call Write directly +/// instead of calling TraceEvent. +/// +internal static class TraceFix { - /// - /// This exists because the Mono implementation of is/was broken, and would call Write directly - /// instead of calling TraceEvent. - /// - internal static class TraceFix + private static Type TraceImplType; + + private static object ListenersSyncRoot; + private static TraceListenerCollection Listeners; + private static PropertyInfo prop_AutoFlush; + + private static bool AutoFlush => (bool) prop_AutoFlush.GetValue(null, null); + + + public static void ApplyFix() { - private static Type TraceImplType; + TraceImplType = AppDomain.CurrentDomain.GetAssemblies() + .First(x => x.GetName().Name == "System") + .GetTypes() + .FirstOrDefault(x => x.Name == "TraceImpl"); + // assembly that has already fixed this + if (TraceImplType == null) return; - private static object ListenersSyncRoot; - private static TraceListenerCollection Listeners; - private static PropertyInfo prop_AutoFlush; + ListenersSyncRoot = AccessTools.Property(TraceImplType, "ListenersSyncRoot").GetValue(null, null); - private static bool AutoFlush => (bool) prop_AutoFlush.GetValue(null, null); + Listeners = (TraceListenerCollection) AccessTools.Property(TraceImplType, "Listeners").GetValue(null, null); + + prop_AutoFlush = AccessTools.Property(TraceImplType, "AutoFlush"); - public static void ApplyFix() + var instance = new Harmony("com.bepis.bepinex.tracefix"); + + instance.Patch( + typeof(Trace).GetMethod("DoTrace", BindingFlags.Static | BindingFlags.NonPublic), + new HarmonyMethod(typeof(TraceFix).GetMethod(nameof(DoTraceReplacement), + BindingFlags.Static | BindingFlags.NonPublic))); + } + + + private static bool DoTraceReplacement(string kind, Assembly report, string message) + { + var arg = string.Empty; + try { - TraceImplType = AppDomain.CurrentDomain.GetAssemblies() - .First(x => x.GetName().Name == "System") - .GetTypes() - .FirstOrDefault(x => x.Name == "TraceImpl"); - // assembly that has already fixed this - if (TraceImplType == null) return; + arg = report.GetName().Name; + } + catch (MethodAccessException) { } - ListenersSyncRoot = AccessTools.Property(TraceImplType, "ListenersSyncRoot").GetValue(null, null); + var type = (TraceEventType) Enum.Parse(typeof(TraceEventType), kind); - Listeners = (TraceListenerCollection) AccessTools.Property(TraceImplType, "Listeners").GetValue(null, null); + lock (ListenersSyncRoot) + { + foreach (var obj in Listeners) + { + var traceListener = (TraceListener) obj; + traceListener.TraceEvent(new TraceEventCache(), arg, type, 0, message); - prop_AutoFlush = AccessTools.Property(TraceImplType, "AutoFlush"); - - - var instance = new Harmony("com.bepis.bepinex.tracefix"); - - instance.Patch( - typeof(Trace).GetMethod("DoTrace", BindingFlags.Static | BindingFlags.NonPublic), - new HarmonyMethod(typeof(TraceFix).GetMethod(nameof(DoTraceReplacement), - BindingFlags.Static | BindingFlags.NonPublic))); + if (AutoFlush) traceListener.Flush(); + } } - - private static bool DoTraceReplacement(string kind, Assembly report, string message) - { - var arg = string.Empty; - try - { - arg = report.GetName().Name; - } - catch (MethodAccessException) { } - - var type = (TraceEventType) Enum.Parse(typeof(TraceEventType), kind); - - lock (ListenersSyncRoot) - { - foreach (var obj in Listeners) - { - var traceListener = (TraceListener) obj; - traceListener.TraceEvent(new TraceEventCache(), arg, type, 0, message); - - if (AutoFlush) traceListener.Flush(); - } - } - - return false; - } + return false; } } diff --git a/BepInEx.Preloader.Unity/RuntimeFixes/UnityPatches.cs b/BepInEx.Preloader.Unity/RuntimeFixes/UnityPatches.cs index 57c15fb..6f1b499 100644 --- a/BepInEx.Preloader.Unity/RuntimeFixes/UnityPatches.cs +++ b/BepInEx.Preloader.Unity/RuntimeFixes/UnityPatches.cs @@ -3,40 +3,39 @@ using System.Collections.Generic; using System.Reflection; using HarmonyLib; -namespace BepInEx.Preloader.RuntimeFixes +namespace BepInEx.Preloader.RuntimeFixes; + +internal static class UnityPatches { - internal static class UnityPatches + private static Harmony HarmonyInstance { get; set; } + + public static Dictionary AssemblyLocations { get; } = + new(StringComparer.InvariantCultureIgnoreCase); + + public static void Apply() { - private static Harmony HarmonyInstance { get; set; } + HarmonyInstance = Harmony.CreateAndPatchAll(typeof(UnityPatches)); - public static Dictionary AssemblyLocations { get; } = - new(StringComparer.InvariantCultureIgnoreCase); - - public static void Apply() + try { - HarmonyInstance = Harmony.CreateAndPatchAll(typeof(UnityPatches)); - - try - { - TraceFix.ApplyFix(); - } - catch { } //ignore everything, if it's thrown an exception, we're using an assembly that has already fixed this + TraceFix.ApplyFix(); } + catch { } //ignore everything, if it's thrown an exception, we're using an assembly that has already fixed this + } - [HarmonyPostfix] - [HarmonyPatch(typeof(Assembly), nameof(Assembly.Location), MethodType.Getter)] - public static void GetLocation(ref string __result, Assembly __instance) - { - if (AssemblyLocations.TryGetValue(__instance.FullName, out var location)) - __result = location; - } + [HarmonyPostfix] + [HarmonyPatch(typeof(Assembly), nameof(Assembly.Location), MethodType.Getter)] + public static void GetLocation(ref string __result, Assembly __instance) + { + if (AssemblyLocations.TryGetValue(__instance.FullName, out var location)) + __result = location; + } - [HarmonyPostfix] - [HarmonyPatch(typeof(Assembly), nameof(Assembly.CodeBase), MethodType.Getter)] - public static void GetCodeBase(ref string __result, Assembly __instance) - { - if (AssemblyLocations.TryGetValue(__instance.FullName, out var location)) - __result = $"file://{location.Replace('\\', '/')}"; - } + [HarmonyPostfix] + [HarmonyPatch(typeof(Assembly), nameof(Assembly.CodeBase), MethodType.Getter)] + public static void GetCodeBase(ref string __result, Assembly __instance) + { + if (AssemblyLocations.TryGetValue(__instance.FullName, out var location)) + __result = $"file://{location.Replace('\\', '/')}"; } } diff --git a/BepInEx.Preloader.Unity/RuntimeFixes/XTermFix.cs b/BepInEx.Preloader.Unity/RuntimeFixes/XTermFix.cs index 93ad069..f7b1f9c 100644 --- a/BepInEx.Preloader.Unity/RuntimeFixes/XTermFix.cs +++ b/BepInEx.Preloader.Unity/RuntimeFixes/XTermFix.cs @@ -7,130 +7,127 @@ using MonoMod.RuntimeDetour; using MonoMod.RuntimeDetour.Platforms; using MonoMod.Utils; -namespace BepInEx.Preloader.RuntimeFixes +namespace BepInEx.Preloader.RuntimeFixes; + +internal static class XTermFix { - internal static class XTermFix + public static int intOffset; + + public static void Apply() { - public static int intOffset; + if (PlatformHelper.Is(Platform.Windows)) + return; - public static void Apply() - { - if (PlatformHelper.Is(Platform.Windows)) - return; + if (typeof(Console).Assembly.GetType("System.ConsoleDriver") == null) + // Mono version is too old, use our own TTY implementation instead + return; - if (typeof(Console).Assembly.GetType("System.ConsoleDriver") == null) - // Mono version is too old, use our own TTY implementation instead - return; + if (AccessTools.Method("System.TermInfoReader:DetermineVersion") != null) + // Fix has been applied officially + return; - if (AccessTools.Method("System.TermInfoReader:DetermineVersion") != null) - // Fix has been applied officially - return; + // Apparently on older Unity versions (4.x), using Process.Start can run Console..cctor + // And since MonoMod's PlatformHelper (used by DetourHelper.Native) runs Process.Start to determine ARM/x86 platform, + // this causes a crash owing to TermInfoReader running before it can be patched and fixed + // Because Doorstop does not support ARM at the moment, we can get away with just forcing x86 detour platform. + // TODO: Figure out a way to detect ARM on Unix without running Process.Start + DetourHelper.Native = new DetourNativeX86Platform(); - // Apparently on older Unity versions (4.x), using Process.Start can run Console..cctor - // And since MonoMod's PlatformHelper (used by DetourHelper.Native) runs Process.Start to determine ARM/x86 platform, - // this causes a crash owing to TermInfoReader running before it can be patched and fixed - // Because Doorstop does not support ARM at the moment, we can get away with just forcing x86 detour platform. - // TODO: Figure out a way to detect ARM on Unix without running Process.Start - DetourHelper.Native = new DetourNativeX86Platform(); + var harmony = new Harmony("com.bepinex.xtermfix"); - var harmony = new Harmony("com.bepinex.xtermfix"); + harmony.Patch(AccessTools.Method("System.TermInfoReader:ReadHeader"), + new HarmonyMethod(typeof(XTermFix), nameof(ReadHeaderPrefix))); - harmony.Patch(AccessTools.Method("System.TermInfoReader:ReadHeader"), - new HarmonyMethod(typeof(XTermFix), nameof(ReadHeaderPrefix))); + harmony.Patch(AccessTools.Method("System.TermInfoReader:Get", new[] { AccessTools.TypeByName("System.TermInfoNumbers") }), + transpiler: new HarmonyMethod(typeof(XTermFix), nameof(GetTermInfoNumbersTranspiler))); - harmony.Patch(AccessTools.Method("System.TermInfoReader:Get", new[] { AccessTools.TypeByName("System.TermInfoNumbers") }), - transpiler: new HarmonyMethod(typeof(XTermFix), nameof(GetTermInfoNumbersTranspiler))); + harmony.Patch(AccessTools.Method("System.TermInfoReader:Get", new[] { AccessTools.TypeByName("System.TermInfoStrings") }), + transpiler: new HarmonyMethod(typeof(XTermFix), nameof(GetTermInfoStringsTranspiler))); - harmony.Patch(AccessTools.Method("System.TermInfoReader:Get", new[] { AccessTools.TypeByName("System.TermInfoStrings") }), - transpiler: new HarmonyMethod(typeof(XTermFix), nameof(GetTermInfoStringsTranspiler))); + harmony.Patch(AccessTools.Method("System.TermInfoReader:GetStringBytes", new[] { AccessTools.TypeByName("System.TermInfoStrings") }), + transpiler: new HarmonyMethod(typeof(XTermFix), nameof(GetTermInfoStringsTranspiler))); - harmony.Patch(AccessTools.Method("System.TermInfoReader:GetStringBytes", new[] { AccessTools.TypeByName("System.TermInfoStrings") }), - transpiler: new HarmonyMethod(typeof(XTermFix), nameof(GetTermInfoStringsTranspiler))); + DetourHelper.Native = null; + } - DetourHelper.Native = null; - } + public static int GetInt32(byte[] buffer, int offset) + { + int b1 = buffer[offset]; + int b2 = buffer[offset + 1]; + int b3 = buffer[offset + 2]; + int b4 = buffer[offset + 3]; - public static int GetInt32(byte[] buffer, int offset) - { - int b1 = buffer[offset]; - int b2 = buffer[offset + 1]; - int b3 = buffer[offset + 2]; - int b4 = buffer[offset + 3]; + return b1 | (b2 << 8) | (b3 << 16) | (b4 << 24); + } - return b1 | (b2 << 8) | (b3 << 16) | (b4 << 24); - } + public static short GetInt16(byte[] buffer, int offset) + { + int b1 = buffer[offset]; + int b2 = buffer[offset + 1]; - public static short GetInt16(byte[] buffer, int offset) - { - int b1 = buffer[offset]; - int b2 = buffer[offset + 1]; + return (short) (b1 | (b2 << 8)); + } - return (short) (b1 | (b2 << 8)); - } + public static int GetInteger(byte[] buffer, int offset) => + intOffset == 2 + ? GetInt16(buffer, offset) + : GetInt32(buffer, offset); - public static int GetInteger(byte[] buffer, int offset) => - intOffset == 2 - ? GetInt16(buffer, offset) - : GetInt32(buffer, offset); + public static void DetermineVersion(short magic) + { + if (magic == 0x11a) + intOffset = 2; + else if (magic == 0x21e) + intOffset = 4; + else + throw new Exception($"Unknown xterm header format: {magic}"); + } - public static void DetermineVersion(short magic) - { - if (magic == 0x11a) - intOffset = 2; - else if (magic == 0x21e) - intOffset = 4; - else - throw new Exception($"Unknown xterm header format: {magic}"); - } + public static bool ReadHeaderPrefix(byte[] buffer, + ref int position, + ref short ___boolSize, + ref short ___numSize, + ref short ___strOffsets) + { + var magic = GetInt16(buffer, position); + position += 2; + DetermineVersion(magic); - public static bool ReadHeaderPrefix(byte[] buffer, - ref int position, - ref short ___boolSize, - ref short ___numSize, - ref short ___strOffsets) - { - var magic = GetInt16(buffer, position); - position += 2; - DetermineVersion(magic); + // nameSize = GetInt16(buffer, position); + position += 2; + ___boolSize = GetInt16(buffer, position); + position += 2; + ___numSize = GetInt16(buffer, position); + position += 2; + ___strOffsets = GetInt16(buffer, position); + position += 2; + // strSize = GetInt16(buffer, position); + position += 2; - // nameSize = GetInt16(buffer, position); - position += 2; - ___boolSize = GetInt16(buffer, position); - position += 2; - ___numSize = GetInt16(buffer, position); - position += 2; - ___strOffsets = GetInt16(buffer, position); - position += 2; - // strSize = GetInt16(buffer, position); - position += 2; + return false; + } - return false; - } + public static IEnumerable GetTermInfoNumbersTranspiler(IEnumerable instructions) + { + // This implementation does not seem to have changed so I will be using indexes like the lazy fuck I am - public static IEnumerable GetTermInfoNumbersTranspiler( - IEnumerable instructions) - { - // This implementation does not seem to have changed so I will be using indexes like the lazy fuck I am + var list = instructions.ToList(); - var list = instructions.ToList(); + list[31] = new CodeInstruction(OpCodes.Ldsfld, AccessTools.Field(typeof(XTermFix), nameof(intOffset))); + list[36] = new CodeInstruction(OpCodes.Nop); + list[39] = new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(XTermFix), nameof(GetInteger))); - list[31] = new CodeInstruction(OpCodes.Ldsfld, AccessTools.Field(typeof(XTermFix), nameof(intOffset))); - list[36] = new CodeInstruction(OpCodes.Nop); - list[39] = new CodeInstruction(OpCodes.Call, AccessTools.Method(typeof(XTermFix), nameof(GetInteger))); + return list; + } - return list; - } + public static IEnumerable GetTermInfoStringsTranspiler(IEnumerable instructions) + { + // This implementation does not seem to have changed so I will be using indexes like the lazy fuck I am - public static IEnumerable GetTermInfoStringsTranspiler( - IEnumerable instructions) - { - // This implementation does not seem to have changed so I will be using indexes like the lazy fuck I am + var list = instructions.ToList(); - var list = instructions.ToList(); + list[32] = new CodeInstruction(OpCodes.Ldsfld, AccessTools.Field(typeof(XTermFix), nameof(intOffset))); - list[32] = new CodeInstruction(OpCodes.Ldsfld, AccessTools.Field(typeof(XTermFix), nameof(intOffset))); - - return list; - } + return list; } } diff --git a/BepInEx.Preloader.Unity/UnityPreloader.cs b/BepInEx.Preloader.Unity/UnityPreloader.cs index 1cdd880..a2fff9c 100644 --- a/BepInEx.Preloader.Unity/UnityPreloader.cs +++ b/BepInEx.Preloader.Unity/UnityPreloader.cs @@ -12,192 +12,191 @@ using BepInEx.Preloader.Core.Patching; using BepInEx.Preloader.RuntimeFixes; using MonoMod.Utils; -namespace BepInEx.Preloader.Unity +namespace BepInEx.Preloader.Unity; + +/// +/// The main entrypoint of BepInEx, and initializes all patchers and the chainloader. +/// +internal static class UnityPreloader { /// - /// The main entrypoint of BepInEx, and initializes all patchers and the chainloader. + /// The log writer that is specific to the preloader. /// - internal static class UnityPreloader + private static PreloaderConsoleListener PreloaderLog { get; set; } + + private static ManualLogSource Log => PreloaderLogger.Log; + + public static bool IsPostUnity2017 { get; } = + File.Exists(Path.Combine(Paths.ManagedPath, "UnityEngine.CoreModule.dll")); + + public static void Run() { - /// - /// The log writer that is specific to the preloader. - /// - private static PreloaderConsoleListener PreloaderLog { get; set; } + try + { + InitializeHarmony(); - private static ManualLogSource Log => PreloaderLogger.Log; + ConsoleManager.Initialize(false); + AllocateConsole(); - public static bool IsPostUnity2017 { get; } = - File.Exists(Path.Combine(Paths.ManagedPath, "UnityEngine.CoreModule.dll")); + Utility.TryDo(() => + { + if (ConfigApplyRuntimePatches.Value) + UnityPatches.Apply(); + }, out var runtimePatchException); - public static void Run() + Logger.Sources.Add(new HarmonyLogSource()); + Logger.Sources.Add(TraceLogSource.CreateSource()); + + PreloaderLog = new PreloaderConsoleListener(); + Logger.Listeners.Add(PreloaderLog); + + ChainloaderLogHelper.PrintLogInfo(Log); + + Log.LogInfo($"Running under Unity v{GetUnityVersion()}"); + Log.LogInfo($"CLR runtime version: {Environment.Version}"); + Log.LogInfo($"Supports SRE: {Utility.CLRSupportsDynamicAssemblies}"); + + Log.LogDebug($"Game executable path: {Paths.ExecutablePath}"); + Log.LogDebug($"Unity Managed directory: {Paths.ManagedPath}"); + Log.LogDebug($"BepInEx root path: {Paths.BepInExRootPath}"); + + if (runtimePatchException != null) + Log.LogWarning($"Failed to apply runtime patches for Mono. See more info in the output log. Error message: {runtimePatchException.Message}"); + + Log.LogMessage("Preloader started"); + + TypeLoader.SearchDirectories.UnionWith(Paths.DllSearchPaths); + + using (var assemblyPatcher = new AssemblyPatcher()) + { + assemblyPatcher.AddPatchersFromDirectory(Paths.BepInExAssemblyDirectory); + assemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath); + + Log.LogInfo($"{assemblyPatcher.PatcherContext.PatcherPlugins.Count} patcher plugin{(assemblyPatcher.PatcherContext.PatcherPlugins.Count == 1 ? "" : "s")} loaded"); + + assemblyPatcher.LoadAssemblyDirectories(Paths.DllSearchPaths); + + Log.LogInfo($"{assemblyPatcher.PatcherContext.AvailableAssemblies.Count} assemblies discovered"); + + assemblyPatcher.PatchAndLoad(); + } + + + Log.LogMessage("Preloader finished"); + + Logger.Listeners.Remove(PreloaderLog); + + PreloaderLog.Dispose(); + } + catch (Exception ex) { try { - InitializeHarmony(); + Log.LogFatal("Could not run preloader!"); + Log.LogFatal(ex); - ConsoleManager.Initialize(false); - AllocateConsole(); - - Utility.TryDo(() => + if (!ConsoleManager.ConsoleActive) { - if (ConfigApplyRuntimePatches.Value) - UnityPatches.Apply(); - }, out var runtimePatchException); - - Logger.Sources.Add(new HarmonyLogSource()); - Logger.Sources.Add(TraceLogSource.CreateSource()); - - PreloaderLog = new PreloaderConsoleListener(); - Logger.Listeners.Add(PreloaderLog); - - ChainloaderLogHelper.PrintLogInfo(Log); - - Log.LogInfo($"Running under Unity v{GetUnityVersion()}"); - Log.LogInfo($"CLR runtime version: {Environment.Version}"); - Log.LogInfo($"Supports SRE: {Utility.CLRSupportsDynamicAssemblies}"); - - Log.LogDebug($"Game executable path: {Paths.ExecutablePath}"); - Log.LogDebug($"Unity Managed directory: {Paths.ManagedPath}"); - Log.LogDebug($"BepInEx root path: {Paths.BepInExRootPath}"); - - if (runtimePatchException != null) - Log.LogWarning($"Failed to apply runtime patches for Mono. See more info in the output log. Error message: {runtimePatchException.Message}"); - - Log.LogMessage("Preloader started"); - - TypeLoader.SearchDirectories.UnionWith(Paths.DllSearchPaths); - - using (var assemblyPatcher = new AssemblyPatcher()) - { - assemblyPatcher.AddPatchersFromDirectory(Paths.BepInExAssemblyDirectory); - assemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath); - - Log.LogInfo($"{assemblyPatcher.PatcherContext.PatcherPlugins.Count} patcher plugin{(assemblyPatcher.PatcherContext.PatcherPlugins.Count == 1 ? "" : "s")} loaded"); - - assemblyPatcher.LoadAssemblyDirectories(Paths.DllSearchPaths); - - Log.LogInfo($"{assemblyPatcher.PatcherContext.AvailableAssemblies.Count} assemblies discovered"); - - assemblyPatcher.PatchAndLoad(); + //if we've already attached the console, then the log will already be written to the console + AllocateConsole(); + Console.Write(PreloaderLog); } - - - Log.LogMessage("Preloader finished"); - - Logger.Listeners.Remove(PreloaderLog); - - PreloaderLog.Dispose(); } - catch (Exception ex) - { - try - { - Log.LogFatal("Could not run preloader!"); - Log.LogFatal(ex); + catch { } - if (!ConsoleManager.ConsoleActive) - { - //if we've already attached the console, then the log will already be written to the console - AllocateConsole(); - Console.Write(PreloaderLog); - } - } - catch { } - - var log = string.Empty; - - try - { - // We could use platform-dependent newlines, however the developers use Windows so this will be easier to read :) - - log = string.Join("\r\n", PreloaderConsoleListener.LogEvents.Select(x => x.ToString()).ToArray()); - log += "\r\n"; - - PreloaderLog?.Dispose(); - PreloaderLog = null; - } - catch { } - - File.WriteAllText( - Path.Combine(Paths.GameRootPath, $"preloader_{DateTime.Now:yyyyMMdd_HHmmss_fff}.log"), - log + ex); - } - } - - /// - /// Allocates a console window for use by BepInEx safely. - /// - public static void AllocateConsole() - { - if (!ConsoleManager.ConsoleEnabled) - return; + var log = string.Empty; try { - ConsoleManager.CreateConsole(); - Logger.Listeners.Add(new ConsoleLogListener()); - } - catch (Exception ex) - { - Log.LogError("Failed to allocate console!"); - Log.LogError(ex); + // We could use platform-dependent newlines, however the developers use Windows so this will be easier to read :) + + log = string.Join("\r\n", PreloaderConsoleListener.LogEvents.Select(x => x.ToString()).ToArray()); + log += "\r\n"; + + PreloaderLog?.Dispose(); + PreloaderLog = null; } + catch { } + + File.WriteAllText( + Path.Combine(Paths.GameRootPath, $"preloader_{DateTime.Now:yyyyMMdd_HHmmss_fff}.log"), + log + ex); } - - public static string GetUnityVersion() - { - if (PlatformHelper.Is(Platform.Windows)) - return FileVersionInfo.GetVersionInfo(Paths.ExecutablePath).FileVersion; - - return $"Unknown ({(IsPostUnity2017 ? "post" : "pre")}-2017)"; - } - - private static void InitializeHarmony() - { - switch (ConfigHarmonyBackend.Value) - { - case MonoModBackend.auto: - break; - case MonoModBackend.dynamicmethod: - case MonoModBackend.methodbuilder: - case MonoModBackend.cecil: - Environment.SetEnvironmentVariable("MONOMOD_DMD_TYPE", ConfigHarmonyBackend.Value.ToString()); - break; - default: - throw new ArgumentOutOfRangeException(nameof(ConfigHarmonyBackend), ConfigHarmonyBackend.Value, - "Unknown backend"); - } - } - - private enum MonoModBackend - { - // Enum names are important! - [Description("Auto")] - auto = 0, - - [Description("DynamicMethod")] - dynamicmethod, - - [Description("MethodBuilder")] - methodbuilder, - - [Description("Cecil")] - cecil - } - - #region Config - - internal static readonly ConfigEntry ConfigApplyRuntimePatches = ConfigFile.CoreConfig.Bind( - "Preloader", "ApplyRuntimePatches", - true, - "Enables or disables runtime patches.\nThis should always be true, unless you cannot start the game due to a Harmony related issue (such as running .NET Standard runtime) or you know what you're doing."); - - private static readonly ConfigEntry ConfigHarmonyBackend = ConfigFile.CoreConfig.Bind( - "Preloader", - "HarmonyBackend", - MonoModBackend.auto, - "Specifies which MonoMod backend to use for Harmony patches. Auto uses the best available backend.\nThis setting should only be used for development purposes (e.g. debugging in dnSpy). Other code might override this setting."); - - #endregion } + + /// + /// Allocates a console window for use by BepInEx safely. + /// + public static void AllocateConsole() + { + if (!ConsoleManager.ConsoleEnabled) + return; + + try + { + ConsoleManager.CreateConsole(); + Logger.Listeners.Add(new ConsoleLogListener()); + } + catch (Exception ex) + { + Log.LogError("Failed to allocate console!"); + Log.LogError(ex); + } + } + + public static string GetUnityVersion() + { + if (PlatformHelper.Is(Platform.Windows)) + return FileVersionInfo.GetVersionInfo(Paths.ExecutablePath).FileVersion; + + return $"Unknown ({(IsPostUnity2017 ? "post" : "pre")}-2017)"; + } + + private static void InitializeHarmony() + { + switch (ConfigHarmonyBackend.Value) + { + case MonoModBackend.auto: + break; + case MonoModBackend.dynamicmethod: + case MonoModBackend.methodbuilder: + case MonoModBackend.cecil: + Environment.SetEnvironmentVariable("MONOMOD_DMD_TYPE", ConfigHarmonyBackend.Value.ToString()); + break; + default: + throw new ArgumentOutOfRangeException(nameof(ConfigHarmonyBackend), ConfigHarmonyBackend.Value, + "Unknown backend"); + } + } + + private enum MonoModBackend + { + // Enum names are important! + [Description("Auto")] + auto = 0, + + [Description("DynamicMethod")] + dynamicmethod, + + [Description("MethodBuilder")] + methodbuilder, + + [Description("Cecil")] + cecil + } + + #region Config + + internal static readonly ConfigEntry ConfigApplyRuntimePatches = ConfigFile.CoreConfig.Bind( + "Preloader", "ApplyRuntimePatches", + true, + "Enables or disables runtime patches.\nThis should always be true, unless you cannot start the game due to a Harmony related issue (such as running .NET Standard runtime) or you know what you're doing."); + + private static readonly ConfigEntry ConfigHarmonyBackend = ConfigFile.CoreConfig.Bind( + "Preloader", + "HarmonyBackend", + MonoModBackend.auto, + "Specifies which MonoMod backend to use for Harmony patches. Auto uses the best available backend.\nThis setting should only be used for development purposes (e.g. debugging in dnSpy). Other code might override this setting."); + + #endregion } diff --git a/BepInEx.Shared/BepInEx.Shared.shproj b/BepInEx.Shared/BepInEx.Shared.shproj index 24a9ba1..7a5c502 100644 --- a/BepInEx.Shared/BepInEx.Shared.shproj +++ b/BepInEx.Shared/BepInEx.Shared.shproj @@ -3,9 +3,9 @@ {5C7847FB-C16D-47BD-BB17-1045669F9AA7} - - - - - + + + + + diff --git a/BepInEx.Shared/BuildInfoAttribute.cs b/BepInEx.Shared/BuildInfoAttribute.cs index 9f0f251..166048b 100644 --- a/BepInEx.Shared/BuildInfoAttribute.cs +++ b/BepInEx.Shared/BuildInfoAttribute.cs @@ -1,20 +1,19 @@ using System; -namespace BepInEx.Shared -{ - /// - /// This class is appended to AssemblyInfo.cs when BepInEx is built via a CI pipeline. - /// It is mainly intended to signify that the current build is not a release build and is special, like for instance a - /// bleeding edge build. - /// - [AttributeUsage(AttributeTargets.Assembly)] - public class BuildInfoAttribute : Attribute - { - public BuildInfoAttribute(string info) - { - Info = info; - } +namespace BepInEx.Shared; - public string Info { get; } +/// +/// This class is appended to AssemblyInfo.cs when BepInEx is built via a CI pipeline. +/// It is mainly intended to signify that the current build is not a release build and is special, like for instance a +/// bleeding edge build. +/// +[AttributeUsage(AttributeTargets.Assembly)] +public class BuildInfoAttribute : Attribute +{ + public BuildInfoAttribute(string info) + { + Info = info; } + + public string Info { get; } } diff --git a/BepInEx.Unity/BaseUnityPlugin.cs b/BepInEx.Unity/BaseUnityPlugin.cs index 655d792..8ea0ff2 100644 --- a/BepInEx.Unity/BaseUnityPlugin.cs +++ b/BepInEx.Unity/BaseUnityPlugin.cs @@ -3,52 +3,51 @@ using BepInEx.Configuration; using BepInEx.Logging; using UnityEngine; -namespace BepInEx +namespace BepInEx; + +/// +/// The base plugin type that is used by the BepInEx plugin loader. +/// +public abstract class BaseUnityPlugin : MonoBehaviour { /// - /// The base plugin type that is used by the BepInEx plugin loader. + /// Create a new instance of a plugin and all of its tied in objects. /// - public abstract class BaseUnityPlugin : MonoBehaviour + /// BepInPlugin attribute is missing. + protected BaseUnityPlugin() { - /// - /// Create a new instance of a plugin and all of its tied in objects. - /// - /// BepInPlugin attribute is missing. - protected BaseUnityPlugin() + 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."); + + Info = new PluginInfo { - 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."); + Metadata = metadata, + Instance = this, + Dependencies = MetadataHelper.GetDependencies(GetType()), + Processes = MetadataHelper.GetAttributes(GetType()), + Location = GetType().Assembly.Location + }; - Info = new PluginInfo - { - Metadata = metadata, - Instance = this, - Dependencies = MetadataHelper.GetDependencies(GetType()), - Processes = MetadataHelper.GetAttributes(GetType()), - Location = GetType().Assembly.Location - }; + Logger = Logging.Logger.CreateLogSource(metadata.Name); - Logger = Logging.Logger.CreateLogSource(metadata.Name); - - Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, metadata.GUID + ".cfg"), false, metadata); - } - - /// - /// Information about this plugin as it was loaded. - /// - public PluginInfo Info { get; } - - /// - /// Logger instance tied to this plugin. - /// - protected 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. - /// - public ConfigFile Config { get; } + Config = new ConfigFile(Utility.CombinePaths(Paths.ConfigPath, metadata.GUID + ".cfg"), false, metadata); } + + /// + /// Information about this plugin as it was loaded. + /// + public PluginInfo Info { get; } + + /// + /// Logger instance tied to this plugin. + /// + protected 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. + /// + public ConfigFile Config { get; } } diff --git a/BepInEx.Unity/BepInEx.Unity.csproj b/BepInEx.Unity/BepInEx.Unity.csproj index ce292e4..297d203 100644 --- a/BepInEx.Unity/BepInEx.Unity.csproj +++ b/BepInEx.Unity/BepInEx.Unity.csproj @@ -4,7 +4,7 @@ net35;netstandard2.0 BepInEx.Unity BepInEx.Unity - 9 + 10 diff --git a/BepInEx.Unity/BepInExInstance.cs b/BepInEx.Unity/BepInExInstance.cs index f731a2d..cda5895 100644 --- a/BepInEx.Unity/BepInExInstance.cs +++ b/BepInEx.Unity/BepInExInstance.cs @@ -1,9 +1,8 @@ using BepInEx.Unity.Bootstrap; -namespace BepInEx.Unity +namespace BepInEx.Unity; + +public static class BepInExInstance { - public static class BepInExInstance - { - public static UnityChainloader Chainloader { get; } - } + public static UnityChainloader Chainloader { get; } } diff --git a/BepInEx.Unity/Bootstrap/UnityChainloader.cs b/BepInEx.Unity/Bootstrap/UnityChainloader.cs index 15ad83c..8f846b2 100644 --- a/BepInEx.Unity/Bootstrap/UnityChainloader.cs +++ b/BepInEx.Unity/Bootstrap/UnityChainloader.cs @@ -11,90 +11,89 @@ using MonoMod.Utils; using UnityEngine; using Logger = BepInEx.Logging.Logger; -namespace BepInEx.Unity.Bootstrap +namespace BepInEx.Unity.Bootstrap; + +/// +/// The manager and loader for all plugins, and the entry point for BepInEx plugin system. +/// +public class UnityChainloader : BaseChainloader { + private static readonly ConfigEntry ConfigUnityLogging = ConfigFile.CoreConfig.Bind( + "Logging", "UnityLogListening", + true, + "Enables showing unity log messages in the BepInEx logging system."); + + private static readonly ConfigEntry ConfigDiskWriteUnityLog = ConfigFile.CoreConfig.Bind( + "Logging.Disk", "WriteUnityLog", + false, + "Include unity log messages in log file output."); + + private string _consoleTitle; + /// - /// The manager and loader for all plugins, and the entry point for BepInEx plugin system. + /// The GameObject that all plugins are attached to as components. /// - public class UnityChainloader : BaseChainloader + public static GameObject ManagerObject { get; private set; } + + protected override string ConsoleTitle => _consoleTitle; + + + // In some rare cases calling Application.unityVersion seems to cause MissingMethodException + // if a preloader patch applies Harmony patch to Chainloader.Initialize. + // The issue could be related to BepInEx being compiled against Unity 5.6 version of UnityEngine.dll, + // but the issue is apparently present with both official Harmony and HarmonyX + // We specifically prevent inlining to prevent early resolving + // TODO: Figure out better version obtaining mechanism (e.g. from globalmanagers) + private static string UnityVersion { - private static readonly ConfigEntry ConfigUnityLogging = ConfigFile.CoreConfig.Bind( - "Logging", "UnityLogListening", - true, - "Enables showing unity log messages in the BepInEx logging system."); - - private static readonly ConfigEntry ConfigDiskWriteUnityLog = ConfigFile.CoreConfig.Bind( - "Logging.Disk", "WriteUnityLog", - false, - "Include unity log messages in log file output."); - - private string _consoleTitle; - - /// - /// The GameObject that all plugins are attached to as components. - /// - public static GameObject ManagerObject { get; private set; } - - protected override string ConsoleTitle => _consoleTitle; - - - // In some rare cases calling Application.unityVersion seems to cause MissingMethodException - // if a preloader patch applies Harmony patch to Chainloader.Initialize. - // The issue could be related to BepInEx being compiled against Unity 5.6 version of UnityEngine.dll, - // but the issue is apparently present with both official Harmony and HarmonyX - // We specifically prevent inlining to prevent early resolving - // TODO: Figure out better version obtaining mechanism (e.g. from globalmanagers) - private static string UnityVersion - { - [MethodImpl(MethodImplOptions.NoInlining)] - get => Application.unityVersion; - } - - private static void StaticStart(string gameExePath = null) - { - var instance = new UnityChainloader(); - instance.Initialize(gameExePath); - instance.Execute(); - } - - - public override void Initialize(string gameExePath = null) - { - UnityTomlTypeConverters.AddUnityEngineConverters(); - - ThreadingHelper.Initialize(); - - ManagerObject = new GameObject("BepInEx_Manager") { hideFlags = HideFlags.HideAndDontSave }; - Object.DontDestroyOnLoad(ManagerObject); - - var productNameProp = - typeof(Application).GetProperty("productName", BindingFlags.Public | BindingFlags.Static); - _consoleTitle = - $"{CurrentAssemblyName} {CurrentAssemblyVersion} - {productNameProp?.GetValue(null, null) ?? Path.GetFileNameWithoutExtension(Process.GetCurrentProcess().ProcessName)}"; - - base.Initialize(gameExePath); - } - - protected override void InitializeLoggers() - { - base.InitializeLoggers(); - - Logger.Listeners.Add(new UnityLogListener()); - - if (!PlatformHelper.Is(Platform.Windows)) Logger.LogInfo($"Detected Unity version: v{UnityVersion}"); - - - if (!ConfigDiskWriteUnityLog.Value) DiskLogListener.BlacklistedSources.Add("Unity Log"); - - - ChainloaderLogHelper.RewritePreloaderLogs(); - - - if (ConfigUnityLogging.Value) - Logger.Sources.Add(new UnityLogSource()); - } - - public override BaseUnityPlugin LoadPlugin(PluginInfo pluginInfo, Assembly pluginAssembly) => - (BaseUnityPlugin) ManagerObject.AddComponent(pluginAssembly.GetType(pluginInfo.TypeName)); + [MethodImpl(MethodImplOptions.NoInlining)] + get => Application.unityVersion; } + + private static void StaticStart(string gameExePath = null) + { + var instance = new UnityChainloader(); + instance.Initialize(gameExePath); + instance.Execute(); + } + + + public override void Initialize(string gameExePath = null) + { + UnityTomlTypeConverters.AddUnityEngineConverters(); + + ThreadingHelper.Initialize(); + + ManagerObject = new GameObject("BepInEx_Manager") { hideFlags = HideFlags.HideAndDontSave }; + Object.DontDestroyOnLoad(ManagerObject); + + var productNameProp = + typeof(Application).GetProperty("productName", BindingFlags.Public | BindingFlags.Static); + _consoleTitle = + $"{CurrentAssemblyName} {CurrentAssemblyVersion} - {productNameProp?.GetValue(null, null) ?? Path.GetFileNameWithoutExtension(Process.GetCurrentProcess().ProcessName)}"; + + base.Initialize(gameExePath); + } + + protected override void InitializeLoggers() + { + base.InitializeLoggers(); + + Logger.Listeners.Add(new UnityLogListener()); + + if (!PlatformHelper.Is(Platform.Windows)) Logger.LogInfo($"Detected Unity version: v{UnityVersion}"); + + + if (!ConfigDiskWriteUnityLog.Value) DiskLogListener.BlacklistedSources.Add("Unity Log"); + + + ChainloaderLogHelper.RewritePreloaderLogs(); + + + if (ConfigUnityLogging.Value) + Logger.Sources.Add(new UnityLogSource()); + } + + public override BaseUnityPlugin LoadPlugin(PluginInfo pluginInfo, Assembly pluginAssembly) => + (BaseUnityPlugin) ManagerObject.AddComponent(pluginAssembly.GetType(pluginInfo.TypeName)); } diff --git a/BepInEx.Unity/Configuration/KeyboardShortcut.cs b/BepInEx.Unity/Configuration/KeyboardShortcut.cs index 596b043..7410a73 100644 --- a/BepInEx.Unity/Configuration/KeyboardShortcut.cs +++ b/BepInEx.Unity/Configuration/KeyboardShortcut.cs @@ -5,178 +5,177 @@ using BepInEx.Logging; using UnityEngine; using Logger = BepInEx.Logging.Logger; -namespace BepInEx.Configuration +namespace BepInEx.Configuration; + +/// +/// A keyboard shortcut that can be used in Update method to check if user presses a key combo. The shortcut is only +/// triggered when the user presses the exact combination. For example, F + LeftCtrl will trigger only if user +/// presses and holds only LeftCtrl, and then presses F. If any other keys are pressed, the shortcut will not trigger. +/// Can be used as a value of a setting in +/// to allow user to change this shortcut and have the changes saved. +/// How to use: Use in this class instead of in the +/// Update loop. +/// +public struct KeyboardShortcut { - /// - /// A keyboard shortcut that can be used in Update method to check if user presses a key combo. The shortcut is only - /// triggered when the user presses the exact combination. For example, F + LeftCtrl will trigger only if user - /// presses and holds only LeftCtrl, and then presses F. If any other keys are pressed, the shortcut will not trigger. - /// Can be used as a value of a setting in - /// to allow user to change this shortcut and have the changes saved. - /// How to use: Use in this class instead of in the - /// Update loop. - /// - public struct KeyboardShortcut + static KeyboardShortcut() { - static KeyboardShortcut() + TomlTypeConverter.AddConverter( + typeof(KeyboardShortcut), + new TypeConverter + { + ConvertToString = (o, type) => ((KeyboardShortcut) o).Serialize(), + ConvertToObject = (s, type) => Deserialize(s) + }); + } + + /// + /// Shortcut that never triggers. + /// + 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 readonly KeyCode[] _allKeys; + + /// + /// Create a new keyboard shortcut. + /// + /// Main key to press + /// Keys that should be held down before main key is registered + public KeyboardShortcut(KeyCode mainKey, params KeyCode[] modifiers) : this(new[] { mainKey }.Concat(modifiers) + .ToArray()) + { + if (mainKey == KeyCode.None && modifiers.Any()) + throw new + ArgumentException($"Can't set {nameof(mainKey)} to KeyCode.None if there are any {nameof(modifiers)}"); + } + + private KeyboardShortcut(KeyCode[] keys) + { + _allKeys = SanitizeKeys(keys); + } + + private static KeyCode[] SanitizeKeys(params KeyCode[] keys) + { + if (keys.Length == 0 || keys[0] == KeyCode.None) + return new[] { KeyCode.None }; + + return new[] { keys[0] }.Concat(keys.Skip(1).Distinct().Where(x => x != keys[0]).OrderBy(x => (int) x)) + .ToArray(); + } + + /// + /// Main key of the key combination. It has to be pressed / let go last for the combination to be triggered. + /// If the combination is empty, is returned. + /// + public KeyCode MainKey => _allKeys != null && _allKeys.Length > 0 ? _allKeys[0] : KeyCode.None; + + /// + /// Modifiers of the key combination, if any. + /// + public IEnumerable Modifiers => _allKeys?.Skip(1) ?? Enumerable.Empty(); + + /// + /// Attempt to deserialize key combination from the string. + /// + public static KeyboardShortcut Deserialize(string str) + { + try { - TomlTypeConverter.AddConverter( - typeof(KeyboardShortcut), - new TypeConverter - { - ConvertToString = (o, type) => ((KeyboardShortcut) o).Serialize(), - ConvertToObject = (s, type) => Deserialize(s) - }); + var parts = str.Split(new[] { ' ', '+', ',', ';', '|' }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => (KeyCode) Enum.Parse(typeof(KeyCode), x)).ToArray(); + return new KeyboardShortcut(parts); } - - /// - /// Shortcut that never triggers. - /// - 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[] + catch (SystemException ex) { - KeyCode.Mouse0, KeyCode.Mouse1, KeyCode.Mouse2, KeyCode.Mouse3, KeyCode.Mouse4, KeyCode.Mouse5, - KeyCode.Mouse6, KeyCode.None - }).ToArray(); - - private readonly KeyCode[] _allKeys; - - /// - /// Create a new keyboard shortcut. - /// - /// Main key to press - /// Keys that should be held down before main key is registered - public KeyboardShortcut(KeyCode mainKey, params KeyCode[] modifiers) : this(new[] { mainKey }.Concat(modifiers) - .ToArray()) - { - if (mainKey == KeyCode.None && modifiers.Any()) - throw new - ArgumentException($"Can't set {nameof(mainKey)} to KeyCode.None if there are any {nameof(modifiers)}"); - } - - private KeyboardShortcut(KeyCode[] keys) - { - _allKeys = SanitizeKeys(keys); - } - - private static KeyCode[] SanitizeKeys(params KeyCode[] keys) - { - if (keys.Length == 0 || keys[0] == KeyCode.None) - return new[] { KeyCode.None }; - - return new[] { keys[0] }.Concat(keys.Skip(1).Distinct().Where(x => x != keys[0]).OrderBy(x => (int) x)) - .ToArray(); - } - - /// - /// Main key of the key combination. It has to be pressed / let go last for the combination to be triggered. - /// If the combination is empty, is returned. - /// - public KeyCode MainKey => _allKeys != null && _allKeys.Length > 0 ? _allKeys[0] : KeyCode.None; - - /// - /// Modifiers of the key combination, if any. - /// - public IEnumerable Modifiers => _allKeys?.Skip(1) ?? Enumerable.Empty(); - - /// - /// Attempt to deserialize key combination from the string. - /// - public static KeyboardShortcut Deserialize(string str) - { - try - { - var parts = str.Split(new[] { ' ', '+', ',', ';', '|' }, StringSplitOptions.RemoveEmptyEntries) - .Select(x => (KeyCode) Enum.Parse(typeof(KeyCode), x)).ToArray(); - return new KeyboardShortcut(parts); - } - catch (SystemException ex) - { - Logger.Log(LogLevel.Error, "Failed to read keybind from settings: " + ex.Message); - return Empty; - } - } - - /// - /// Serialize the key combination into a user readable string. - /// - public string Serialize() - { - if (_allKeys == null) return string.Empty; - return string.Join(" + ", _allKeys.Select(x => x.ToString()).ToArray()); - } - - /// - /// Check if the main key was just pressed (Input.GetKeyDown), and specified modifier keys are all pressed - /// - public bool IsDown() - { - var mainKey = MainKey; - if (mainKey == KeyCode.None) return false; - - return Input.GetKeyDown(mainKey) && ModifierKeyTest(); - } - - /// - /// Check if the main key is currently held down (Input.GetKey), and specified modifier keys are all pressed - /// - public bool IsPressed() - { - var mainKey = MainKey; - if (mainKey == KeyCode.None) return false; - - return Input.GetKey(mainKey) && ModifierKeyTest(); - } - - /// - /// Check if the main key was just lifted (Input.GetKeyUp), and specified modifier keys are all pressed. - /// - public bool IsUp() - { - var mainKey = MainKey; - if (mainKey == KeyCode.None) return false; - - return Input.GetKeyUp(mainKey) && ModifierKeyTest(); - } - - private bool ModifierKeyTest() - { - var allKeys = _allKeys; - var mainKey = MainKey; - - var allModifiersPressed = allKeys.All(c => c == mainKey || Input.GetKey(c)); - if (!allModifiersPressed) return false; - - var noOtherModifiersPressed = _modifierBlockKeyCodes.All(c => !Input.GetKey(c) || allKeys.Contains(c)); - return noOtherModifiersPressed; - } - - /// - public override string ToString() - { - if (MainKey == KeyCode.None) return "Not set"; - - return string.Join(" + ", _allKeys.Select(c => c.ToString()).ToArray()); - } - - /// - public override bool Equals(object obj) => - obj is KeyboardShortcut shortcut && MainKey == shortcut.MainKey && - Modifiers.SequenceEqual(shortcut.Modifiers); - - /// - public override int GetHashCode() - { - if (MainKey == KeyCode.None) return 0; - - return _allKeys.Aggregate(_allKeys.Length, (current, item) => unchecked(current * 31 + (int) item)); + Logger.Log(LogLevel.Error, "Failed to read keybind from settings: " + ex.Message); + return Empty; } } + + /// + /// Serialize the key combination into a user readable string. + /// + public string Serialize() + { + if (_allKeys == null) return string.Empty; + return string.Join(" + ", _allKeys.Select(x => x.ToString()).ToArray()); + } + + /// + /// Check if the main key was just pressed (Input.GetKeyDown), and specified modifier keys are all pressed + /// + public bool IsDown() + { + var mainKey = MainKey; + if (mainKey == KeyCode.None) return false; + + return Input.GetKeyDown(mainKey) && ModifierKeyTest(); + } + + /// + /// Check if the main key is currently held down (Input.GetKey), and specified modifier keys are all pressed + /// + public bool IsPressed() + { + var mainKey = MainKey; + if (mainKey == KeyCode.None) return false; + + return Input.GetKey(mainKey) && ModifierKeyTest(); + } + + /// + /// Check if the main key was just lifted (Input.GetKeyUp), and specified modifier keys are all pressed. + /// + public bool IsUp() + { + var mainKey = MainKey; + if (mainKey == KeyCode.None) return false; + + return Input.GetKeyUp(mainKey) && ModifierKeyTest(); + } + + private bool ModifierKeyTest() + { + var allKeys = _allKeys; + var mainKey = MainKey; + + var allModifiersPressed = allKeys.All(c => c == mainKey || Input.GetKey(c)); + if (!allModifiersPressed) return false; + + var noOtherModifiersPressed = _modifierBlockKeyCodes.All(c => !Input.GetKey(c) || allKeys.Contains(c)); + return noOtherModifiersPressed; + } + + /// + public override string ToString() + { + if (MainKey == KeyCode.None) return "Not set"; + + return string.Join(" + ", _allKeys.Select(c => c.ToString()).ToArray()); + } + + /// + public override bool Equals(object obj) => + obj is KeyboardShortcut shortcut && MainKey == shortcut.MainKey && + Modifiers.SequenceEqual(shortcut.Modifiers); + + /// + public override int GetHashCode() + { + if (MainKey == KeyCode.None) return 0; + + return _allKeys.Aggregate(_allKeys.Length, (current, item) => unchecked(current * 31 + (int) item)); + } } diff --git a/BepInEx.Unity/Logging/UnityLogSource.cs b/BepInEx.Unity/Logging/UnityLogSource.cs index da39f75..4ac3b38 100644 --- a/BepInEx.Unity/Logging/UnityLogSource.cs +++ b/BepInEx.Unity/Logging/UnityLogSource.cs @@ -3,95 +3,94 @@ using System.Reflection; using BepInEx.Logging; using UnityEngine; -namespace BepInEx.Unity.Logging +namespace BepInEx.Unity.Logging; + +/// +/// Logs entries using Unity specific outputs. +/// +public class UnityLogSource : ILogSource { + private bool disposed; + /// - /// Logs entries using Unity specific outputs. + /// Creates a new Unity log source. /// - public class UnityLogSource : ILogSource + public UnityLogSource() { - private bool disposed; - - /// - /// Creates a new Unity log source. - /// - public UnityLogSource() - { - InternalUnityLogMessage += UnityLogMessageHandler; - } - - /// - public string SourceName { get; } = "Unity Log"; - - /// - public event EventHandler LogEvent; - - /// - public void Dispose() - { - if (!disposed) - { - InternalUnityLogMessage -= UnityLogMessageHandler; - disposed = true; - } - } - - private void UnityLogMessageHandler(object sender, LogEventArgs eventArgs) - { - var newEventArgs = new LogEventArgs(eventArgs.Data, eventArgs.Level, this); - LogEvent?.Invoke(this, newEventArgs); - } - - #region Static Unity handler - - private static event EventHandler InternalUnityLogMessage; - - static UnityLogSource() - { - var callback = new Application.LogCallback(OnUnityLogMessageReceived); - - var logEvent = - typeof(Application).GetEvent("logMessageReceived", BindingFlags.Public | BindingFlags.Static); - if (logEvent != null) - { - logEvent.AddEventHandler(null, callback); - //UnsubscribeAction = () => logEvent.RemoveEventHandler(null, callback); - } - else - { - var registerLogCallback = - typeof(Application).GetMethod("RegisterLogCallback", BindingFlags.Public | BindingFlags.Static); - registerLogCallback.Invoke(null, new object[] { callback }); - //UnsubscribeAction = () => registerLogCallback.Invoke(null, new object[] { null }); - } - } - - private static void OnUnityLogMessageReceived(string message, string stackTrace, LogType type) - { - LogLevel logLevel; - - switch (type) - { - case LogType.Error: - case LogType.Assert: - case LogType.Exception: - logLevel = LogLevel.Error; - break; - case LogType.Warning: - logLevel = LogLevel.Warning; - break; - case LogType.Log: - default: - logLevel = LogLevel.Info; - break; - } - - if (type == LogType.Exception) - message += $"\nStack trace:\n{stackTrace}"; - - InternalUnityLogMessage?.Invoke(null, new LogEventArgs(message, logLevel, null)); - } - - #endregion + InternalUnityLogMessage += UnityLogMessageHandler; } + + /// + public string SourceName { get; } = "Unity Log"; + + /// + public event EventHandler LogEvent; + + /// + public void Dispose() + { + if (!disposed) + { + InternalUnityLogMessage -= UnityLogMessageHandler; + disposed = true; + } + } + + private void UnityLogMessageHandler(object sender, LogEventArgs eventArgs) + { + var newEventArgs = new LogEventArgs(eventArgs.Data, eventArgs.Level, this); + LogEvent?.Invoke(this, newEventArgs); + } + + #region Static Unity handler + + private static event EventHandler InternalUnityLogMessage; + + static UnityLogSource() + { + var callback = new Application.LogCallback(OnUnityLogMessageReceived); + + var logEvent = + typeof(Application).GetEvent("logMessageReceived", BindingFlags.Public | BindingFlags.Static); + if (logEvent != null) + { + logEvent.AddEventHandler(null, callback); + //UnsubscribeAction = () => logEvent.RemoveEventHandler(null, callback); + } + else + { + var registerLogCallback = + typeof(Application).GetMethod("RegisterLogCallback", BindingFlags.Public | BindingFlags.Static); + registerLogCallback.Invoke(null, new object[] { callback }); + //UnsubscribeAction = () => registerLogCallback.Invoke(null, new object[] { null }); + } + } + + private static void OnUnityLogMessageReceived(string message, string stackTrace, LogType type) + { + LogLevel logLevel; + + switch (type) + { + case LogType.Error: + case LogType.Assert: + case LogType.Exception: + logLevel = LogLevel.Error; + break; + case LogType.Warning: + logLevel = LogLevel.Warning; + break; + case LogType.Log: + default: + logLevel = LogLevel.Info; + break; + } + + if (type == LogType.Exception) + message += $"\nStack trace:\n{stackTrace}"; + + InternalUnityLogMessage?.Invoke(null, new LogEventArgs(message, logLevel, null)); + } + + #endregion } diff --git a/BepInEx.Unity/ThreadingHelper.cs b/BepInEx.Unity/ThreadingHelper.cs index ae3cdaa..d5cf50c 100644 --- a/BepInEx.Unity/ThreadingHelper.cs +++ b/BepInEx.Unity/ThreadingHelper.cs @@ -8,294 +8,293 @@ using BepInEx.Logging; using UnityEngine; using Logger = BepInEx.Logging.Logger; -namespace BepInEx +namespace BepInEx; + +/// +/// Provides methods for running code on other threads and synchronizing with the main thread. +/// +public sealed class ThreadingHelper : MonoBehaviour, ISynchronizeInvoke { + private readonly object _invokeLock = new(); + private Action _invokeList; + private Thread _mainThread; + /// - /// Provides methods for running code on other threads and synchronizing with the main thread. + /// Current instance of the helper. /// - public sealed class ThreadingHelper : MonoBehaviour, ISynchronizeInvoke + public static ThreadingHelper Instance { get; private set; } + + /// + /// Gives methods for invoking delegates on the main unity thread, both synchronously and asynchronously. + /// Can be used in many built-in framework types, for example + /// + /// and to make their events fire on the main unity thread. + /// + public static ISynchronizeInvoke SynchronizingObject => Instance; + + private void Update() { - private readonly object _invokeLock = new(); - private Action _invokeList; - private Thread _mainThread; + // The CurrentThread can change between Awake and later methods, it's safest to get it here. + if (_mainThread == null) + _mainThread = Thread.CurrentThread; - /// - /// Current instance of the helper. - /// - public static ThreadingHelper Instance { get; private set; } + // Safe to do outside of lock because nothing can remove callbacks, at worst we execute with 1 frame delay + if (_invokeList == null) return; - /// - /// Gives methods for invoking delegates on the main unity thread, both synchronously and asynchronously. - /// Can be used in many built-in framework types, for example - /// - /// and to make their events fire on the main unity thread. - /// - public static ISynchronizeInvoke SynchronizingObject => Instance; - - private void Update() + Action toRun; + lock (_invokeLock) { - // The CurrentThread can change between Awake and later methods, it's safest to get it here. - if (_mainThread == null) - _mainThread = Thread.CurrentThread; + toRun = _invokeList; + _invokeList = null; + } - // Safe to do outside of lock because nothing can remove callbacks, at worst we execute with 1 frame delay - if (_invokeList == null) return; - - Action toRun; - lock (_invokeLock) + // Need to execute outside of the lock in case the callback itself calls Invoke we could deadlock + // The invocation would also block any threads that call Invoke + foreach (var action in toRun.GetInvocationList().Cast()) + try { - toRun = _invokeList; - _invokeList = null; + action(); } - - // Need to execute outside of the lock in case the callback itself calls Invoke we could deadlock - // The invocation would also block any threads that call Invoke - foreach (var action in toRun.GetInvocationList().Cast()) - try - { - action(); - } - catch (Exception ex) - { - LogInvocationException(ex); - } - } - - internal static void Initialize() - { - var go = new GameObject("BepInEx_ThreadingHelper") { hideFlags = HideFlags.HideAndDontSave }; - DontDestroyOnLoad(go); - Instance = go.AddComponent(); - } - - /// - /// Queue the delegate to be invoked on the main unity thread. Use to synchronize your threads. - /// - public void StartSyncInvoke(Action action) - { - if (action == null) throw new ArgumentNullException(nameof(action)); - - lock (_invokeLock) + catch (Exception ex) { - _invokeList += action; + LogInvocationException(ex); } - } + } - /// - /// Queue the delegate to be invoked on a background thread. Use this to run slow tasks without affecting the game. - /// NOTE: Most of Unity API can not be accessed while running on another thread! - /// - /// - /// Task to be executed on another thread. Can optionally return an Action that will be executed on the main thread. - /// You can use this action to return results of your work safely. Return null if this is not needed. - /// - public void StartAsyncInvoke(Func action) - { - void DoWork(object _) - { - try - { - var result = action(); - - if (result != null) - StartSyncInvoke(result); - } - catch (Exception ex) - { - LogInvocationException(ex); - } - } - - if (!ThreadPool.QueueUserWorkItem(DoWork)) - throw new NotSupportedException("Failed to queue the action on ThreadPool"); - } - - private static void LogInvocationException(Exception ex) - { - Logger.Log(LogLevel.Error, ex); - if (ex.InnerException != null) Logger.Log(LogLevel.Error, "INNER: " + ex.InnerException); - } - - #region ISynchronizeInvoke - - IAsyncResult ISynchronizeInvoke.BeginInvoke(Delegate method, object[] args) - { - var result = new InvokeResult(); - - object Invoke() - { - try - { - return method.DynamicInvoke(args); - } - catch (Exception ex) - { - result.ExceptionThrown = true; - return ex; - } - } - - if (!InvokeRequired) - result.Finish(Invoke(), true); - else - StartSyncInvoke(() => result.Finish(Invoke(), false)); - - return result; - } - - object ISynchronizeInvoke.EndInvoke(IAsyncResult result) - { - var invokeResult = (InvokeResult) result; - invokeResult.AsyncWaitHandle.WaitOne(); - - if (invokeResult.ExceptionThrown) - throw (Exception) invokeResult.AsyncState; - return invokeResult.AsyncState; - } - - object ISynchronizeInvoke.Invoke(Delegate method, object[] args) - { - var invokeResult = ((ISynchronizeInvoke) this).BeginInvoke(method, args); - return ((ISynchronizeInvoke) this).EndInvoke(invokeResult); - } - - /// - /// False if current code is executing on the main unity thread, otherwise True. - /// Warning: Will return true before the first frame finishes (i.e. inside plugin Awake and Start methods). - /// - /// - public bool InvokeRequired => _mainThread == null || _mainThread != Thread.CurrentThread; - - private sealed class InvokeResult : IAsyncResult - { - internal bool ExceptionThrown; - - public InvokeResult() - { - AsyncWaitHandle = new EventWaitHandle(false, EventResetMode.ManualReset); - } - - public bool IsCompleted { get; private set; } - public WaitHandle AsyncWaitHandle { get; } - public object AsyncState { get; private set; } - public bool CompletedSynchronously { get; private set; } - - public void Finish(object result, bool completedSynchronously) - { - AsyncState = result; - CompletedSynchronously = completedSynchronously; - IsCompleted = true; - ((EventWaitHandle) AsyncWaitHandle).Set(); - } - } - - #endregion + internal static void Initialize() + { + var go = new GameObject("BepInEx_ThreadingHelper") { hideFlags = HideFlags.HideAndDontSave }; + DontDestroyOnLoad(go); + Instance = go.AddComponent(); } /// - /// Convenience extensions for utilizing multiple threads and using the . + /// Queue the delegate to be invoked on the main unity thread. Use to synchronize your threads. /// - public static class ThreadingExtensions + public void StartSyncInvoke(Action action) { - /// - public static IEnumerable RunParallel(this IEnumerable data, - Func work, - int workerCount = -1) + if (action == null) throw new ArgumentNullException(nameof(action)); + + lock (_invokeLock) { - foreach (var result in RunParallel(data.ToList(), work)) - yield return result; + _invokeList += action; + } + } + + /// + /// Queue the delegate to be invoked on a background thread. Use this to run slow tasks without affecting the game. + /// NOTE: Most of Unity API can not be accessed while running on another thread! + /// + /// + /// Task to be executed on another thread. Can optionally return an Action that will be executed on the main thread. + /// You can use this action to return results of your work safely. Return null if this is not needed. + /// + public void StartAsyncInvoke(Func action) + { + void DoWork(object _) + { + try + { + var result = action(); + + if (result != null) + StartSyncInvoke(result); + } + catch (Exception ex) + { + LogInvocationException(ex); + } } - /// - /// Apply a function to a collection of data by spreading the work on multiple threads. - /// Outputs of the functions are returned to the current thread and yielded one by one. - /// - /// Type of the input values. - /// Type of the output values. - /// Input values for the work function. - /// Function to apply to the data on multiple threads at once. - /// Number of worker threads. By default SystemInfo.processorCount is used. - /// - /// An exception was thrown inside one of the threads, and the operation was - /// aborted. - /// - /// Need at least 1 workerCount. - public static IEnumerable RunParallel(this IList data, - Func work, - int workerCount = -1) + if (!ThreadPool.QueueUserWorkItem(DoWork)) + throw new NotSupportedException("Failed to queue the action on ThreadPool"); + } + + private static void LogInvocationException(Exception ex) + { + Logger.Log(LogLevel.Error, ex); + if (ex.InnerException != null) Logger.Log(LogLevel.Error, "INNER: " + ex.InnerException); + } + + #region ISynchronizeInvoke + + IAsyncResult ISynchronizeInvoke.BeginInvoke(Delegate method, object[] args) + { + var result = new InvokeResult(); + + object Invoke() { - if (workerCount < 0) - workerCount = Mathf.Max(2, Environment.ProcessorCount); - else if (workerCount == 0) - throw new ArgumentException("Need at least 1 worker", nameof(workerCount)); - - var perThreadCount = Mathf.CeilToInt(data.Count / (float) workerCount); - var doneCount = 0; - - var lockObj = new object(); - var are = new ManualResetEvent(false); - IEnumerable doneItems = null; - Exception exceptionThrown = null; - - // Start threads to process the data - for (var i = 0; i < workerCount; i++) + try { - var first = i * perThreadCount; - var last = Mathf.Min(first + perThreadCount, data.Count); - ThreadPool.QueueUserWorkItem( - _ => + return method.DynamicInvoke(args); + } + catch (Exception ex) + { + result.ExceptionThrown = true; + return ex; + } + } + + if (!InvokeRequired) + result.Finish(Invoke(), true); + else + StartSyncInvoke(() => result.Finish(Invoke(), false)); + + return result; + } + + object ISynchronizeInvoke.EndInvoke(IAsyncResult result) + { + var invokeResult = (InvokeResult) result; + invokeResult.AsyncWaitHandle.WaitOne(); + + if (invokeResult.ExceptionThrown) + throw (Exception) invokeResult.AsyncState; + return invokeResult.AsyncState; + } + + object ISynchronizeInvoke.Invoke(Delegate method, object[] args) + { + var invokeResult = ((ISynchronizeInvoke) this).BeginInvoke(method, args); + return ((ISynchronizeInvoke) this).EndInvoke(invokeResult); + } + + /// + /// False if current code is executing on the main unity thread, otherwise True. + /// Warning: Will return true before the first frame finishes (i.e. inside plugin Awake and Start methods). + /// + /// + public bool InvokeRequired => _mainThread == null || _mainThread != Thread.CurrentThread; + + private sealed class InvokeResult : IAsyncResult + { + internal bool ExceptionThrown; + + public InvokeResult() + { + AsyncWaitHandle = new EventWaitHandle(false, EventResetMode.ManualReset); + } + + public bool IsCompleted { get; private set; } + public WaitHandle AsyncWaitHandle { get; } + public object AsyncState { get; private set; } + public bool CompletedSynchronously { get; private set; } + + public void Finish(object result, bool completedSynchronously) + { + AsyncState = result; + CompletedSynchronously = completedSynchronously; + IsCompleted = true; + ((EventWaitHandle) AsyncWaitHandle).Set(); + } + } + + #endregion +} + +/// +/// Convenience extensions for utilizing multiple threads and using the . +/// +public static class ThreadingExtensions +{ + /// + public static IEnumerable RunParallel(this IEnumerable data, + Func work, + int workerCount = -1) + { + foreach (var result in RunParallel(data.ToList(), work)) + yield return result; + } + + /// + /// Apply a function to a collection of data by spreading the work on multiple threads. + /// Outputs of the functions are returned to the current thread and yielded one by one. + /// + /// Type of the input values. + /// Type of the output values. + /// Input values for the work function. + /// Function to apply to the data on multiple threads at once. + /// Number of worker threads. By default SystemInfo.processorCount is used. + /// + /// An exception was thrown inside one of the threads, and the operation was + /// aborted. + /// + /// Need at least 1 workerCount. + public static IEnumerable RunParallel(this IList data, + Func work, + int workerCount = -1) + { + if (workerCount < 0) + workerCount = Mathf.Max(2, Environment.ProcessorCount); + else if (workerCount == 0) + throw new ArgumentException("Need at least 1 worker", nameof(workerCount)); + + var perThreadCount = Mathf.CeilToInt(data.Count / (float) workerCount); + var doneCount = 0; + + var lockObj = new object(); + var are = new ManualResetEvent(false); + IEnumerable doneItems = null; + Exception exceptionThrown = null; + + // Start threads to process the data + for (var i = 0; i < workerCount; i++) + { + var first = i * perThreadCount; + var last = Mathf.Min(first + perThreadCount, data.Count); + ThreadPool.QueueUserWorkItem( + _ => + { + var results = new List(perThreadCount); + + try { - var results = new List(perThreadCount); - - try + for (var dataIndex = first; dataIndex < last; dataIndex++) { - for (var dataIndex = first; dataIndex < last; dataIndex++) - { - if (exceptionThrown != null) break; - results.Add(work(data[dataIndex])); - } - } - catch (Exception ex) - { - exceptionThrown = ex; + if (exceptionThrown != null) break; + results.Add(work(data[dataIndex])); } + } + catch (Exception ex) + { + exceptionThrown = ex; + } - lock (lockObj) - { - doneItems = doneItems == null - ? results - : results.Concat(doneItems); - doneCount++; - are.Set(); - } - }); - } - - // Main thread waits for results and returns them until all threads finish - while (true) - { - are.WaitOne(); - - IEnumerable toOutput; - bool isDone; - lock (lockObj) - { - toOutput = doneItems; - doneItems = null; - isDone = doneCount == workerCount; - } - - if (toOutput != null) - foreach (var doneItem in toOutput) - yield return doneItem; - - if (isDone) - break; - } - - if (exceptionThrown != null) - throw new TargetInvocationException("An exception was thrown inside one of the threads", - exceptionThrown); + lock (lockObj) + { + doneItems = doneItems == null + ? results + : results.Concat(doneItems); + doneCount++; + are.Set(); + } + }); } + + // Main thread waits for results and returns them until all threads finish + while (true) + { + are.WaitOne(); + + IEnumerable toOutput; + bool isDone; + lock (lockObj) + { + toOutput = doneItems; + doneItems = null; + isDone = doneCount == workerCount; + } + + if (toOutput != null) + foreach (var doneItem in toOutput) + yield return doneItem; + + if (isDone) + break; + } + + if (exceptionThrown != null) + throw new TargetInvocationException("An exception was thrown inside one of the threads", + exceptionThrown); } } diff --git a/BepInEx.Unity/UnityTomlTypeConverters.cs b/BepInEx.Unity/UnityTomlTypeConverters.cs index 9d16ad7..8e5615c 100644 --- a/BepInEx.Unity/UnityTomlTypeConverters.cs +++ b/BepInEx.Unity/UnityTomlTypeConverters.cs @@ -3,39 +3,38 @@ using System.Runtime.CompilerServices; using BepInEx.Configuration; using UnityEngine; -namespace BepInEx.Unity +namespace BepInEx.Unity; + +/// +/// Config types that are unity specific +/// +internal static class UnityTomlTypeConverters { - /// - /// Config types that are unity specific - /// - internal static class UnityTomlTypeConverters + [MethodImpl(MethodImplOptions.NoInlining)] + public static void AddUnityEngineConverters() { - [MethodImpl(MethodImplOptions.NoInlining)] - public static void AddUnityEngineConverters() + var colorConverter = new TypeConverter { - var colorConverter = new TypeConverter + ConvertToString = (obj, type) => ColorUtility.ToHtmlStringRGBA((Color) obj), + ConvertToObject = (str, type) => { - ConvertToString = (obj, type) => ColorUtility.ToHtmlStringRGBA((Color) obj), - ConvertToObject = (str, type) => - { - if (!ColorUtility.TryParseHtmlString("#" + str.Trim('#', ' '), out var c)) - throw new FormatException("Invalid color string, expected hex #RRGGBBAA"); - return c; - } - }; + if (!ColorUtility.TryParseHtmlString("#" + str.Trim('#', ' '), out var c)) + throw new FormatException("Invalid color string, expected hex #RRGGBBAA"); + return c; + } + }; - TomlTypeConverter.AddConverter(typeof(Color), colorConverter); + TomlTypeConverter.AddConverter(typeof(Color), colorConverter); - var jsonConverter = new TypeConverter - { - ConvertToString = (obj, type) => JsonUtility.ToJson(obj), - ConvertToObject = (str, type) => JsonUtility.FromJson(type: type, json: str) - }; + var jsonConverter = new TypeConverter + { + ConvertToString = (obj, type) => JsonUtility.ToJson(obj), + ConvertToObject = (str, type) => JsonUtility.FromJson(type: type, json: str) + }; - TomlTypeConverter.AddConverter(typeof(Vector2), jsonConverter); - TomlTypeConverter.AddConverter(typeof(Vector3), jsonConverter); - TomlTypeConverter.AddConverter(typeof(Vector4), jsonConverter); - TomlTypeConverter.AddConverter(typeof(Quaternion), jsonConverter); - } + TomlTypeConverter.AddConverter(typeof(Vector2), jsonConverter); + TomlTypeConverter.AddConverter(typeof(Vector3), jsonConverter); + TomlTypeConverter.AddConverter(typeof(Vector4), jsonConverter); + TomlTypeConverter.AddConverter(typeof(Quaternion), jsonConverter); } } diff --git a/BepInExTests/AssemblyInit.cs b/BepInExTests/AssemblyInit.cs index 74c9220..9fc78d2 100644 --- a/BepInExTests/AssemblyInit.cs +++ b/BepInExTests/AssemblyInit.cs @@ -1,26 +1,25 @@ using System.IO; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace BepInEx.Tests +namespace BepInEx.Tests; + +[TestClass] +public class AssemblyInit { - [TestClass] - public class AssemblyInit + private static string _testPath; + + [AssemblyInitialize] + public static void InitAss(TestContext context) { - private static string _testPath; + _testPath = Path.Combine(Path.GetTempPath(), "BepinexTestDir"); + Directory.CreateDirectory(_testPath); - [AssemblyInitialize] - public static void InitAss(TestContext context) - { - _testPath = Path.Combine(Path.GetTempPath(), "BepinexTestDir"); - Directory.CreateDirectory(_testPath); + var exePath = Path.Combine(_testPath, "Text.exe"); + File.WriteAllBytes(exePath, new byte[] { }); - var exePath = Path.Combine(_testPath, "Text.exe"); - File.WriteAllBytes(exePath, new byte[] { }); - - Paths.SetExecutablePath(_testPath); - } - - [AssemblyCleanup] - public static void CleanupAss() => Directory.Delete(_testPath, true); + Paths.SetExecutablePath(_testPath); } + + [AssemblyCleanup] + public static void CleanupAss() => Directory.Delete(_testPath, true); } diff --git a/BepInExTests/Configuration/ConfigFileTests.cs b/BepInExTests/Configuration/ConfigFileTests.cs index e74f13d..124a1d9 100644 --- a/BepInExTests/Configuration/ConfigFileTests.cs +++ b/BepInExTests/Configuration/ConfigFileTests.cs @@ -7,312 +7,311 @@ using System.Threading; using Microsoft.VisualStudio.TestTools.UnitTesting; using UEC::UnityEngine; -namespace BepInEx.Configuration.Tests +namespace BepInEx.Configuration.Tests; + +[TestClass] +public class ConfigFileTests { - [TestClass] - public class ConfigFileTests + private static ConcurrentBag _toRemove; + + [ClassInitialize] + public static void Init(TestContext context) => _toRemove = new ConcurrentBag(); + + [ClassCleanup] + public static void Cleanup() { - private static ConcurrentBag _toRemove; + foreach (var configFile in _toRemove) + File.Delete(configFile.ConfigFilePath); + } - [ClassInitialize] - public static void Init(TestContext context) => _toRemove = new ConcurrentBag(); + private static ConfigFile MakeConfig() + { + var configPath = Path.GetTempFileName(); + if (configPath == null) throw new InvalidOperationException("Wtf"); + var config = new ConfigFile(configPath, true); + _toRemove.Add(config); + return config; + } - [ClassCleanup] - public static void Cleanup() + [TestMethod] + public void SaveTest() => MakeConfig().Save(); + + [TestMethod] + public void SaveTestValueChange() + { + var c = MakeConfig(); + + var w = c.Bind("Cat", "Key", 0, new ConfigDescription("Test")); + var lines = File.ReadAllLines(c.ConfigFilePath); + Assert.AreEqual(1, lines.Count(x => x.Equals("[Cat]"))); + Assert.AreEqual(1, lines.Count(x => x.Equals("## Test"))); + Assert.AreEqual(1, lines.Count(x => x.Equals("Key = 0"))); + + c.Save(); + lines = File.ReadAllLines(c.ConfigFilePath); + Assert.AreEqual(1, lines.Count(x => x.Equals("[Cat]"))); + Assert.AreEqual(1, lines.Count(x => x.Equals("## Test"))); + Assert.AreEqual(1, lines.Count(x => x.Equals("Key = 0"))); + + w.Value = 69; + lines = File.ReadAllLines(c.ConfigFilePath); + Assert.AreEqual(1, lines.Count(x => x.Equals("[Cat]"))); + Assert.AreEqual(1, lines.Count(x => x.Equals("## Test"))); + Assert.AreEqual(1, lines.Count(x => x.Equals("Key = 69"))); + } + + [TestMethod] + public void AutoSaveTest() + { + var c = MakeConfig(); + c.Bind("Cat", "Key", 0, new ConfigDescription("Test")); + + var eventFired = new AutoResetEvent(false); + c.ConfigReloaded += (sender, args) => eventFired.Set(); + + c.Save(); + + Assert.IsFalse(eventFired.WaitOne(200)); + } + + [TestMethod] + public void ReadTest() + { + var c = MakeConfig(); + File.WriteAllText(c.ConfigFilePath, "[Cat]\n# Test\nKey=1\n"); + c.Reload(); + var w = c.Bind("Cat", "Key", 0, "Test"); + Assert.AreEqual(w.Value, 1); + var w2 = c.Bind("Cat", "Key2", 0, new ConfigDescription("Test")); + Assert.AreEqual(w2.Value, 0); + } + + [TestMethod] + public void ReadTest2() + { + var c = MakeConfig(); + var w = c.Bind("Cat", "Key", 0, new ConfigDescription("Test")); + Assert.AreEqual(w.Value, 0); + + File.WriteAllText(c.ConfigFilePath, "[Cat]\n# Test\nKey = 1 \n"); + + c.Reload(); + Assert.AreEqual(w.Value, 1); + } + + [TestMethod] + public void FileWatchTestNoSelfReload() + { + var c = MakeConfig(); + + var eventFired = new AutoResetEvent(false); + c.ConfigReloaded += (sender, args) => eventFired.Set(); + + c.Save(); + + Assert.IsFalse(eventFired.WaitOne(200)); + } + + [TestMethod] + public void EventTestWrapper() + { + var c = MakeConfig(); + var w = c.Bind("Cat", "Key", 0, new ConfigDescription("Test")); + + File.WriteAllText(c.ConfigFilePath, "[Cat]\n# Test\nKey=1\n"); + + var eventFired = false; + w.SettingChanged += (sender, args) => eventFired = true; + + c.Reload(); + + Assert.IsTrue(eventFired); + } + + [TestMethod] + public void PersistHomeless() + { + var c = MakeConfig(); + + File.WriteAllText(c.ConfigFilePath, "[Cat]\n# Test\nKey=1\nHomeless=0"); + c.Reload(); + + var w = c.Bind("Cat", "Key", 0, new ConfigDescription("Test")); + + c.Save(); + + Assert.IsTrue(File.ReadAllLines(c.ConfigFilePath) + .Single(x => x.StartsWith("Homeless") && x.EndsWith("0")) != null); + } + + [TestMethod] + public void EventTestReload() + { + var c = MakeConfig(); + var eventFired = false; + + var w = c.Bind("Cat", "Key", 0, new ConfigDescription("Test")); + w.SettingChanged += (sender, args) => eventFired = true; + + Assert.IsFalse(eventFired); + + File.WriteAllText(c.ConfigFilePath, "[Cat]\n# Test\nKey=1\n"); + c.Reload(); + + Assert.IsTrue(eventFired); + } + + [TestMethod] + public void ValueRangeTest() + { + var c = MakeConfig(); + var w = c.Bind("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange(0, 2))); + + Assert.AreEqual(0, w.Value); + w.Value = 2; + Assert.AreEqual(2, w.Value); + w.Value = -2; + Assert.AreEqual(0, w.Value); + w.Value = 4; + Assert.AreEqual(2, w.Value); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void ValueRangeBadTypeTest() + { + var c = MakeConfig(); + c.Bind("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange(1, 2))); + Assert.Fail(); + } + + [TestMethod] + public void ValueRangeDefaultTest() + { + var c = MakeConfig(); + var w = c.Bind("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange(1, 2))); + + Assert.AreEqual(w.Value, 1); + } + + [TestMethod] + public void ValueRangeLoadTest() + { + var c = MakeConfig(); + + File.WriteAllText(c.ConfigFilePath, "[Cat]\nKey = 1\n"); + c.Reload(); + + var w = c.Bind("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange(0, 2))); + + Assert.AreEqual(w.Value, 1); + + File.WriteAllText(c.ConfigFilePath, "[Cat]\nKey = 5\n"); + c.Reload(); + + Assert.AreEqual(w.Value, 2); + } + + [TestMethod] + public void ValueListTest() + { + var c = MakeConfig(); + var w = c.Bind("Cat", "Key", "kek", + new ConfigDescription("Test", new AcceptableValueList("lel", "kek", "wew", "why"))); + + Assert.AreEqual("kek", w.Value); + w.Value = "wew"; + Assert.AreEqual("wew", w.Value); + w.Value = "no"; + Assert.AreEqual("lel", w.Value); + w.Value = null; + Assert.AreEqual("lel", w.Value); + } + + [TestMethod] + public void KeyboardShortcutTest() + { + var shortcut = new KeyboardShortcut(KeyCode.H, KeyCode.O, KeyCode.R, KeyCode.S, KeyCode.E, KeyCode.Y); + var s = shortcut.Serialize(); + var d = KeyboardShortcut.Deserialize(s); + Assert.AreEqual(shortcut, d); + + var c = MakeConfig(); + var w = c.Bind("Cat", "Key", new KeyboardShortcut(KeyCode.A, KeyCode.LeftShift)); + Assert.AreEqual(new KeyboardShortcut(KeyCode.A, KeyCode.LeftShift), w.Value); + + w.Value = shortcut; + c.Reload(); + Assert.AreEqual(shortcut, w.Value); + } + + [TestMethod] + public void KeyboardShortcutTest2() + { + Assert.AreEqual(KeyboardShortcut.Empty, new KeyboardShortcut()); + + var c = MakeConfig(); + + var w = c.Bind("Cat", "Key", KeyboardShortcut.Empty, new ConfigDescription("Test")); + + Assert.AreEqual("", w.GetSerializedValue()); + + w.SetSerializedValue(w.GetSerializedValue()); + Assert.AreEqual(KeyboardShortcut.Empty, w.Value); + + var testShortcut = new KeyboardShortcut(KeyCode.A, KeyCode.B, KeyCode.C); + w.Value = testShortcut; + + w.SetSerializedValue(w.GetSerializedValue()); + Assert.AreEqual(testShortcut, w.Value); + + c.Save(); + c.Reload(); + + Assert.AreEqual(testShortcut, w.Value); + } + + [TestMethod] + public void StringEscapeChars() + { + const string testVal = "new line\n test \t\0"; + + var c = MakeConfig(); + var w = c.Bind("Cat", "Key", testVal, new ConfigDescription("Test")); + + Assert.AreEqual(testVal, w.Value); + Assert.IsFalse(w.GetSerializedValue().Any(x => x == '\n')); + + w.SetSerializedValue(w.GetSerializedValue()); + Assert.AreEqual(testVal, w.Value); + + c.Save(); + c.Reload(); + + Assert.AreEqual(testVal, w.Value); + } + + [TestMethod] + public void UnescapedPathString() + { + var c = MakeConfig(); + var w = c.Bind("Cat", "Key", "", new ConfigDescription("Test")); + + var unescaped = @"D:\test\p ath"; + foreach (var testVal in new[] { unescaped, @"D:\\test\\p ath" }) { - foreach (var configFile in _toRemove) - File.Delete(configFile.ConfigFilePath); - } - - private static ConfigFile MakeConfig() - { - var configPath = Path.GetTempFileName(); - if (configPath == null) throw new InvalidOperationException("Wtf"); - var config = new ConfigFile(configPath, true); - _toRemove.Add(config); - return config; - } - - [TestMethod] - public void SaveTest() => MakeConfig().Save(); - - [TestMethod] - public void SaveTestValueChange() - { - var c = MakeConfig(); - - var w = c.Bind("Cat", "Key", 0, new ConfigDescription("Test")); - var lines = File.ReadAllLines(c.ConfigFilePath); - Assert.AreEqual(1, lines.Count(x => x.Equals("[Cat]"))); - Assert.AreEqual(1, lines.Count(x => x.Equals("## Test"))); - Assert.AreEqual(1, lines.Count(x => x.Equals("Key = 0"))); - - c.Save(); - lines = File.ReadAllLines(c.ConfigFilePath); - Assert.AreEqual(1, lines.Count(x => x.Equals("[Cat]"))); - Assert.AreEqual(1, lines.Count(x => x.Equals("## Test"))); - Assert.AreEqual(1, lines.Count(x => x.Equals("Key = 0"))); - - w.Value = 69; - lines = File.ReadAllLines(c.ConfigFilePath); - Assert.AreEqual(1, lines.Count(x => x.Equals("[Cat]"))); - Assert.AreEqual(1, lines.Count(x => x.Equals("## Test"))); - Assert.AreEqual(1, lines.Count(x => x.Equals("Key = 69"))); - } - - [TestMethod] - public void AutoSaveTest() - { - var c = MakeConfig(); - c.Bind("Cat", "Key", 0, new ConfigDescription("Test")); - - var eventFired = new AutoResetEvent(false); - c.ConfigReloaded += (sender, args) => eventFired.Set(); - - c.Save(); - - Assert.IsFalse(eventFired.WaitOne(200)); - } - - [TestMethod] - public void ReadTest() - { - var c = MakeConfig(); - File.WriteAllText(c.ConfigFilePath, "[Cat]\n# Test\nKey=1\n"); - c.Reload(); - var w = c.Bind("Cat", "Key", 0, "Test"); - Assert.AreEqual(w.Value, 1); - var w2 = c.Bind("Cat", "Key2", 0, new ConfigDescription("Test")); - Assert.AreEqual(w2.Value, 0); - } - - [TestMethod] - public void ReadTest2() - { - var c = MakeConfig(); - var w = c.Bind("Cat", "Key", 0, new ConfigDescription("Test")); - Assert.AreEqual(w.Value, 0); - - File.WriteAllText(c.ConfigFilePath, "[Cat]\n# Test\nKey = 1 \n"); - - c.Reload(); - Assert.AreEqual(w.Value, 1); - } - - [TestMethod] - public void FileWatchTestNoSelfReload() - { - var c = MakeConfig(); - - var eventFired = new AutoResetEvent(false); - c.ConfigReloaded += (sender, args) => eventFired.Set(); - - c.Save(); - - Assert.IsFalse(eventFired.WaitOne(200)); - } - - [TestMethod] - public void EventTestWrapper() - { - var c = MakeConfig(); - var w = c.Bind("Cat", "Key", 0, new ConfigDescription("Test")); - - File.WriteAllText(c.ConfigFilePath, "[Cat]\n# Test\nKey=1\n"); - - var eventFired = false; - w.SettingChanged += (sender, args) => eventFired = true; - + File.WriteAllText(c.ConfigFilePath, $"[Cat]\n# Test\nKey={testVal}\n"); c.Reload(); - Assert.IsTrue(eventFired); - } - - [TestMethod] - public void PersistHomeless() - { - var c = MakeConfig(); - - File.WriteAllText(c.ConfigFilePath, "[Cat]\n# Test\nKey=1\nHomeless=0"); - c.Reload(); - - var w = c.Bind("Cat", "Key", 0, new ConfigDescription("Test")); - - c.Save(); - - Assert.IsTrue(File.ReadAllLines(c.ConfigFilePath) - .Single(x => x.StartsWith("Homeless") && x.EndsWith("0")) != null); - } - - [TestMethod] - public void EventTestReload() - { - var c = MakeConfig(); - var eventFired = false; - - var w = c.Bind("Cat", "Key", 0, new ConfigDescription("Test")); - w.SettingChanged += (sender, args) => eventFired = true; - - Assert.IsFalse(eventFired); - - File.WriteAllText(c.ConfigFilePath, "[Cat]\n# Test\nKey=1\n"); - c.Reload(); - - Assert.IsTrue(eventFired); - } - - [TestMethod] - public void ValueRangeTest() - { - var c = MakeConfig(); - var w = c.Bind("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange(0, 2))); - - Assert.AreEqual(0, w.Value); - w.Value = 2; - Assert.AreEqual(2, w.Value); - w.Value = -2; - Assert.AreEqual(0, w.Value); - w.Value = 4; - Assert.AreEqual(2, w.Value); - } - - [TestMethod] - [ExpectedException(typeof(ArgumentException))] - public void ValueRangeBadTypeTest() - { - var c = MakeConfig(); - c.Bind("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange(1, 2))); - Assert.Fail(); - } - - [TestMethod] - public void ValueRangeDefaultTest() - { - var c = MakeConfig(); - var w = c.Bind("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange(1, 2))); - - Assert.AreEqual(w.Value, 1); - } - - [TestMethod] - public void ValueRangeLoadTest() - { - var c = MakeConfig(); - - File.WriteAllText(c.ConfigFilePath, "[Cat]\nKey = 1\n"); - c.Reload(); - - var w = c.Bind("Cat", "Key", 0, new ConfigDescription("Test", new AcceptableValueRange(0, 2))); - - Assert.AreEqual(w.Value, 1); - - File.WriteAllText(c.ConfigFilePath, "[Cat]\nKey = 5\n"); - c.Reload(); - - Assert.AreEqual(w.Value, 2); - } - - [TestMethod] - public void ValueListTest() - { - var c = MakeConfig(); - var w = c.Bind("Cat", "Key", "kek", - new ConfigDescription("Test", new AcceptableValueList("lel", "kek", "wew", "why"))); - - Assert.AreEqual("kek", w.Value); - w.Value = "wew"; - Assert.AreEqual("wew", w.Value); - w.Value = "no"; - Assert.AreEqual("lel", w.Value); - w.Value = null; - Assert.AreEqual("lel", w.Value); - } - - [TestMethod] - public void KeyboardShortcutTest() - { - var shortcut = new KeyboardShortcut(KeyCode.H, KeyCode.O, KeyCode.R, KeyCode.S, KeyCode.E, KeyCode.Y); - var s = shortcut.Serialize(); - var d = KeyboardShortcut.Deserialize(s); - Assert.AreEqual(shortcut, d); - - var c = MakeConfig(); - var w = c.Bind("Cat", "Key", new KeyboardShortcut(KeyCode.A, KeyCode.LeftShift)); - Assert.AreEqual(new KeyboardShortcut(KeyCode.A, KeyCode.LeftShift), w.Value); - - w.Value = shortcut; - c.Reload(); - Assert.AreEqual(shortcut, w.Value); - } - - [TestMethod] - public void KeyboardShortcutTest2() - { - Assert.AreEqual(KeyboardShortcut.Empty, new KeyboardShortcut()); - - var c = MakeConfig(); - - var w = c.Bind("Cat", "Key", KeyboardShortcut.Empty, new ConfigDescription("Test")); - - Assert.AreEqual("", w.GetSerializedValue()); + Assert.AreEqual(unescaped, w.Value); w.SetSerializedValue(w.GetSerializedValue()); - Assert.AreEqual(KeyboardShortcut.Empty, w.Value); - - var testShortcut = new KeyboardShortcut(KeyCode.A, KeyCode.B, KeyCode.C); - w.Value = testShortcut; - - w.SetSerializedValue(w.GetSerializedValue()); - Assert.AreEqual(testShortcut, w.Value); + Assert.AreEqual(unescaped, w.Value); c.Save(); c.Reload(); - Assert.AreEqual(testShortcut, w.Value); - } - - [TestMethod] - public void StringEscapeChars() - { - const string testVal = "new line\n test \t\0"; - - var c = MakeConfig(); - var w = c.Bind("Cat", "Key", testVal, new ConfigDescription("Test")); - - Assert.AreEqual(testVal, w.Value); - Assert.IsFalse(w.GetSerializedValue().Any(x => x == '\n')); - - w.SetSerializedValue(w.GetSerializedValue()); - Assert.AreEqual(testVal, w.Value); - - c.Save(); - c.Reload(); - - Assert.AreEqual(testVal, w.Value); - } - - [TestMethod] - public void UnescapedPathString() - { - var c = MakeConfig(); - var w = c.Bind("Cat", "Key", "", new ConfigDescription("Test")); - - var unescaped = @"D:\test\p ath"; - foreach (var testVal in new[] { unescaped, @"D:\\test\\p ath" }) - { - File.WriteAllText(c.ConfigFilePath, $"[Cat]\n# Test\nKey={testVal}\n"); - c.Reload(); - - Assert.AreEqual(unescaped, w.Value); - - w.SetSerializedValue(w.GetSerializedValue()); - Assert.AreEqual(unescaped, w.Value); - - c.Save(); - c.Reload(); - - Assert.AreEqual(unescaped, w.Value); - } + Assert.AreEqual(unescaped, w.Value); } } } diff --git a/BepInExTests/TrampolineTests.cs b/BepInExTests/TrampolineTests.cs index c750470..9eca0d0 100644 --- a/BepInExTests/TrampolineTests.cs +++ b/BepInExTests/TrampolineTests.cs @@ -4,77 +4,76 @@ using BepInEx.IL2CPP; using Iced.Intel; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace BepInEx.Tests +namespace BepInEx.Tests; + +[TestClass] +public class TrampolineTests { - [TestClass] - public class TrampolineTests + [DataTestMethod] + [DataRow(64)] + [DataRow(32)] + public void TrampolineTest(int bitness) { - [DataTestMethod] - [DataRow(64)] - [DataRow(32)] - public void TrampolineTest(int bitness) + byte[] exampleCode = { - byte[] exampleCode = + 0x48, 0x89, 0x5C, 0x24, 0x10, 0x48, 0x89, 0x74, 0x24, 0x18, 0x55, 0x57, 0x41, 0x56, 0x48, 0x8D, + 0xAC, 0x24, 0x00, 0xFF, 0xFF, 0xFF, 0x48, 0x81, 0xEC, 0x00, 0x02, 0x00, 0x00, 0x48, 0x8B, 0x05, + 0x18, 0x57, 0x0A, 0x00, 0x48, 0x33, 0xC4, 0x48, 0x89, 0x85, 0xF0, 0x00, 0x00, 0x00, 0x4C, 0x8B, + 0x05, 0x2F, 0x24, 0x0A, 0x00, 0x48, 0x8D, 0x05, 0x78, 0x7C, 0x04, 0x00, 0x33, 0xFF + }; + + var exampleCodePointer = Marshal.AllocHGlobal(80); + var trampolineCodePointer = Marshal.AllocHGlobal(80); + Marshal.Copy(exampleCode, 0, exampleCodePointer, exampleCode.Length); + + + void Disassemble(byte[] data, ulong ip) + { + var formatter = new NasmFormatter(); + var output = new StringOutput(); + var codeReader = new ByteArrayCodeReader(data); + var decoder = Decoder.Create(bitness, codeReader); + decoder.IP = ip; + while (codeReader.CanReadByte) { - 0x48, 0x89, 0x5C, 0x24, 0x10, 0x48, 0x89, 0x74, 0x24, 0x18, 0x55, 0x57, 0x41, 0x56, 0x48, 0x8D, - 0xAC, 0x24, 0x00, 0xFF, 0xFF, 0xFF, 0x48, 0x81, 0xEC, 0x00, 0x02, 0x00, 0x00, 0x48, 0x8B, 0x05, - 0x18, 0x57, 0x0A, 0x00, 0x48, 0x33, 0xC4, 0x48, 0x89, 0x85, 0xF0, 0x00, 0x00, 0x00, 0x4C, 0x8B, - 0x05, 0x2F, 0x24, 0x0A, 0x00, 0x48, 0x8D, 0x05, 0x78, 0x7C, 0x04, 0x00, 0x33, 0xFF - }; - - var exampleCodePointer = Marshal.AllocHGlobal(80); - var trampolineCodePointer = Marshal.AllocHGlobal(80); - Marshal.Copy(exampleCode, 0, exampleCodePointer, exampleCode.Length); - - - void Disassemble(byte[] data, ulong ip) - { - var formatter = new NasmFormatter(); - var output = new StringOutput(); - var codeReader = new ByteArrayCodeReader(data); - var decoder = Decoder.Create(bitness, codeReader); - decoder.IP = ip; - while (codeReader.CanReadByte) - { - decoder.Decode(out var instr); - formatter.Format(instr, output); - Console.WriteLine($"{instr.IP:X16} {output.ToStringAndReset()}"); - } - - Console.WriteLine(); + decoder.Decode(out var instr); + formatter.Format(instr, output); + Console.WriteLine($"{instr.IP:X16} {output.ToStringAndReset()}"); } - - Console.WriteLine("Original:"); Console.WriteLine(); - - - Disassemble(exampleCode, (ulong) exampleCodePointer.ToInt64()); - - DetourGenerator.CreateTrampolineFromFunction(exampleCodePointer, out var trampolineLength, out _); - - Console.WriteLine("Modified:"); - Console.WriteLine(); - - - Marshal.Copy(exampleCodePointer, exampleCode, 0, exampleCode.Length); - Disassemble(exampleCode, (ulong) exampleCodePointer.ToInt64()); - - - Console.WriteLine(); - Console.WriteLine("Trampoline:"); - Console.WriteLine(); - - var trampolineArray = new byte[trampolineLength]; - Marshal.Copy(trampolineCodePointer, trampolineArray, 0, trampolineLength); - - Disassemble(trampolineArray, (ulong) trampolineCodePointer.ToInt64()); - - - Marshal.FreeHGlobal(exampleCodePointer); - Marshal.FreeHGlobal(trampolineCodePointer); - - Assert.IsFalse(false); } + + + Console.WriteLine("Original:"); + Console.WriteLine(); + + + Disassemble(exampleCode, (ulong) exampleCodePointer.ToInt64()); + + DetourGenerator.CreateTrampolineFromFunction(exampleCodePointer, out var trampolineLength, out _); + + Console.WriteLine("Modified:"); + Console.WriteLine(); + + + Marshal.Copy(exampleCodePointer, exampleCode, 0, exampleCode.Length); + Disassemble(exampleCode, (ulong) exampleCodePointer.ToInt64()); + + + Console.WriteLine(); + Console.WriteLine("Trampoline:"); + Console.WriteLine(); + + var trampolineArray = new byte[trampolineLength]; + Marshal.Copy(trampolineCodePointer, trampolineArray, 0, trampolineLength); + + Disassemble(trampolineArray, (ulong) trampolineCodePointer.ToInt64()); + + + Marshal.FreeHGlobal(exampleCodePointer); + Marshal.FreeHGlobal(trampolineCodePointer); + + Assert.IsFalse(false); } } diff --git a/README.md b/README.md index 60cf022..dc4918f 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@

# BepInEx + ![Github All Releases](https://img.shields.io/github/downloads/bepinex/bepinex/total.svg) ![GitHub release](https://img.shields.io/github/release/bepinex/bepinex.svg) [![BepInEx Discord](https://user-images.githubusercontent.com/7288322/34429117-c74dbd12-ecb8-11e7-896d-46369cd0de5b.png)](https://discord.gg/MpFEDAg) @@ -25,7 +26,6 @@ BepInEx is a plugin / modding framework for Unity Mono, IL2CPP and .NET framewor A more comprehensive comparison list of features and compatibility is available at https://bepis.io/unity.html - ## Resources **[Latest releases](https://github.com/BepInEx/BepInEx/releases)** @@ -56,6 +56,7 @@ A more comprehensive comparison list of features and compatibility is available | uMod | [BepInEx.uMod.Loader](https://github.com/BepInEx/BepInEx.uMod.Loader) | ## Used libraries + - [NeighTools/UnityDoorstop](https://github.com/NeighTools/UnityDoorstop) - 3.4.0.0 ([fad307f](https://github.com/NeighTools/UnityDoorstop/commit/fad307fda5c968d05675f17a49af7e790966fec3)) - [NeighTools/UnityDoorstop.Unix](https://github.com/NeighTools/UnityDoorstop.Unix) - @@ -80,7 +81,9 @@ A more comprehensive comparison list of features and compatibility is available fork ([7328415](https://github.com/BepInEx/mono/commit/7328415ac575399a71f32487e97bce9d5fe7f6ca)) ## Credits + - [Usagirei](https://github.com/Usagirei) - Code for using the console and for assisting with technical support - [essu](https://github.com/exdownloader) - Project logo, moral support and lots of misc. help -- [denikson](https://github.com/denikson) - [UnityDoorstop](https://github.com/NeighTools/UnityDoorstop) for the patchless loader +- [denikson](https://github.com/denikson) - [UnityDoorstop](https://github.com/NeighTools/UnityDoorstop) for the + patchless loader - [nn@](https://twitter.com/NnAone2cmg) - Japanese translation of the wiki