| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408 | 
							- using BepInEx.Configuration;
 
- using BepInEx.Logging;
 
- using System;
 
- using System.Collections.Generic;
 
- using System.IO;
 
- using System.Linq;
 
- using System.Reflection;
 
- using System.Text;
 
- using System.Text.RegularExpressions;
 
- using Mono.Cecil;
 
- using UnityEngine;
 
- using UnityInjector.ConsoleUtil;
 
- using Logger = BepInEx.Logging.Logger;
 
- namespace BepInEx.Bootstrap
 
- {
 
- 	/// <summary>
 
- 	/// The manager and loader for all plugins, and the entry point for BepInEx plugin system.
 
- 	/// </summary>
 
- 	public static class Chainloader
 
- 	{
 
- 		/// <summary>
 
- 		/// The loaded and initialized list of plugins.
 
- 		/// </summary>
 
- 		public static Dictionary<string, PluginInfo> PluginInfos { get; } = new Dictionary<string, PluginInfo>();
 
- 		private static readonly List<BaseUnityPlugin> _plugins = new List<BaseUnityPlugin>();
 
- 		[Obsolete("Use PluginInfos instead")]
 
- 		public static List<BaseUnityPlugin> Plugins
 
- 		{
 
- 			get
 
- 			{
 
- 				lock (_plugins)
 
- 				{
 
- 					_plugins.RemoveAll(x => x == null);
 
- 					return _plugins.ToList();
 
- 				}
 
- 			}
 
- 		}
 
- 		public static List<string> DependencyErrors { get; } = new List<string>();
 
- 		/// <summary>
 
- 		/// The GameObject that all plugins are attached to as components.
 
- 		/// </summary>
 
- 		public static GameObject ManagerObject { get; private set; }
 
- 		private static bool _loaded = false;
 
- 		private static bool _initialized = false;
 
- 		/// <summary>
 
- 		/// Initializes BepInEx to be able to start the chainloader.
 
- 		/// </summary>
 
- 		public static void Initialize(string gameExePath, bool startConsole = true, ICollection<LogEventArgs> preloaderLogEvents = null)
 
- 		{
 
- 			if (_initialized)
 
- 				return;
 
- 			ThreadingHelper.Initialize();
 
- 			// 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);
 
- 			}
 
- 			// Start logging
 
- 			if (ConsoleWindow.ConfigConsoleEnabled.Value && startConsole)
 
- 			{
 
- 				ConsoleWindow.Attach();
 
- 				Logger.Listeners.Add(new ConsoleLogListener());
 
- 			}
 
- 			// Fix for standard output getting overwritten by UnityLogger
 
- 			if (ConsoleWindow.StandardOut != null)
 
- 			{
 
- 				Console.SetOut(ConsoleWindow.StandardOut);
 
- 				var encoding = ConsoleWindow.ConfigConsoleShiftJis.Value ? 932 : (uint)Encoding.UTF8.CodePage;
 
- 				ConsoleEncoding.ConsoleCodePage = encoding;
 
- 				Console.OutputEncoding = ConsoleEncoding.GetEncoding(encoding);
 
- 			}
 
- 			Logger.Listeners.Add(new UnityLogListener());
 
- 			if (ConfigDiskLogging.Value)
 
- 				Logger.Listeners.Add(new DiskLogListener("LogOutput.log", ConfigDiskConsoleDisplayedLevel.Value, ConfigDiskAppend.Value, ConfigDiskWriteUnityLog.Value));
 
- 			if (!TraceLogSource.IsListening)
 
- 				Logger.Sources.Add(TraceLogSource.CreateSource());
 
- 			if (ConfigUnityLogging.Value)
 
- 				Logger.Sources.Add(new UnityLogSource());
 
- 			// Temporarily disable the console log listener as we replay the preloader logs
 
- 			var logListener = Logger.Listeners.FirstOrDefault(logger => logger is ConsoleLogListener);
 
- 			
 
- 			if (logListener != null)
 
- 				Logger.Listeners.Remove(logListener);
 
- 			var preloaderLogSource = Logger.CreateLogSource("Preloader");
 
- 			foreach (var preloaderLogEvent in preloaderLogEvents)
 
- 			{
 
- 				preloaderLogSource.Log(preloaderLogEvent.Level, preloaderLogEvent.Data);
 
- 			}
 
- 			Logger.Sources.Remove(preloaderLogSource);
 
- 			if (logListener != null)
 
- 				Logger.Listeners.Add(logListener);
 
- 			Logger.LogMessage("Chainloader ready");
 
- 			_initialized = true;
 
- 		}
 
- 		private static Regex allowedGuidRegex { get; } = new Regex(@"^[a-zA-Z0-9\._\-]+$");
 
- 		internal static PluginInfo ToPluginInfo(TypeDefinition type)
 
- 		{
 
- 			if (type.IsInterface || type.IsAbstract)
 
- 				return null;
 
- 			try
 
- 			{
 
- 				if (!type.IsSubtypeOf(typeof(BaseUnityPlugin)))
 
- 					return null;
 
- 			}
 
- 			catch (AssemblyResolutionException ex)
 
- 			{
 
- 				// 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 = PluginMetadata.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 = ProcessFilter.FromCecilType(type);
 
- 			var dependencies = PluginDependency.FromCecilType(type);
 
- 			var incompatibilities = PluginIncompatibility.FromCecilType(type);
 
- 			return new PluginInfo
 
- 			{
 
- 				Metadata = metadata,
 
- 				Processes = filters,
 
- 				Dependencies = dependencies,
 
- 				Incompatibilities = incompatibilities,
 
- 				TypeName = type.FullName
 
- 			};
 
- 		}
 
- 		private static readonly string CurrentAssemblyName = Assembly.GetExecutingAssembly().GetName().Name;
 
- 		private 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(BaseUnityPlugin).FullName))
 
