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));
}