using System; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using BepInEx.Preloader.Core.Logging; using BepInEx.Unity.Common; using BepInEx.Unity.Mono.Logging; using UnityEngine; using Logger = BepInEx.Logging.Logger; using Object = UnityEngine.Object; [assembly: InternalsVisibleTo("UnityEngine")] [assembly: InternalsVisibleTo("UnityEngine.Core")] namespace BepInEx.Unity.Mono.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 static readonly bool staticStartHasBeenCalled = false; private string _consoleTitle; // TODO: Remove once proper instance handling exists public static UnityChainloader Instance { get; set; } /// /// The GameObject that all plugins are attached to as components. /// public static GameObject ManagerObject { get; private set; } 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; } /// /// Reads the runtime Unity version, returning null if it is unavailable. /// Unity's managed code stripper can remove Application.unityVersion entirely, in which case /// touching it throws MissingMethodException. The version is only used for an informational /// log line, so it must not be able to abort chainloader initialisation. /// [MethodImpl(MethodImplOptions.NoInlining)] private static string TryGetUnityVersion() { try { return UnityVersion; } catch (Exception e) { Logger.Log(LogLevel.Debug, $"Could not read the runtime Unity version: {e.Message}"); return null; } } [Obsolete("This method is public due to a limitation with Unity 4.x. DO NOT CALL", true)] public static void StaticStart(string gameExePath = null) { try { if (staticStartHasBeenCalled) throw new InvalidOperationException("Cannot call StaticStart again"); Logger.Log(LogLevel.Debug, "Entering chainloader StaticStart"); var instance = new UnityChainloader(); instance.Initialize(gameExePath); instance.Execute(); Logger.Log(LogLevel.Debug, "Exiting chainloader StaticStart"); } catch (Exception ex) { Logger.Log(LogLevel.Fatal, $"Unable to complete chainloader StaticStart: {ex.Message}"); Logger.Log(LogLevel.Fatal, ex.StackTrace); } } public override void Initialize(string gameExePath = null) { try { Logger.Log(LogLevel.Debug, "Entering chainloader initialize"); Instance = this; UnityTomlTypeConverters.AddUnityEngineConverters(); Logger.Log(LogLevel.Debug, "Initializing ThreadingHelper"); ThreadingHelper.Initialize(); Logger.Log(LogLevel.Debug, "Creating Manager object"); ManagerObject = new GameObject("BepInEx_Manager") { hideFlags = HideFlags.HideAndDontSave }; Object.DontDestroyOnLoad(ManagerObject); Logger.Log(LogLevel.Debug, "Getting game product name"); var productNameProp = typeof(Application).GetProperty("productName", BindingFlags.Public | BindingFlags.Static); _consoleTitle = $"{CurrentAssemblyName} {CurrentAssemblyVersion} - {productNameProp?.GetValue(null, null) ?? Path.GetFileNameWithoutExtension(Process.GetCurrentProcess().ProcessName)}"; Logger.Log(LogLevel.Debug, "Falling back to BaseChainloader initializer"); base.Initialize(gameExePath); Logger.Log(LogLevel.Debug, "Exiting chainloader initialize"); } catch (Exception ex) { Logger.Log(LogLevel.Fatal, $"Unable to complete chainloader init: {ex.Message}"); Logger.Log(LogLevel.Fatal, ex.StackTrace); // Rethrow so callers do not go on to Execute() a chainloader that was never // initialised. Swallowing here produces a far worse failure mode than crashing: // the loader carries on to log "Chainloader startup complete" while every plugin // silently never runs, which is extremely hard to diagnose. throw; } } protected override void InitializeLoggers() { base.InitializeLoggers(); Logger.Listeners.Add(new UnityLogListener()); // Update version info from runtime in case it wasn't set yet. // May be unavailable on aggressively stripped builds; it is informational only. var runtimeUnityVersion = TryGetUnityVersion(); if (runtimeUnityVersion != null) { var prevVersion = UnityInfo.Version; UnityInfo.SetRuntimeUnityVersion(runtimeUnityVersion); if (UnityInfo.Version != prevVersion) Logger.Log(LogLevel.Info, $"UnityPlayer version: {UnityInfo.Version}"); } 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)); }