예제 #1
0
        /// <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();
        }
예제 #2
0
        /// <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));
            }
        }
예제 #3
0
        /// <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}");
                }
            }
        }