- 				return false;
 
- 			return true;
 
- 		}
 
- 		/// <summary>
 
- 		/// The entrypoint for the BepInEx plugin system.
 
- 		/// </summary>
 
- 		public static void Start()
 
- 		{
 
- 			if (_loaded)
 
- 				return;
 
- 			if (!_initialized)
 
- 				throw new InvalidOperationException("BepInEx has not been initialized. Please call Chainloader.Initialize prior to starting the chainloader instance.");
 
- 			if (!Directory.Exists(Paths.PluginPath))
 
- 				Directory.CreateDirectory(Paths.PluginPath);
 
- 			if (!Directory.Exists(Paths.PatcherPluginPath))
 
- 				Directory.CreateDirectory(Paths.PatcherPluginPath);
 
- 			try
 
- 			{
 
- 				var productNameProp = typeof(Application).GetProperty("productName", BindingFlags.Public | BindingFlags.Static);
 
- 				if (productNameProp != null)
 
- 					ConsoleWindow.Title = $"BepInEx {Assembly.GetExecutingAssembly().GetName().Version} - {productNameProp.GetValue(null, null)}";
 
- 				Logger.LogMessage("Chainloader started");
 
- 				ManagerObject = new GameObject("BepInEx_Manager");
 
- 				UnityEngine.Object.DontDestroyOnLoad(ManagerObject);
 
- 				var pluginsToLoad = TypeLoader.FindPluginTypes(Paths.PluginPath, ToPluginInfo, HasBepinPlugins, "chainloader");
 
- 				foreach (var keyValuePair in pluginsToLoad)
 
- 					foreach (var pluginInfo in keyValuePair.Value)
 
- 						pluginInfo.Location = keyValuePair.Key;
 
- 				var pluginInfos = pluginsToLoad.SelectMany(p => p.Value).ToList();
 
- 				var loadedAssemblies = new Dictionary<string, Assembly>();
 
- 				Logger.LogInfo($"{pluginInfos.Count} plugins to load");
 
- 				// We use a sorted dictionary to ensure consistent load order
 
- 				var dependencyDict = new SortedDictionary<string, IEnumerable<string>>(StringComparer.InvariantCultureIgnoreCase);
 
- 				var pluginsByGUID = new Dictionary<string, PluginInfo>();
 
- 				foreach (var pluginInfoGroup in pluginInfos.GroupBy(info => info.Metadata.GUID))
 
- 				{
 
- 					var alreadyLoaded = false;
 
- 					foreach (var pluginInfo in pluginInfoGroup.OrderByDescending(x => x.Metadata.Version))
 
- 					{
 
- 						if (alreadyLoaded)
 
- 						{
 
- 							Logger.LogWarning($"Skipping because a newer version exists [{pluginInfo.Metadata.Name} {pluginInfo.Metadata.Version}]");
 
- 							continue;
 
- 						}
 
- 						alreadyLoaded = true;
 
- 						// Perform checks that will prevent loading plugins in this run
 
- 						var filters = pluginInfo.Processes.ToList();
 
- 						bool invalidProcessName = filters.Count != 0 && filters.All(x => !string.Equals(x.ProcessName.Replace(".exe", ""), Paths.ProcessName, StringComparison.InvariantCultureIgnoreCase));
 
- 						if (invalidProcessName)
 
- 						{
 
- 							Logger.LogWarning($"Skipping because of process filters [{pluginInfo.Metadata.Name} {pluginInfo.Metadata.Version}]");
 
- 							continue;
 
- 						}
 
- 						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();
 
- 						string message = $@"Could not load [{pluginInfo.Metadata.Name}] because it is incompatible with: {string.Join(", ", incompatiblePlugins)}";
 
- 						DependencyErrors.Add(message);
 
- 						Logger.LogError(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();
 
- 				var invalidPlugins = new HashSet<string>();
 
- 				var processedPlugins = new Dictionary<string, Version>();
 
- 				foreach (var pluginGUID in sortedPlugins)
 
- 				{
 
- 					// If the plugin is missing, don't process it
 
- 					if (!pluginsByGUID.TryGetValue(pluginGUID, out var pluginInfo))
 
- 						continue;
 
- 					var dependsOnInvalidPlugin = false;
 
- 					var missingDependencies = new List<PluginDependency>();
 
- 					foreach (var dependency in pluginInfo.Dependencies)
 
- 					{
 
- 						// If the depenency wasn't already processed, it's missing altogether
 
- 						bool depenencyExists = processedPlugins.TryGetValue(dependency.DependencyGUID, out var pluginVersion);
 
- 						if (!depenencyExists || pluginVersion < dependency.MinimumVersion)
 
- 						{
 
- 							// If the dependency is hard, collect it into a list to show
 
- 							if ((dependency.Flags & PluginDependency.DependencyFlags.HardDependency) != 0)
 
- 								missingDependencies.Add(dependency);
 
- 							continue;
 
- 						}
 
- 						// If the dependency is invalid (e.g. has missing depedencies), report that to the user
 
- 						if (invalidPlugins.Contains(dependency.DependencyGUID))
 
- 						{
 
- 							dependsOnInvalidPlugin = true;
 
- 							break;
 
- 						}
 
- 					}
 
- 					processedPlugins.Add(pluginGUID, pluginInfo.Metadata.Version);
 
- 					if (dependsOnInvalidPlugin)
 
- 					{
 
- 						string message = $"Skipping [{pluginInfo.Metadata.Name}] 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)
 
- 					{
 
- 						bool IsEmptyVersion(Version v) => v.Major == 0 && v.Minor == 0 && v.Build <= 0 && v.Revision <= 0;
 
- 						string message = $@"Could not load [{pluginInfo.Metadata.Name}] because it has missing dependencies: {
 
- 							string.Join(", ", missingDependencies.Select(s => IsEmptyVersion(s.MinimumVersion) ? s.DependencyGUID : $"{s.DependencyGUID} (v{s.MinimumVersion} or newer)").ToArray())
 
- 							}";
 
- 						DependencyErrors.Add(message);
 
- 						Logger.LogError(message);
 
- 						invalidPlugins.Add(pluginGUID);
 
- 						continue;
 
- 					}
 
- 					try
 
- 					{
 
- 						Logger.LogInfo($"Loading [{pluginInfo.Metadata.Name} {pluginInfo.Metadata.Version}]");
 
- 						if (!loadedAssemblies.TryGetValue(pluginInfo.Location, out var ass))
 
- 							loadedAssemblies[pluginInfo.Location] = ass = Assembly.LoadFile(pluginInfo.Location);
 
- 						PluginInfos[pluginGUID] = pluginInfo;
 
- 						pluginInfo.Instance = (BaseUnityPlugin)ManagerObject.AddComponent(ass.GetType(pluginInfo.TypeName));
 
- 						_plugins.Add(pluginInfo.Instance);
 
- 					}
 
- 					catch (Exception ex)
 
- 					{
 
- 						invalidPlugins.Add(pluginGUID);
 
- 						PluginInfos.Remove(pluginGUID);
 
- 						Logger.LogError($"Error loading [{pluginInfo.Metadata.Name}] : {ex.Message}");
 
- 						if (ex is ReflectionTypeLoadException re)
 
- 							Logger.LogDebug(TypeLoader.TypeLoadExceptionToString(re));
 
- 						else
 
- 							Logger.LogDebug(ex);
 
- 					}
 
- 				}
 
- 			}
 
- 			catch (Exception ex)
 
- 			{
 
- 				ConsoleWindow.Attach();
 
- 				Console.WriteLine("Error occurred starting the game");
 
- 				Console.WriteLine(ex.ToString());
 
- 			}
 
- 			Logger.LogMessage("Chainloader startup complete");
 
- 			_loaded = true;
 
- 		}
 
- 		#region Config
 
- 		private static readonly ConfigEntry<bool> ConfigUnityLogging = ConfigFile.CoreConfig.Bind(
 
- 			"Logging", "UnityLogListening",
 
- 			true,
 
- 			"Enables showing unity log messages in the BepInEx logging system.");
 
- 		private static readonly ConfigEntry<bool> ConfigDiskWriteUnityLog = ConfigFile.CoreConfig.Bind(
 
- 			"Logging.Disk", "WriteUnityLog",
 
- 			false,
 
- 			"Include unity log messages in log file output.");
 
- 		private static readonly ConfigEntry<bool> ConfigDiskAppend = ConfigFile.CoreConfig.Bind(
 
- 			"Logging.Disk", "AppendLog",
 
- 			false,
 
- 			"Appends to the log file instead of overwriting, on game startup.");
 
- 		private static readonly ConfigEntry<bool> ConfigDiskLogging = ConfigFile.CoreConfig.Bind(
 
- 			"Logging.Disk", "Enabled",
 
- 			true,
 
- 			"Enables writing log messages to disk.");
 
- 		private static readonly ConfigEntry<LogLevel> ConfigDiskConsoleDisplayedLevel = ConfigFile.CoreConfig.Bind(
 
- 			"Logging.Disk", "DisplayedLogLevel",
 
- 			LogLevel.Info,
 
- 			"Only displays the specified log level and above in the console output.");
 
- 		#endregion
 
- 	}
 
- }
 
 
  |