From 579ee8511ba42d7c4cd1fb444cbb27372e9316a8 Mon Sep 17 00:00:00 2001 From: ghorsington Date: Sun, 10 Jul 2022 20:44:56 +0300 Subject: [PATCH] Add extended Unity version resolver; reorganise projects The new resolver resides in BepInEx.Unity.Core The resolver is based on multiple heuristics and integrates all workarounds we currently use so this shouldn't be a breaking change --- BepInEx.Core/Paths.cs | 13 +- BepInEx.IL2CPP/BepInEx.IL2CPP.csproj | 1 + BepInEx.IL2CPP/Preloader.cs | 3 + BepInEx.IL2CPP/UnityPreloadRunner.cs | 2 +- .../BepInEx.Preloader.UnityMono.csproj | 1 + BepInEx.Preloader.UnityMono/UnityPreloader.cs | 15 +- .../UnityPreloaderRunner.cs | 4 +- BepInEx.Unity.Core/BepInEx.Unity.Core.csproj | 13 ++ BepInEx.Unity.Core/UnityInfo.cs | 144 ++++++++++++++++++ .../Bootstrap/UnityChainloader.cs | 9 +- BepInEx.sln | 15 ++ 11 files changed, 200 insertions(+), 20 deletions(-) create mode 100644 BepInEx.Unity.Core/BepInEx.Unity.Core.csproj create mode 100644 BepInEx.Unity.Core/UnityInfo.cs diff --git a/BepInEx.Core/Paths.cs b/BepInEx.Core/Paths.cs index b64bb05..34d3e98 100644 --- a/BepInEx.Core/Paths.cs +++ b/BepInEx.Core/Paths.cs @@ -20,10 +20,15 @@ public static class Paths .InformationalVersion); /// - /// The path to the Managed folder of the currently running Unity game. + /// The path to the Managed folder that contains the main managed assemblies. /// public static string ManagedPath { get; private set; } + /// + /// The path to the game data folder of the currently running Unity game. + /// + public static string GameDataPath { get; private set; } + /// /// The directory that the core BepInEx DLLs reside in. /// @@ -91,6 +96,7 @@ public static class Paths public static void SetExecutablePath(string executablePath, string bepinRootPath = null, string managedPath = null, + bool gameDataRelativeToManaged = false, string[] dllSearchPath = null) { ExecutablePath = executablePath; @@ -100,7 +106,10 @@ public static class Paths ? Utility.ParentDirectory(executablePath, 4) : Path.GetDirectoryName(executablePath); - ManagedPath = managedPath ?? Utility.CombinePaths(GameRootPath, $"{ProcessName}_Data", "Managed"); + GameDataPath = managedPath != null && gameDataRelativeToManaged + ? Path.GetDirectoryName(managedPath) + : Path.Combine(GameRootPath, $"{ProcessName}_Data"); + ManagedPath = managedPath ?? Path.Combine(GameDataPath, "Managed"); BepInExRootPath = bepinRootPath ?? Path.Combine(GameRootPath, "BepInEx"); ConfigPath = Path.Combine(BepInExRootPath, "config"); BepInExConfigPath = Path.Combine(ConfigPath, "BepInEx.cfg"); diff --git a/BepInEx.IL2CPP/BepInEx.IL2CPP.csproj b/BepInEx.IL2CPP/BepInEx.IL2CPP.csproj index af1671a..db9ea4c 100644 --- a/BepInEx.IL2CPP/BepInEx.IL2CPP.csproj +++ b/BepInEx.IL2CPP/BepInEx.IL2CPP.csproj @@ -12,6 +12,7 @@ + diff --git a/BepInEx.IL2CPP/Preloader.cs b/BepInEx.IL2CPP/Preloader.cs index 63e01d8..539c9ba 100644 --- a/BepInEx.IL2CPP/Preloader.cs +++ b/BepInEx.IL2CPP/Preloader.cs @@ -37,6 +37,7 @@ public static class Preloader try { HarmonyBackendFix.Initialize(); + UnityInfo.Initialize(Paths.ExecutablePath, Paths.GameDataPath); ConsoleManager.Initialize(false, true); @@ -50,6 +51,8 @@ public static class Preloader } ChainloaderLogHelper.PrintLogInfo(Log); + + Logger.Log(LogLevel.Info, $"Running under Unity {UnityInfo.Version}"); Logger.Log(LogLevel.Info, $"Runtime version: {Environment.Version}"); Logger.Log(LogLevel.Info, $"Runtime information: {RuntimeInformation.FrameworkDescription}"); diff --git a/BepInEx.IL2CPP/UnityPreloadRunner.cs b/BepInEx.IL2CPP/UnityPreloadRunner.cs index da13ab4..1b88420 100644 --- a/BepInEx.IL2CPP/UnityPreloadRunner.cs +++ b/BepInEx.IL2CPP/UnityPreloadRunner.cs @@ -15,7 +15,7 @@ internal static class UnityPreloaderRunner PlatformUtils.SetPlatform(); - Paths.SetExecutablePath(EnvVars.DOORSTOP_PROCESS_PATH, bepinPath, EnvVars.DOORSTOP_MANAGED_FOLDER_DIR, + Paths.SetExecutablePath(EnvVars.DOORSTOP_PROCESS_PATH, bepinPath, EnvVars.DOORSTOP_MANAGED_FOLDER_DIR, false, EnvVars.DOORSTOP_DLL_SEARCH_DIRS); // Cecil 0.11 requires one to manually set up list of trusted assemblies for assembly resolving diff --git a/BepInEx.Preloader.UnityMono/BepInEx.Preloader.UnityMono.csproj b/BepInEx.Preloader.UnityMono/BepInEx.Preloader.UnityMono.csproj index 114caa7..2a2c718 100644 --- a/BepInEx.Preloader.UnityMono/BepInEx.Preloader.UnityMono.csproj +++ b/BepInEx.Preloader.UnityMono/BepInEx.Preloader.UnityMono.csproj @@ -14,6 +14,7 @@ + diff --git a/BepInEx.Preloader.UnityMono/UnityPreloader.cs b/BepInEx.Preloader.UnityMono/UnityPreloader.cs index 60c516a..a64d679 100644 --- a/BepInEx.Preloader.UnityMono/UnityPreloader.cs +++ b/BepInEx.Preloader.UnityMono/UnityPreloader.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics; using System.IO; using System.Linq; using BepInEx.Bootstrap; @@ -11,7 +10,7 @@ using BepInEx.Preloader.Core.Patching; using BepInEx.Preloader.RuntimeFixes; using BepInEx.Preloader.UnityMono.RuntimeFixes; using BepInEx.Preloader.UnityMono.Utils; -using MonoMod.Utils; +using BepInEx.Unity.Core; namespace BepInEx.Preloader.UnityMono; @@ -44,6 +43,7 @@ internal static class UnityPreloader try { HarmonyBackendFix.Initialize(); + UnityInfo.Initialize(Paths.ExecutablePath, Paths.GameDataPath); ConsoleManager.Initialize(false, false); AllocateConsole(); @@ -61,8 +61,7 @@ internal static class UnityPreloader Logger.Listeners.Add(PreloaderLog); ChainloaderLogHelper.PrintLogInfo(Log); - - Log.Log(LogLevel.Info, $"Running under Unity v{GetUnityVersion()}"); + Log.Log(LogLevel.Info, $"Running under Unity {UnityInfo.Version}"); Log.Log(LogLevel.Info, $"CLR runtime version: {Environment.Version}"); Log.Log(LogLevel.Info, $"Supports SRE: {Utility.CLRSupportsDynamicAssemblies}"); @@ -153,12 +152,4 @@ internal static class UnityPreloader Log.LogError(ex); } } - - public static string GetUnityVersion() - { - if (PlatformHelper.Is(Platform.Windows)) - return FileVersionInfo.GetVersionInfo(Paths.ExecutablePath).FileVersion; - - return $"Unknown ({(IsPostUnity2017 ? "post" : "pre")}-2017)"; - } } diff --git a/BepInEx.Preloader.UnityMono/UnityPreloaderRunner.cs b/BepInEx.Preloader.UnityMono/UnityPreloaderRunner.cs index 61ca287..39c8c0f 100644 --- a/BepInEx.Preloader.UnityMono/UnityPreloaderRunner.cs +++ b/BepInEx.Preloader.UnityMono/UnityPreloaderRunner.cs @@ -45,8 +45,10 @@ internal static class UnityPreloaderRunner var bepinPath = Utility.ParentDirectory(Path.GetFullPath(EnvVars.DOORSTOP_INVOKE_DLL_PATH), 2); - Paths.SetExecutablePath(EnvVars.DOORSTOP_PROCESS_PATH, bepinPath, + Paths.SetExecutablePath(EnvVars.DOORSTOP_PROCESS_PATH, + bepinPath, EnvVars.DOORSTOP_MANAGED_FOLDER_DIR, + true, EnvVars.DOORSTOP_DLL_SEARCH_DIRS); LoadCriticalAssemblies(); diff --git a/BepInEx.Unity.Core/BepInEx.Unity.Core.csproj b/BepInEx.Unity.Core/BepInEx.Unity.Core.csproj new file mode 100644 index 0000000..1e726f9 --- /dev/null +++ b/BepInEx.Unity.Core/BepInEx.Unity.Core.csproj @@ -0,0 +1,13 @@ + + + + net35;netstandard2.0 + + + + + + + + + diff --git a/BepInEx.Unity.Core/UnityInfo.cs b/BepInEx.Unity.Core/UnityInfo.cs new file mode 100644 index 0000000..aa3bb87 --- /dev/null +++ b/BepInEx.Unity.Core/UnityInfo.cs @@ -0,0 +1,144 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; +using AssetRipper.VersionUtilities; +using MonoMod.Utils; + +[assembly: InternalsVisibleTo("BepInEx.Preloader.UnityMono")] +[assembly: InternalsVisibleTo("BepInEx.UnityMono")] +[assembly: InternalsVisibleTo("BepInEx.IL2CPP")] + +namespace BepInEx.Unity.Core; + +/// +/// Various information about the currently executing Unity player. +/// +public static class UnityInfo +{ + // Adapted from https://github.com/SamboyCoding/Cpp2IL/blob/development/LibCpp2IL/LibCpp2IlMain.cs + private static readonly ManagerLookup[] ManagerVersionLookup = + { + new("globalgamemanagers", 0x14, 0x30), + new("data.unity3d", 0x12), + new("mainData", 0x14) + }; + + private static bool initialized; + + /// + /// Path to the player executable. + /// + public static string PlayerPath { get; private set; } + + /// + /// Path to the game data directory (directory that contains the game assets). + /// + public static string GameDataPath { get; private set; } + + /// + /// Version of the Unity player + /// + /// + /// Because BepInEx can execute very early, the exact Unity version might not be available in early + /// bootstrapping phases. The version should be treated as an estimation of the actual version of the Unity player. + /// + public static UnityVersion Version { get; private set; } + + internal static void Initialize(string unityPlayerPath, string gameDataPath) + { + if (initialized) + return; + PlayerPath = Path.GetFullPath(unityPlayerPath ?? throw new ArgumentNullException(nameof(unityPlayerPath))); + GameDataPath = Path.GetFullPath(gameDataPath ?? throw new ArgumentNullException(nameof(gameDataPath))); + + DetermineVersion(); + initialized = true; + } + + internal static void SetRuntimeUnityVersion(string version) => Version = UnityVersion.Parse(version); + + private static void DetermineVersion() + { + // Try looking up first since it's more reliable + foreach (var lookup in ManagerVersionLookup) + if (lookup.TryLookup(out var version)) + { + Version = version; + return; + } + + // On Windows, we can try to parse executable name, but some games can mess up the file version as well + if (PlatformHelper.Is(Platform.Windows)) + try + { + var version = FileVersionInfo.GetVersionInfo(PlayerPath); + // Parse manually because some games can also wipe the file version (so it's an empty string) + var simpleVersion = new Version(version.FileVersion); + Version = new UnityVersion((ushort) simpleVersion.Major, (ushort) simpleVersion.Minor, + (ushort) simpleVersion.Build); + return; + } + catch (Exception) + { + // Some games have version stripped or intentionally wrong + // In that case pass through + } + + // We can't determine the version fully, so we'll try to guess + // On UnityMono, UnityEngine.CoreModule.dll is present for post-2017 + // We'll also mark it as "experimental" so that we can detect this via logs + var managed = Path.Combine(GameDataPath, "Managed"); + if (File.Exists(Path.Combine(managed, "UnityEngine.CoreModule.dll"))) + Version = new UnityVersion(2017, 0, 0, UnityVersionType.Experimental); + + Version = default; + } + + private class ManagerLookup + { + private readonly string filePath; + private readonly int[] lookupOffsets; + + public ManagerLookup(string filePath, params int[] lookupOffsets) + { + this.filePath = filePath; + this.lookupOffsets = lookupOffsets; + } + + public bool TryLookup(out UnityVersion version) + { + var path = Path.Combine(GameDataPath, filePath); + if (!File.Exists(path)) + { + version = default; + return false; + } + + using var fs = File.OpenRead(path); + foreach (var offset in lookupOffsets) + { + var sb = new StringBuilder(); + fs.Position = offset; + + byte b; + while ((b = (byte) fs.ReadByte()) != 0) + sb.Append((char) b); + + try + { + version = UnityVersion.Parse(sb.ToString()); + return true; + } + catch (Exception e) + { + // Ignore + } + } + + version = default; + return false; + } + } +} diff --git a/BepInEx.UnityMono/Bootstrap/UnityChainloader.cs b/BepInEx.UnityMono/Bootstrap/UnityChainloader.cs index 8c90670..dfc6596 100644 --- a/BepInEx.UnityMono/Bootstrap/UnityChainloader.cs +++ b/BepInEx.UnityMono/Bootstrap/UnityChainloader.cs @@ -81,7 +81,6 @@ public class UnityChainloader : BaseChainloader } } - public override void Initialize(string gameExePath = null) { try @@ -124,12 +123,14 @@ public class UnityChainloader : BaseChainloader Logger.Listeners.Add(new UnityLogListener()); - if (!PlatformHelper.Is(Platform.Windows)) Logger.Log(LogLevel.Info, $"Detected Unity version: v{UnityVersion}"); - + // Update version info from runtime in case it wasn't set yet + var prevVersion = UnityInfo.Version; + UnityInfo.SetRuntimeUnityVersion(UnityVersion); + if (UnityInfo.Version != prevVersion) + Logger.Log(LogLevel.Info, $"UnityPlayer version: {UnityInfo.Version}"); if (!ConfigDiskWriteUnityLog.Value) DiskLogListener.BlacklistedSources.Add("Unity Log"); - ChainloaderLogHelper.RewritePreloaderLogs(); diff --git a/BepInEx.sln b/BepInEx.sln index 49bf21f..a233958 100644 --- a/BepInEx.sln +++ b/BepInEx.sln @@ -58,6 +58,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Doorstop", "Doorstop", "{C7 doorstop\run_bepinex_mono.sh = doorstop\run_bepinex_mono.sh EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Unity", "Unity", "{BF8F4E07-3B89-4152-89F4-9EF9D4AAA1E0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BepInEx.Unity.Core", "BepInEx.Unity.Core\BepInEx.Unity.Core.csproj", "{C32E74A3-C0BD-42DE-9E2F-D033D496ACB9}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution BepInEx.NetLauncher.Shared\BepInEx.NetLauncher.Shared.projitems*{067bcc8c-fc25-4929-b4f4-c6a5a7496994}*SharedItemsImports = 13 @@ -145,6 +149,14 @@ Global {08083F40-98AA-4176-BA01-817C18C35C7F}.Release_Unity|Any CPU.Build.0 = Release|Any CPU {08083F40-98AA-4176-BA01-817C18C35C7F}.Release|Any CPU.ActiveCfg = Release|Any CPU {08083F40-98AA-4176-BA01-817C18C35C7F}.Release|Any CPU.Build.0 = Release|Any CPU + {C32E74A3-C0BD-42DE-9E2F-D033D496ACB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C32E74A3-C0BD-42DE-9E2F-D033D496ACB9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C32E74A3-C0BD-42DE-9E2F-D033D496ACB9}.Release_NetFramework|Any CPU.ActiveCfg = Debug|Any CPU + {C32E74A3-C0BD-42DE-9E2F-D033D496ACB9}.Release_NetFramework|Any CPU.Build.0 = Debug|Any CPU + {C32E74A3-C0BD-42DE-9E2F-D033D496ACB9}.Release_Unity|Any CPU.ActiveCfg = Debug|Any CPU + {C32E74A3-C0BD-42DE-9E2F-D033D496ACB9}.Release_Unity|Any CPU.Build.0 = Debug|Any CPU + {C32E74A3-C0BD-42DE-9E2F-D033D496ACB9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C32E74A3-C0BD-42DE-9E2F-D033D496ACB9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -160,6 +172,9 @@ Global {68C57430-3E92-4B50-A9A5-208FE9CCD914} = {12081C35-76DF-450C-AC83-1E1CB84516AA} {A6DDBD9E-773C-437A-88AA-995BC5FE90DF} = {12081C35-76DF-450C-AC83-1E1CB84516AA} {C76B3E16-93A8-4CD7-95A2-18EBF2437001} = {12081C35-76DF-450C-AC83-1E1CB84516AA} + {D5F47CFC-E4F3-46EE-B4F3-2474CF7D9E2C} = {BF8F4E07-3B89-4152-89F4-9EF9D4AAA1E0} + {6E2DD21E-0854-4F4A-B925-D90E0016D03D} = {BF8F4E07-3B89-4152-89F4-9EF9D4AAA1E0} + {C32E74A3-C0BD-42DE-9E2F-D033D496ACB9} = {BF8F4E07-3B89-4152-89F4-9EF9D4AAA1E0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {55AC11EF-F568-4C79-A356-7ED9510145B1}