/// <summary> /// Performs work to dispose collection objects. /// </summary> public void Dispose() { foreach (var assembly in AssembliesToPatch) { assembly.Value.Dispose(); } AssembliesToPatch.Clear(); // Clear to allow GC collection. PatcherPlugins.Clear(); }
/// <summary> /// Adds all assemblies in a directory to be patched and loaded by this patcher instance. Non-managed assemblies are skipped. /// </summary> /// <param name="directory">The directory to search.</param> /// <param name="assemblyExtensions">The file extensions to attempt to load.</param> public void LoadAssemblyDirectory(string directory, params string[] assemblyExtensions) { var filesToSearch = assemblyExtensions .SelectMany(ext => Directory.GetFiles(directory, "*." + ext, SearchOption.TopDirectoryOnly)); foreach (string 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; } AssembliesToPatch.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)); } }
/// <summary> /// Applies patchers to all assemblies in the given directory and loads patched assemblies into memory. /// </summary> /// <param name="directory">Directory to load CLR assemblies from.</param> public void PatchAndLoad() { // First, create a copy of the assembly dictionary as the initializer can change them var assemblies = new Dictionary <string, AssemblyDefinition>(AssembliesToPatch, StringComparer.InvariantCultureIgnoreCase); // Next, initialize all the patchers foreach (var assemblyPatcher in PatcherPluginsSafe) { try { assemblyPatcher.Initializer?.Invoke(); } catch (Exception ex) { Logger.LogError($"Failed to run initializer of {assemblyPatcher.TypeName}: {ex}"); } } // Then, perform the actual patching var patchedAssemblies = new HashSet <string>(StringComparer.InvariantCultureIgnoreCase); var resolvedAssemblies = new Dictionary <string, string>(); // TODO: Maybe instead reload the assembly and repatch with other valid patchers? var invalidAssemblies = new HashSet <string>(StringComparer.InvariantCultureIgnoreCase); foreach (var assemblyPatcher in PatcherPluginsSafe) { foreach (string targetDll in assemblyPatcher.TargetDLLs()) { if (AssembliesToPatch.TryGetValue(targetDll, out var assembly) && !invalidAssemblies.Contains(targetDll)) { Logger.LogInfo($"Patching [{assembly.Name.Name}] with [{assemblyPatcher.TypeName}]"); try { assemblyPatcher.Patcher?.Invoke(ref assembly); } catch (Exception e) { Logger.LogError($"Failed to run [{assemblyPatcher.TypeName}] when patching [{assembly.Name.Name}]. This assembly will not be patched. Error: {e}"); patchedAssemblies.Remove(targetDll); invalidAssemblies.Add(targetDll); continue; } AssembliesToPatch[targetDll] = assembly; patchedAssemblies.Add(targetDll); 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] = assemblyPatcher.TypeName; } } } } } // 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 <string>(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()); } // Finally, load patched assemblies into memory if (ConfigDumpAssemblies.Value || ConfigLoadDumpedAssemblies.Value) { if (!Directory.Exists(DumpedAssembliesPath)) { Directory.CreateDirectory(DumpedAssembliesPath); } foreach (KeyValuePair <string, AssemblyDefinition> kv in assemblies) { string filename = kv.Key; var assembly = kv.Value; if (patchedAssemblies.Contains(filename)) { assembly.Write(Path.Combine(DumpedAssembliesPath, filename)); } } } 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 {DumpedAssembliesPath}"); Logger.LogInfo("Load any assemblies into the debugger, set breakpoints and continue execution."); Debugger.Break(); } foreach (var kv in assemblies) { string filename = kv.Key; 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)) { Assembly loadedAssembly; if (ConfigLoadDumpedAssemblies.Value) { loadedAssembly = Assembly.LoadFile(Path.Combine(DumpedAssembliesPath, filename)); } else { using (var assemblyStream = new MemoryStream()) { assembly.Write(assemblyStream); loadedAssembly = Assembly.Load(assemblyStream.ToArray()); } } 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?.Invoke(); } catch (Exception ex) { Logger.LogError($"Failed to run finalizer of {assemblyPatcher.TypeName}: {ex}"); } } }