mirror of
https://github.com/BepInEx/BepInEx.git
synced 2026-10-19 15:51:45 +00:00
Move preloader to a separate project
This commit is contained in:
1 parent
886856c4aa
commit
bd7597fa56
17 files changed
+707
-581
No files matched your search
@@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>BepInEx.Preloader</RootNamespace>
|
||||
<AssemblyName>BepInEx.Preloader</AssemblyName>
|
||||
<TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<Deterministic>true</Deterministic>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>..\bin\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'v2018|AnyCPU'">
|
||||
<OutputPath>bin\v2018\</OutputPath>
|
||||
<DefineConstants>TRACE;UNITY_2018</DefineConstants>
|
||||
<Optimize>true</Optimize>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Mono.Cecil">
|
||||
<HintPath>..\lib\Mono.Cecil.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Patcher\AssemblyPatcher.cs" />
|
||||
<Compile Include="Entrypoint.cs" />
|
||||
<Compile Include="Patcher\PatcherPlugin.cs" />
|
||||
<Compile Include="Preloader.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="UnityPatches.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BepInEx\BepInEx.csproj">
|
||||
<Project>{4ffba620-f5ed-47f9-b90c-dad1316fd9b9}</Project>
|
||||
<Name>BepInEx</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\submodules\BepInEx.Harmony\BepInEx.Harmony\BepInEx.Harmony.csproj">
|
||||
<Project>{54161cfe-ff42-4dde-b161-3a49545db5cd}</Project>
|
||||
<Name>BepInEx.Harmony</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\submodules\BepInEx.Harmony\submodules\Harmony\Harmony\Harmony.csproj">
|
||||
<Project>{a15d6ee6-f954-415b-8605-8a8470cc87dc}</Project>
|
||||
<Name>Harmony</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
@@ -2,9 +2,9 @@
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace BepInEx.Bootstrap
|
||||
namespace BepInEx.Preloader
|
||||
{
|
||||
public static class Entrypoint
|
||||
internal static class Entrypoint
|
||||
{
|
||||
/// <summary>
|
||||
/// The main entrypoint of BepInEx, called from Doorstop.
|
||||
@@ -15,7 +15,9 @@ namespace BepInEx.Bootstrap
|
||||
/// </param>
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
Paths.ExecutablePath = args[0];
|
||||
// Manually set up the path for patchers to work
|
||||
typeof(Paths).GetProperty(nameof(Paths.ExecutablePath)).SetValue(null, args[0], null);
|
||||
//Paths.ExecutablePath = args[0];
|
||||
AppDomain.CurrentDomain.AssemblyResolve += LocalResolve;
|
||||
|
||||
Preloader.Run();
|
||||
@@ -35,7 +37,8 @@ namespace BepInEx.Bootstrap
|
||||
{
|
||||
var assemblyName = new AssemblyName(args.Name);
|
||||
|
||||
var foundAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(x => x.GetName().Name == assemblyName.Name);
|
||||
var foundAssembly = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.FirstOrDefault(x => x.GetName().Name == assemblyName.Name);
|
||||
|
||||
if (foundAssembly != null)
|
||||
return foundAssembly;
|
||||
@@ -0,0 +1,180 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using BepInEx.Logging;
|
||||
using Mono.Cecil;
|
||||
|
||||
namespace BepInEx.Preloader.Patcher
|
||||
{
|
||||
/// <summary>
|
||||
/// Delegate used in patching assemblies.
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly that is being patched.</param>
|
||||
internal delegate void AssemblyPatcherDelegate(ref AssemblyDefinition assembly);
|
||||
|
||||
/// <summary>
|
||||
/// Worker class which is used for loading and patching entire folders of assemblies, or alternatively patching and
|
||||
/// loading assemblies one at a time.
|
||||
/// </summary>
|
||||
internal static class AssemblyPatcher
|
||||
{
|
||||
private static readonly List<PatcherPlugin> patchers = new List<PatcherPlugin>();
|
||||
|
||||
/// <summary>
|
||||
/// Configuration value of whether assembly dumping is enabled or not.
|
||||
/// </summary>
|
||||
private static bool DumpingEnabled =>
|
||||
Utility.SafeParseBool(Config.GetEntry("dump-assemblies", "false", "Preloader"));
|
||||
|
||||
/// <summary>
|
||||
/// Adds a single assembly patcher to the pool of applicable patches.
|
||||
/// </summary>
|
||||
/// <param name="patcher">Patcher to apply.</param>
|
||||
public static void AddPatcher(PatcherPlugin patcher)
|
||||
{
|
||||
patchers.Add(patcher);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds all patchers from all managed assemblies specified in a directory.
|
||||
/// </summary>
|
||||
/// <param name="directory">Directory to search patcher DLLs from.</param>
|
||||
/// <param name="patcherLocator">A function that locates assembly patchers in a given managed assembly.</param>
|
||||
public static void AddPatchersFromDirectory(string directory,
|
||||
Func<Assembly, List<PatcherPlugin>> patcherLocator)
|
||||
{
|
||||
if (!Directory.Exists(directory))
|
||||
return;
|
||||
|
||||
var sortedPatchers = new SortedDictionary<string, PatcherPlugin>();
|
||||
|
||||
foreach (string assemblyPath in Directory.GetFiles(directory, "*.dll"))
|
||||
try
|
||||
{
|
||||
var assembly = Assembly.LoadFrom(assemblyPath);
|
||||
|
||||
foreach (var patcher in patcherLocator(assembly))
|
||||
sortedPatchers.Add(patcher.Name, patcher);
|
||||
}
|
||||
catch (BadImageFormatException)
|
||||
{
|
||||
} //unmanaged DLL
|
||||
catch (ReflectionTypeLoadException)
|
||||
{
|
||||
} //invalid references
|
||||
|
||||
foreach (KeyValuePair<string, PatcherPlugin> patcher in sortedPatchers)
|
||||
AddPatcher(patcher.Value);
|
||||
}
|
||||
|
||||
private static void InitializePatchers()
|
||||
{
|
||||
foreach (var assemblyPatcher in patchers)
|
||||
assemblyPatcher.Initializer?.Invoke();
|
||||
}
|
||||
|
||||
private static void FinalizePatching()
|
||||
{
|
||||
foreach (var assemblyPatcher in patchers)
|
||||
assemblyPatcher.Finalizer?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases all patchers to let them be collected by GC.
|
||||
/// </summary>
|
||||
public static void DisposePatchers()
|
||||
{
|
||||
patchers.Clear();
|
||||
}
|
||||
|
||||
/// <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 static void PatchAndLoad(string directory)
|
||||
{
|
||||
// First, load patchable assemblies into Cecil
|
||||
var assemblies = new Dictionary<string, AssemblyDefinition>();
|
||||
|
||||
foreach (string assemblyPath in Directory.GetFiles(directory, "*.dll"))
|
||||
{
|
||||
var assembly = AssemblyDefinition.ReadAssembly(assemblyPath);
|
||||
|
||||
//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;
|
||||
}
|
||||
|
||||
if (UnityPatches.AssemblyLocations.ContainsKey(assembly.FullName))
|
||||
{
|
||||
Logger.Log(LogLevel.Warning,
|
||||
$"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));
|
||||
}
|
||||
|
||||
// Next, initialize all the patchers
|
||||
InitializePatchers();
|
||||
|
||||
// Then, perform the actual patching
|
||||
var patchedAssemblies = new HashSet<string>();
|
||||
foreach (var assemblyPatcher in patchers)
|
||||
foreach (string targetDll in assemblyPatcher.TargetDLLs)
|
||||
if (assemblies.TryGetValue(targetDll, out var assembly))
|
||||
{
|
||||
assemblyPatcher.Patcher?.Invoke(ref assembly);
|
||||
assemblies[targetDll] = assembly;
|
||||
patchedAssemblies.Add(targetDll);
|
||||
}
|
||||
|
||||
// Finally, load all assemblies into memory
|
||||
foreach (KeyValuePair<string, AssemblyDefinition> kv in assemblies)
|
||||
{
|
||||
string filename = kv.Key;
|
||||
var assembly = kv.Value;
|
||||
|
||||
if (DumpingEnabled && patchedAssemblies.Contains(filename))
|
||||
using (var mem = new MemoryStream())
|
||||
{
|
||||
string dirPath = Path.Combine(Paths.PluginPath, "DumpedAssemblies");
|
||||
|
||||
if (!Directory.Exists(dirPath))
|
||||
Directory.CreateDirectory(dirPath);
|
||||
|
||||
assembly.Write(mem);
|
||||
File.WriteAllBytes(Path.Combine(dirPath, filename), mem.ToArray());
|
||||
}
|
||||
|
||||
Load(assembly);
|
||||
assembly.Dispose();
|
||||
}
|
||||
|
||||
//run all finalizers
|
||||
FinalizePatching();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads an individual assembly defintion into the CLR.
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly to load.</param>
|
||||
public static void Load(AssemblyDefinition assembly)
|
||||
{
|
||||
using (var assemblyStream = new MemoryStream())
|
||||
{
|
||||
assembly.Write(assemblyStream);
|
||||
Assembly.Load(assemblyStream.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BepInEx.Preloader.Patcher
|
||||
{
|
||||
/// <summary>
|
||||
/// A single assembly patcher.
|
||||
/// </summary>
|
||||
internal class PatcherPlugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Target assemblies to patch.
|
||||
/// </summary>
|
||||
public IEnumerable<string> TargetDLLs { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Initializer method that is run before any patching occurs.
|
||||
/// </summary>
|
||||
public Action Initializer { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Finalizer method that is run after all patching is done.
|
||||
/// </summary>
|
||||
public Action Finalizer { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// The main patcher method that is called on every DLL defined in <see cref="TargetDLLs" />.
|
||||
/// </summary>
|
||||
public AssemblyPatcherDelegate Patcher { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Name of the patcher.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using BepInEx.Logging;
|
||||
using BepInEx.Preloader.Patcher;
|
||||
using Mono.Cecil;
|
||||
using Mono.Cecil.Cil;
|
||||
using UnityInjector.ConsoleUtil;
|
||||
using MethodAttributes = Mono.Cecil.MethodAttributes;
|
||||
|
||||
namespace BepInEx.Preloader
|
||||
{
|
||||
/// <summary>
|
||||
/// The main entrypoint of BepInEx, and initializes all patchers and the chainloader.
|
||||
/// </summary>
|
||||
internal static class Preloader
|
||||
{
|
||||
/// <summary>
|
||||
/// The log writer that is specific to the preloader.
|
||||
/// </summary>
|
||||
private static PreloaderLogWriter PreloaderLog { get; set; }
|
||||
|
||||
public static void Run()
|
||||
{
|
||||
try
|
||||
{
|
||||
AllocateConsole();
|
||||
|
||||
UnityPatches.Apply();
|
||||
|
||||
PreloaderLog =
|
||||
new PreloaderLogWriter(
|
||||
Utility.SafeParseBool(Config.GetEntry("preloader-logconsole", "false", "BepInEx")));
|
||||
PreloaderLog.Enabled = true;
|
||||
|
||||
string consoleTile =
|
||||
$"BepInEx {typeof(Paths).Assembly.GetName().Version} - {Process.GetCurrentProcess().ProcessName}";
|
||||
ConsoleWindow.Title = consoleTile;
|
||||
|
||||
Logger.SetLogger(PreloaderLog);
|
||||
|
||||
PreloaderLog.WriteLine(consoleTile);
|
||||
|
||||
#if DEBUG
|
||||
object[] attributes =
|
||||
typeof(DebugInfoAttribute).Assembly.GetCustomAttributes(typeof(DebugInfoAttribute), false);
|
||||
|
||||
if (attributes.Length > 0)
|
||||
{
|
||||
var attribute = (DebugInfoAttribute) attributes[0];
|
||||
|
||||
PreloaderLog.WriteLine(attribute.Info);
|
||||
}
|
||||
#endif
|
||||
|
||||
Logger.Log(LogLevel.Message, "Preloader started");
|
||||
|
||||
string entrypointAssembly = Config.GetEntry("entrypoint-assembly", "UnityEngine.dll", "Preloader");
|
||||
|
||||
AssemblyPatcher.AddPatcher(new PatcherPlugin
|
||||
{TargetDLLs = new[] {entrypointAssembly}, Patcher = PatchEntrypoint});
|
||||
AssemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath, GetPatcherMethods);
|
||||
|
||||
AssemblyPatcher.PatchAndLoad(Paths.ManagedPath);
|
||||
|
||||
AssemblyPatcher.DisposePatchers();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log(LogLevel.Fatal, "Could not run preloader!");
|
||||
Logger.Log(LogLevel.Fatal, ex);
|
||||
|
||||
PreloaderLog.Enabled = false;
|
||||
|
||||
try
|
||||
{
|
||||
if (!ConsoleWindow.IsAttatched)
|
||||
{
|
||||
//if we've already attached the console, then the log will already be written to the console
|
||||
AllocateConsole();
|
||||
Console.Write(PreloaderLog);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.WriteAllText(
|
||||
Path.Combine(Paths.GameRootPath, $"preloader_{DateTime.Now:yyyyMMdd_HHmmss_fff}.log"),
|
||||
PreloaderLog.ToString());
|
||||
|
||||
PreloaderLog.Dispose();
|
||||
PreloaderLog = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scans the assembly for classes that use the patcher contract, and returns a list of valid patchers.
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly to scan.</param>
|
||||
/// <returns>A list of assembly patchers that were found in the assembly.</returns>
|
||||
public static List<PatcherPlugin> GetPatcherMethods(Assembly assembly)
|
||||
{
|
||||
var patcherMethods = new List<PatcherPlugin>();
|
||||
var flags = BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase;
|
||||
|
||||
foreach (var type in assembly.GetExportedTypes())
|
||||
try
|
||||
{
|
||||
if (type.IsInterface)
|
||||
continue;
|
||||
|
||||
var targetsProperty = type.GetProperty("TargetDLLs",
|
||||
flags,
|
||||
null,
|
||||
typeof(IEnumerable<string>),
|
||||
Type.EmptyTypes,
|
||||
null);
|
||||
|
||||
//first try get the ref patcher method
|
||||
var patcher = type.GetMethod("Patch",
|
||||
flags,
|
||||
null,
|
||||
CallingConventions.Any,
|
||||
new[] {typeof(AssemblyDefinition).MakeByRefType()},
|
||||
null);
|
||||
|
||||
if (patcher == null) //otherwise try getting the non-ref patcher method
|
||||
patcher = type.GetMethod("Patch",
|
||||
flags,
|
||||
null,
|
||||
CallingConventions.Any,
|
||||
new[] {typeof(AssemblyDefinition)},
|
||||
null);
|
||||
|
||||
if (targetsProperty == null || !targetsProperty.CanRead || patcher == null)
|
||||
continue;
|
||||
|
||||
var assemblyPatcher = new PatcherPlugin();
|
||||
|
||||
assemblyPatcher.Name = $"{assembly.GetName().Name}{type.FullName}";
|
||||
assemblyPatcher.Patcher = (ref AssemblyDefinition ass) =>
|
||||
{
|
||||
//we do the array fuckery here to get the ref result out
|
||||
object[] args = {ass};
|
||||
|
||||
patcher.Invoke(null, args);
|
||||
|
||||
ass = (AssemblyDefinition) args[0];
|
||||
};
|
||||
|
||||
assemblyPatcher.TargetDLLs = (IEnumerable<string>) targetsProperty.GetValue(null, null);
|
||||
|
||||
var initMethod = type.GetMethod("Initialize",
|
||||
flags,
|
||||
null,
|
||||
CallingConventions.Any,
|
||||
Type.EmptyTypes,
|
||||
null);
|
||||
|
||||
if (initMethod != null)
|
||||
assemblyPatcher.Initializer = () => initMethod.Invoke(null, null);
|
||||
|
||||
var finalizeMethod = type.GetMethod("Finish",
|
||||
flags,
|
||||
null,
|
||||
CallingConventions.Any,
|
||||
Type.EmptyTypes,
|
||||
null);
|
||||
|
||||
if (finalizeMethod != null)
|
||||
assemblyPatcher.Finalizer = () => finalizeMethod.Invoke(null, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log(LogLevel.Warning, $"Could not load patcher methods from {assembly.GetName().Name}");
|
||||
Logger.Log(LogLevel.Warning, $"{ex}");
|
||||
}
|
||||
|
||||
Logger.Log(LogLevel.Info,
|
||||
$"Loaded {patcherMethods.Count} patcher methods from {assembly.GetName().Name}");
|
||||
|
||||
return patcherMethods;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inserts BepInEx's own chainloader entrypoint into UnityEngine.
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly that will be attempted to be patched.</param>
|
||||
public static void PatchEntrypoint(ref AssemblyDefinition assembly)
|
||||
{
|
||||
if (assembly.MainModule.AssemblyReferences.Any(x => x.Name.Contains("BepInEx")))
|
||||
throw new Exception(
|
||||
"BepInEx has been detected to be patched! Please unpatch before using a patchless variant!");
|
||||
|
||||
string entrypointType = Config.GetEntry("entrypoint-type", "Application", "Preloader");
|
||||
string entrypointMethod = Config.GetEntry("entrypoint-method", ".cctor", "Preloader");
|
||||
|
||||
bool isCctor = entrypointMethod.IsNullOrWhiteSpace() || entrypointMethod == ".cctor";
|
||||
|
||||
|
||||
var entryType = assembly.MainModule.Types.FirstOrDefault(x => x.Name == entrypointType);
|
||||
|
||||
if (entryType == null) throw new Exception("The entrypoint type is invalid! Please check your config.ini");
|
||||
|
||||
using (var injected = AssemblyDefinition.ReadAssembly(Paths.BepInExAssemblyPath))
|
||||
{
|
||||
var originalInitMethod = injected.MainModule.Types.First(x => x.Name == "Chainloader").Methods
|
||||
.First(x => x.Name == "Initialize");
|
||||
|
||||
var originalStartMethod = injected.MainModule.Types.First(x => x.Name == "Chainloader").Methods
|
||||
.First(x => x.Name == "Start");
|
||||
|
||||
var initMethod = assembly.MainModule.ImportReference(originalInitMethod);
|
||||
var startMethod = assembly.MainModule.ImportReference(originalStartMethod);
|
||||
|
||||
var methods = new List<MethodDefinition>();
|
||||
|
||||
if (isCctor)
|
||||
{
|
||||
var cctor = entryType.Methods.FirstOrDefault(m => m.IsConstructor && m.IsStatic);
|
||||
|
||||
if (cctor == null)
|
||||
{
|
||||
cctor = new MethodDefinition(".cctor",
|
||||
MethodAttributes.Static | MethodAttributes.Private | MethodAttributes.HideBySig
|
||||
| MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
|
||||
assembly.MainModule.ImportReference(typeof(void)));
|
||||
|
||||
entryType.Methods.Add(cctor);
|
||||
var il = cctor.Body.GetILProcessor();
|
||||
il.Append(il.Create(OpCodes.Ret));
|
||||
}
|
||||
|
||||
methods.Add(cctor);
|
||||
}
|
||||
else
|
||||
{
|
||||
methods.AddRange(entryType.Methods.Where(x => x.Name == entrypointMethod));
|
||||
}
|
||||
|
||||
if (!methods.Any())
|
||||
throw new Exception("The entrypoint method is invalid! Please check your config.ini");
|
||||
|
||||
foreach (var method in methods)
|
||||
{
|
||||
var il = method.Body.GetILProcessor();
|
||||
|
||||
var ins = il.Body.Instructions.First();
|
||||
|
||||
il.InsertBefore(ins, il.Create(OpCodes.Ldstr, Paths.ExecutablePath)); //containerExePath
|
||||
il.InsertBefore(ins,
|
||||
il.Create(OpCodes
|
||||
.Ldc_I4_0)); //startConsole (always false, we already load the console in Preloader)
|
||||
il.InsertBefore(ins,
|
||||
il.Create(OpCodes.Call,
|
||||
initMethod)); //Chainloader.Initialize(string containerExePath, bool startConsole = true)
|
||||
il.InsertBefore(ins, il.Create(OpCodes.Call, startMethod));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocates a console window for use by BepInEx safely.
|
||||
/// </summary>
|
||||
public static void AllocateConsole()
|
||||
{
|
||||
bool console = Utility.SafeParseBool(Config.GetEntry("console", "false", "BepInEx"));
|
||||
bool shiftjis = Utility.SafeParseBool(Config.GetEntry("console-shiftjis", "false", "BepInEx"));
|
||||
|
||||
if (!console)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
ConsoleWindow.Attach();
|
||||
|
||||
var encoding = (uint) Encoding.UTF8.CodePage;
|
||||
|
||||
if (shiftjis)
|
||||
encoding = 932;
|
||||
|
||||
ConsoleEncoding.ConsoleCodePage = encoding;
|
||||
Console.OutputEncoding = ConsoleEncoding.GetEncoding(encoding);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log(LogLevel.Error, "Failed to allocate console!");
|
||||
Logger.Log(LogLevel.Error, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("BepInEx.Preloader")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("BepInEx.Preloader")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2019")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("f7abbe07-c02f-4f7c-bf6e-c6656bf588ca")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using BepInEx.Harmony;
|
||||
using Harmony;
|
||||
|
||||
namespace BepInEx.Preloader
|
||||
{
|
||||
internal static class UnityPatches
|
||||
{
|
||||
public static HarmonyInstance HarmonyInstance { get; } = HarmonyInstance.Create("com.bepinex.unitypatches");
|
||||
|
||||
public static Dictionary<string, string> AssemblyLocations { get; } =
|
||||
new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
|
||||
|
||||
public static void Apply()
|
||||
{
|
||||
HarmonyWrapper.PatchAll(typeof(UnityPatches), HarmonyInstance);
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(Assembly), nameof(Assembly.Location), MethodType.Getter)]
|
||||
public static void GetLocation(ref string __result, Assembly __instance)
|
||||
{
|
||||
if (AssemblyLocations.TryGetValue(__instance.FullName, out string location))
|
||||
__result = location;
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(Assembly), nameof(Assembly.CodeBase), MethodType.Getter)]
|
||||
public static void GetCodeBase(ref string __result, Assembly __instance)
|
||||
{
|
||||
if (AssemblyLocations.TryGetValue(__instance.FullName, out string location))
|
||||
__result = $"file://{location.Replace('\\', '/')}";
|
||||
}
|
||||
|
||||
#if UNITY_2018
|
||||
/*
|
||||
* DESC: Workaround for Trace class not working because of missing .config file
|
||||
* AFFECTS: Unity 2018+
|
||||
*/
|
||||
[HarmonyPostfix, HarmonyPatch(typeof(AppDomain), nameof(AppDomain.SetupInformation), MethodType.Getter)]
|
||||
public static void GetExeConfigName(AppDomainSetup __result)
|
||||
{
|
||||
__result.ApplicationBase = $"file://{Paths.GameRootPath}";
|
||||
__result.ConfigurationFile = "app.config";
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
+22
@@ -21,11 +21,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Harmony", "submodules\BepIn
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Patcher", "Patcher", "{A9071994-3533-4C1B-89DC-D817B676AB41}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BepInEx.Preloader", "BepInEx.Preloader\BepInEx.Preloader.csproj", "{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
v2018 Release|Any CPU = v2018 Release|Any CPU
|
||||
v2018|Any CPU = v2018|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
@@ -34,30 +37,48 @@ Global
|
||||
{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.v2018 Release|Any CPU.ActiveCfg = v2018|Any CPU
|
||||
{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.v2018 Release|Any CPU.Build.0 = v2018|Any CPU
|
||||
{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.v2018|Any CPU.ActiveCfg = v2018|Any CPU
|
||||
{4FFBA620-F5ED-47F9-B90C-DAD1316FD9B9}.v2018|Any CPU.Build.0 = v2018|Any CPU
|
||||
{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Debug|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Debug|Any CPU.Build.0 = Release|Any CPU
|
||||
{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.v2018 Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.v2018 Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.v2018|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DC89F18B-235B-4C01-AB31-AF40DCE5C4C7}.v2018|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Debug|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Debug|Any CPU.Build.0 = Release|Any CPU
|
||||
{6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.v2018 Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.v2018 Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.v2018|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB}.v2018|Any CPU.Build.0 = Release|Any CPU
|
||||
{54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Debug|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Debug|Any CPU.Build.0 = Release|Any CPU
|
||||
{54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{54161CFE-FF42-4DDE-B161-3A49545DB5CD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{54161CFE-FF42-4DDE-B161-3A49545DB5CD}.v2018 Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{54161CFE-FF42-4DDE-B161-3A49545DB5CD}.v2018 Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{54161CFE-FF42-4DDE-B161-3A49545DB5CD}.v2018|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{54161CFE-FF42-4DDE-B161-3A49545DB5CD}.v2018|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A15D6EE6-F954-415B-8605-8A8470CC87DC}.Debug|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A15D6EE6-F954-415B-8605-8A8470CC87DC}.Debug|Any CPU.Build.0 = Release|Any CPU
|
||||
{A15D6EE6-F954-415B-8605-8A8470CC87DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A15D6EE6-F954-415B-8605-8A8470CC87DC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A15D6EE6-F954-415B-8605-8A8470CC87DC}.v2018 Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A15D6EE6-F954-415B-8605-8A8470CC87DC}.v2018 Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A15D6EE6-F954-415B-8605-8A8470CC87DC}.v2018|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A15D6EE6-F954-415B-8605-8A8470CC87DC}.v2018|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.v2018 Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.v2018 Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.v2018|Any CPU.ActiveCfg = v2018|Any CPU
|
||||
{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA}.v2018|Any CPU.Build.0 = v2018|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -67,6 +88,7 @@ Global
|
||||
{6E6BC1E5-5BE8-4566-B3AE-52C4CB218AEB} = {A9071994-3533-4C1B-89DC-D817B676AB41}
|
||||
{54161CFE-FF42-4DDE-B161-3A49545DB5CD} = {BAC58F7E-AAD8-4D0C-9490-9765ACBBA6FB}
|
||||
{A15D6EE6-F954-415B-8605-8A8470CC87DC} = {BAC58F7E-AAD8-4D0C-9490-9765ACBBA6FB}
|
||||
{F7ABBE07-C02F-4F7C-BF6E-C6656BF588CA} = {A9071994-3533-4C1B-89DC-D817B676AB41}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {55AC11EF-F568-4C79-A356-7ED9510145B1}
|
||||
|
||||
@@ -56,11 +56,7 @@
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Bootstrap\Entrypoint.cs" />
|
||||
<Compile Include="Bootstrap\UnityPatches.cs" />
|
||||
<Compile Include="Contract\Attributes.cs" />
|
||||
<Compile Include="Bootstrap\AssemblyPatcher.cs" />
|
||||
<Compile Include="Bootstrap\Preloader.cs" />
|
||||
<Compile Include="Config.cs" />
|
||||
<Compile Include="ConfigWrapper.cs" />
|
||||
<Compile Include="ConsoleUtil\ConsoleEncoding\ConsoleEncoding.Buffers.cs" />
|
||||
|
||||
@@ -1,239 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using BepInEx.Harmony;
|
||||
using BepInEx.Logging;
|
||||
using Harmony;
|
||||
using Mono.Cecil;
|
||||
|
||||
namespace BepInEx.Bootstrap
|
||||
{
|
||||
/// <summary>
|
||||
/// Delegate used in patching assemblies.
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly that is being patched.</param>
|
||||
internal delegate void AssemblyPatcherDelegate(ref AssemblyDefinition assembly);
|
||||
|
||||
/// <summary>
|
||||
/// A single assembly patcher.
|
||||
/// </summary>
|
||||
internal class PatcherPlugin
|
||||
{
|
||||
/// <summary>
|
||||
/// Target assemblies to patch.
|
||||
/// </summary>
|
||||
public IEnumerable<string> TargetDLLs { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Initializer method that is run before any patching occurs.
|
||||
/// </summary>
|
||||
public Action Initializer { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Finalizer method that is run after all patching is done.
|
||||
/// </summary>
|
||||
public Action Finalizer { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// The main patcher method that is called on every DLL defined in <see cref="TargetDLLs"/>.
|
||||
/// </summary>
|
||||
public AssemblyPatcherDelegate Patcher { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Name of the patcher.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Worker class which is used for loading and patching entire folders of assemblies, or alternatively patching and loading assemblies one at a time.
|
||||
/// </summary>
|
||||
internal static class AssemblyPatcher
|
||||
{
|
||||
private static List<PatcherPlugin> patchers = new List<PatcherPlugin>();
|
||||
|
||||
/// <summary>
|
||||
/// Configuration value of whether assembly dumping is enabled or not.
|
||||
/// </summary>
|
||||
private static bool DumpingEnabled => Utility.SafeParseBool(Config.GetEntry("dump-assemblies", "false", "Preloader"));
|
||||
|
||||
/// <summary>
|
||||
/// Adds a single assembly patcher to the pool of applicable patches.
|
||||
/// </summary>
|
||||
/// <param name="patcher">Patcher to apply.</param>
|
||||
public static void AddPatcher(PatcherPlugin patcher)
|
||||
{
|
||||
patchers.Add(patcher);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds all patchers from all managed assemblies specified in a directory.
|
||||
/// </summary>
|
||||
/// <param name="directory">Directory to search patcher DLLs from.</param>
|
||||
/// <param name="patcherLocator">A function that locates assembly patchers in a given managed assembly.</param>
|
||||
public static void AddPatchersFromDirectory(string directory, Func<Assembly, List<PatcherPlugin>> patcherLocator)
|
||||
{
|
||||
if (!Directory.Exists(directory))
|
||||
return;
|
||||
|
||||
var sortedPatchers = new SortedDictionary<string, PatcherPlugin>();
|
||||
|
||||
foreach (string assemblyPath in Directory.GetFiles(directory, "*.dll"))
|
||||
try
|
||||
{
|
||||
var assembly = Assembly.LoadFrom(assemblyPath);
|
||||
|
||||
foreach (var patcher in patcherLocator(assembly))
|
||||
sortedPatchers.Add(patcher.Name, patcher);
|
||||
}
|
||||
catch (BadImageFormatException) { } //unmanaged DLL
|
||||
catch (ReflectionTypeLoadException) { } //invalid references
|
||||
|
||||
foreach (var patcher in sortedPatchers)
|
||||
AddPatcher(patcher.Value);
|
||||
}
|
||||
|
||||
private static void InitializePatchers()
|
||||
{
|
||||
foreach (var assemblyPatcher in patchers)
|
||||
assemblyPatcher.Initializer?.Invoke();
|
||||
}
|
||||
|
||||
private static void FinalizePatching()
|
||||
{
|
||||
foreach (var assemblyPatcher in patchers)
|
||||
assemblyPatcher.Finalizer?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases all patchers to let them be collected by GC.
|
||||
/// </summary>
|
||||
public static void DisposePatchers()
|
||||
{
|
||||
patchers.Clear();
|
||||
}
|
||||
|
||||
/// <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 static void PatchAndLoad(string directory)
|
||||
{
|
||||
|
||||
// First, load patchable assemblies into Cecil
|
||||
Dictionary<string, AssemblyDefinition> assemblies = new Dictionary<string, AssemblyDefinition>();
|
||||
|
||||
foreach (string assemblyPath in Directory.GetFiles(directory, "*.dll"))
|
||||
{
|
||||
var assembly = AssemblyDefinition.ReadAssembly(assemblyPath);
|
||||
|
||||
//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;
|
||||
}
|
||||
if (PatchedAssemblyResolver.AssemblyLocations.ContainsKey(assembly.FullName))
|
||||
{
|
||||
Logger.Log(LogLevel.Warning, $"Tried to load duplicate assembly {Path.GetFileName(assemblyPath)} from Managed folder! Skipping...");
|
||||
continue;
|
||||
}
|
||||
|
||||
assemblies.Add(Path.GetFileName(assemblyPath), assembly);
|
||||
PatchedAssemblyResolver.AssemblyLocations.Add(assembly.FullName, Path.GetFullPath(assemblyPath));
|
||||
}
|
||||
|
||||
// Next, initialize all the patchers
|
||||
InitializePatchers();
|
||||
|
||||
// Then, perform the actual patching
|
||||
HashSet<string> patchedAssemblies = new HashSet<string>();
|
||||
foreach (var assemblyPatcher in patchers)
|
||||
{
|
||||
foreach (string targetDll in assemblyPatcher.TargetDLLs)
|
||||
{
|
||||
if (assemblies.TryGetValue(targetDll, out var assembly))
|
||||
{
|
||||
assemblyPatcher.Patcher?.Invoke(ref assembly);
|
||||
assemblies[targetDll] = assembly;
|
||||
patchedAssemblies.Add(targetDll);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, load all assemblies into memory
|
||||
foreach (var kv in assemblies)
|
||||
{
|
||||
string filename = kv.Key;
|
||||
var assembly = kv.Value;
|
||||
|
||||
if (DumpingEnabled && patchedAssemblies.Contains(filename))
|
||||
{
|
||||
using (MemoryStream mem = new MemoryStream())
|
||||
{
|
||||
string dirPath = Path.Combine(Paths.PluginPath, "DumpedAssemblies");
|
||||
|
||||
if (!Directory.Exists(dirPath))
|
||||
Directory.CreateDirectory(dirPath);
|
||||
|
||||
assembly.Write(mem);
|
||||
File.WriteAllBytes(Path.Combine(dirPath, filename), mem.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
Load(assembly);
|
||||
assembly.Dispose();
|
||||
}
|
||||
|
||||
// Apply assembly location resolver patch
|
||||
PatchedAssemblyResolver.ApplyPatch();
|
||||
|
||||
//run all finalizers
|
||||
FinalizePatching();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads an individual assembly defintion into the CLR.
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly to load.</param>
|
||||
public static void Load(AssemblyDefinition assembly)
|
||||
{
|
||||
using (MemoryStream assemblyStream = new MemoryStream())
|
||||
{
|
||||
assembly.Write(assemblyStream);
|
||||
Assembly.Load(assemblyStream.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static class PatchedAssemblyResolver
|
||||
{
|
||||
public static HarmonyInstance HarmonyInstance { get; } = HarmonyInstance.Create("com.bepis.bepinex.asmlocationfix");
|
||||
|
||||
public static Dictionary<string, string> AssemblyLocations { get; } = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
|
||||
|
||||
public static void ApplyPatch()
|
||||
{
|
||||
HarmonyWrapper.PatchAll(typeof(PatchedAssemblyResolver), HarmonyInstance);
|
||||
}
|
||||
|
||||
[HarmonyPostfix, HarmonyPatch(typeof(Assembly), nameof(Assembly.Location), MethodType.Getter)]
|
||||
public static void GetLocation(ref string __result, Assembly __instance)
|
||||
{
|
||||
if (AssemblyLocations.TryGetValue(__instance.FullName, out string location))
|
||||
__result = location;
|
||||
}
|
||||
|
||||
[HarmonyPostfix, HarmonyPatch(typeof(Assembly), nameof(Assembly.CodeBase), MethodType.Getter)]
|
||||
public static void GetCodeBase(ref string __result, Assembly __instance)
|
||||
{
|
||||
if (AssemblyLocations.TryGetValue(__instance.FullName, out string location))
|
||||
__result = $"file://{location.Replace('\\', '/')}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,12 +54,11 @@ namespace BepInEx.Bootstrap
|
||||
|
||||
UnityLogWriter unityLogWriter = new UnityLogWriter();
|
||||
|
||||
if (Preloader.PreloaderLog != null)
|
||||
unityLogWriter.WriteToLog($"{Preloader.PreloaderLog}\r\n");
|
||||
if (Logger.CurrentLogger != null && Logger.CurrentLogger is PreloaderLogWriter preloaderLogger)
|
||||
unityLogWriter.WriteToLog($"{preloaderLogger}\r\n");
|
||||
|
||||
Logger.SetLogger(unityLogWriter);
|
||||
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,293 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using BepInEx.Logging;
|
||||
using Mono.Cecil;
|
||||
using Mono.Cecil.Cil;
|
||||
using UnityInjector.ConsoleUtil;
|
||||
using MethodAttributes = Mono.Cecil.MethodAttributes;
|
||||
|
||||
namespace BepInEx.Bootstrap
|
||||
{
|
||||
/// <summary>
|
||||
/// The main entrypoint of BepInEx, and initializes all patchers and the chainloader.
|
||||
/// </summary>
|
||||
internal static class Preloader
|
||||
{
|
||||
/// <summary>
|
||||
/// The log writer that is specific to the preloader.
|
||||
/// </summary>
|
||||
public static PreloaderLogWriter PreloaderLog { get; private set; }
|
||||
|
||||
public static void Run()
|
||||
{
|
||||
try
|
||||
{
|
||||
AllocateConsole();
|
||||
|
||||
UnityPatches.Apply();
|
||||
|
||||
PreloaderLog =
|
||||
new PreloaderLogWriter(Utility.SafeParseBool(Config.GetEntry("preloader-logconsole", "false", "BepInEx")));
|
||||
PreloaderLog.Enabled = true;
|
||||
|
||||
string consoleTile =
|
||||
$"BepInEx {Assembly.GetExecutingAssembly().GetName().Version} - {Process.GetCurrentProcess().ProcessName}";
|
||||
ConsoleWindow.Title = consoleTile;
|
||||
|
||||
Logger.SetLogger(PreloaderLog);
|
||||
|
||||
PreloaderLog.WriteLine(consoleTile);
|
||||
|
||||
#if DEBUG
|
||||
object[] attributes = typeof(DebugInfoAttribute).Assembly.GetCustomAttributes(typeof(DebugInfoAttribute), false);
|
||||
|
||||
if (attributes.Length > 0)
|
||||
{
|
||||
var attribute = (DebugInfoAttribute)attributes[0];
|
||||
|
||||
PreloaderLog.WriteLine(attribute.Info);
|
||||
}
|
||||
#endif
|
||||
|
||||
Logger.Log(LogLevel.Message, "Preloader started");
|
||||
|
||||
string entrypointAssembly = Config.GetEntry("entrypoint-assembly", "UnityEngine.dll", "Preloader");
|
||||
|
||||
AssemblyPatcher.AddPatcher(new PatcherPlugin { TargetDLLs = new []{ entrypointAssembly }, Patcher = PatchEntrypoint});
|
||||
AssemblyPatcher.AddPatchersFromDirectory(Paths.PatcherPluginPath, GetPatcherMethods);
|
||||
|
||||
AssemblyPatcher.PatchAndLoad(Paths.ManagedPath);
|
||||
|
||||
AssemblyPatcher.DisposePatchers();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log(LogLevel.Fatal, "Could not run preloader!");
|
||||
Logger.Log(LogLevel.Fatal, ex);
|
||||
|
||||
PreloaderLog.Enabled = false;
|
||||
|
||||
try
|
||||
{
|
||||
if (!ConsoleWindow.IsAttatched)
|
||||
{
|
||||
//if we've already attached the console, then the log will already be written to the console
|
||||
AllocateConsole();
|
||||
Console.Write(PreloaderLog);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.WriteAllText(Path.Combine(Paths.GameRootPath, $"preloader_{DateTime.Now:yyyyMMdd_HHmmss_fff}.log"),
|
||||
PreloaderLog.ToString());
|
||||
|
||||
PreloaderLog.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scans the assembly for classes that use the patcher contract, and returns a list of valid patchers.
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly to scan.</param>
|
||||
/// <returns>A list of assembly patchers that were found in the assembly.</returns>
|
||||
public static List<PatcherPlugin> GetPatcherMethods(Assembly assembly)
|
||||
{
|
||||
var patcherMethods = new List<PatcherPlugin>();
|
||||
var flags = BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase;
|
||||
|
||||
foreach (var type in assembly.GetExportedTypes())
|
||||
try
|
||||
{
|
||||
if (type.IsInterface)
|
||||
continue;
|
||||
|
||||
var targetsProperty = type.GetProperty("TargetDLLs",
|
||||
flags,
|
||||
null,
|
||||
typeof(IEnumerable<string>),
|
||||
Type.EmptyTypes,
|
||||
null);
|
||||
|
||||
//first try get the ref patcher method
|
||||
var patcher = type.GetMethod("Patch",
|
||||
flags,
|
||||
null,
|
||||
CallingConventions.Any,
|
||||
new[] {typeof(AssemblyDefinition).MakeByRefType()},
|
||||
null);
|
||||
|
||||
if (patcher == null) //otherwise try getting the non-ref patcher method
|
||||
patcher = type.GetMethod("Patch",
|
||||
flags,
|
||||
null,
|
||||
CallingConventions.Any,
|
||||
new[] {typeof(AssemblyDefinition)},
|
||||
null);
|
||||
|
||||
if (targetsProperty == null || !targetsProperty.CanRead || patcher == null)
|
||||
continue;
|
||||
|
||||
var assemblyPatcher = new PatcherPlugin();
|
||||
|
||||
assemblyPatcher.Name = $"{assembly.GetName().Name}{type.FullName}";
|
||||
assemblyPatcher.Patcher = (ref AssemblyDefinition ass) =>
|
||||
{
|
||||
//we do the array fuckery here to get the ref result out
|
||||
object[] args = {ass};
|
||||
|
||||
patcher.Invoke(null, args);
|
||||
|
||||
ass = (AssemblyDefinition) args[0];
|
||||
};
|
||||
|
||||
assemblyPatcher.TargetDLLs = (IEnumerable<string>) targetsProperty.GetValue(null, null);
|
||||
|
||||
var initMethod = type.GetMethod("Initialize",
|
||||
flags,
|
||||
null,
|
||||
CallingConventions.Any,
|
||||
Type.EmptyTypes,
|
||||
null);
|
||||
|
||||
if (initMethod != null)
|
||||
assemblyPatcher.Initializer = () => initMethod.Invoke(null, null);
|
||||
|
||||
var finalizeMethod = type.GetMethod("Finish",
|
||||
flags,
|
||||
null,
|
||||
CallingConventions.Any,
|
||||
Type.EmptyTypes,
|
||||
null);
|
||||
|
||||
if (finalizeMethod != null)
|
||||
assemblyPatcher.Finalizer = () => finalizeMethod.Invoke(null, null);
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log(LogLevel.Warning, $"Could not load patcher methods from {assembly.GetName().Name}");
|
||||
Logger.Log(LogLevel.Warning, $"{ex}");
|
||||
}
|
||||
|
||||
Logger.Log(LogLevel.Info,
|
||||
$"Loaded {patcherMethods.Count} patcher methods from {assembly.GetName().Name}");
|
||||
|
||||
return patcherMethods;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inserts BepInEx's own chainloader entrypoint into UnityEngine.
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly that will be attempted to be patched.</param>
|
||||
public static void PatchEntrypoint(ref AssemblyDefinition assembly)
|
||||
{
|
||||
if (assembly.MainModule.AssemblyReferences.Any(x => x.Name.Contains("BepInEx")))
|
||||
{
|
||||
throw new Exception("BepInEx has been detected to be patched! Please unpatch before using a patchless variant!");
|
||||
}
|
||||
|
||||
string entrypointType = Config.GetEntry("entrypoint-type", "Application", "Preloader");
|
||||
string entrypointMethod = Config.GetEntry("entrypoint-method", ".cctor", "Preloader");
|
||||
|
||||
bool isCctor = entrypointMethod.IsNullOrWhiteSpace() || entrypointMethod == ".cctor";
|
||||
|
||||
|
||||
var entryType = assembly.MainModule.Types.FirstOrDefault(x => x.Name == entrypointType);
|
||||
|
||||
if (entryType == null)
|
||||
{
|
||||
throw new Exception("The entrypoint type is invalid! Please check your config.ini");
|
||||
}
|
||||
|
||||
using (var injected = AssemblyDefinition.ReadAssembly(Paths.BepInExAssemblyPath))
|
||||
{
|
||||
var originalInitMethod = injected.MainModule.Types.First(x => x.Name == "Chainloader").Methods
|
||||
.First(x => x.Name == "Initialize");
|
||||
|
||||
var originalStartMethod = injected.MainModule.Types.First(x => x.Name == "Chainloader").Methods
|
||||
.First(x => x.Name == "Start");
|
||||
|
||||
var initMethod = assembly.MainModule.ImportReference(originalInitMethod);
|
||||
var startMethod = assembly.MainModule.ImportReference(originalStartMethod);
|
||||
|
||||
List<MethodDefinition> methods = new List<MethodDefinition>();
|
||||
|
||||
if (isCctor)
|
||||
{
|
||||
MethodDefinition cctor = entryType.Methods.FirstOrDefault(m => m.IsConstructor && m.IsStatic);
|
||||
|
||||
if (cctor == null)
|
||||
{
|
||||
cctor = new MethodDefinition(".cctor",
|
||||
MethodAttributes.Static | MethodAttributes.Private | MethodAttributes.HideBySig
|
||||
| MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
|
||||
assembly.MainModule.ImportReference(typeof(void)));
|
||||
|
||||
entryType.Methods.Add(cctor);
|
||||
ILProcessor il = cctor.Body.GetILProcessor();
|
||||
il.Append(il.Create(OpCodes.Ret));
|
||||
}
|
||||
|
||||
methods.Add(cctor);
|
||||
}
|
||||
else
|
||||
{
|
||||
methods.AddRange(entryType.Methods.Where(x => x.Name == entrypointMethod));
|
||||
}
|
||||
|
||||
if (!methods.Any())
|
||||
{
|
||||
throw new Exception("The entrypoint method is invalid! Please check your config.ini");
|
||||
}
|
||||
|
||||
foreach (var method in methods)
|
||||
{
|
||||
var il = method.Body.GetILProcessor();
|
||||
|
||||
Instruction ins = il.Body.Instructions.First();
|
||||
|
||||
il.InsertBefore(ins, il.Create(OpCodes.Ldstr, Paths.ExecutablePath)); //containerExePath
|
||||
il.InsertBefore(ins, il.Create(OpCodes.Ldc_I4_0)); //startConsole (always false, we already load the console in Preloader)
|
||||
il.InsertBefore(ins, il.Create(OpCodes.Call, initMethod)); //Chainloader.Initialize(string containerExePath, bool startConsole = true)
|
||||
il.InsertBefore(ins, il.Create(OpCodes.Call, startMethod));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocates a console window for use by BepInEx safely.
|
||||
/// </summary>
|
||||
public static void AllocateConsole()
|
||||
{
|
||||
bool console = Utility.SafeParseBool(Config.GetEntry("console", "false", "BepInEx"));
|
||||
bool shiftjis = Utility.SafeParseBool(Config.GetEntry("console-shiftjis", "false", "BepInEx"));
|
||||
|
||||
if (!console)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
ConsoleWindow.Attach();
|
||||
|
||||
var encoding = (uint) Encoding.UTF8.CodePage;
|
||||
|
||||
if (shiftjis)
|
||||
encoding = 932;
|
||||
|
||||
ConsoleEncoding.ConsoleCodePage = encoding;
|
||||
Console.OutputEncoding = ConsoleEncoding.GetEncoding(encoding);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log(LogLevel.Error, "Failed to allocate console!");
|
||||
Logger.Log(LogLevel.Error, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using System;
|
||||
using BepInEx.Harmony;
|
||||
using Harmony;
|
||||
|
||||
namespace BepInEx.Bootstrap
|
||||
{
|
||||
internal static class UnityPatches
|
||||
{
|
||||
public static HarmonyInstance HarmonyInstance { get; } = HarmonyInstance.Create("com.bepinex.unitypatches");
|
||||
|
||||
public static void Apply()
|
||||
{
|
||||
HarmonyWrapper.PatchAll(typeof(UnityPatches), HarmonyInstance);
|
||||
}
|
||||
|
||||
#if UNITY_2018
|
||||
/*
|
||||
* DESC: Workaround for Trace class not working because of missing .config file
|
||||
* AFFECTS: Unity 2018+
|
||||
*/
|
||||
[HarmonyPostfix, HarmonyPatch(typeof(AppDomain), nameof(AppDomain.SetupInformation), MethodType.Getter)]
|
||||
public static void GetExeConfigName(AppDomainSetup __result)
|
||||
{
|
||||
__result.ApplicationBase = $"file://{Paths.GameRootPath}";
|
||||
__result.ConfigurationFile = "app.config";
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ namespace UnityInjector.ConsoleUtil
|
||||
// http://jonskeet.uk/csharp/ebcdic/
|
||||
// using only safe (managed) code
|
||||
// --------------------------------------------------
|
||||
internal partial class ConsoleEncoding : Encoding
|
||||
public partial class ConsoleEncoding : Encoding
|
||||
{
|
||||
private readonly uint _codePage;
|
||||
public override int CodePage => (int) _codePage;
|
||||
|
||||
@@ -10,7 +10,7 @@ using System.Text;
|
||||
|
||||
namespace UnityInjector.ConsoleUtil
|
||||
{
|
||||
internal class ConsoleWindow
|
||||
public class ConsoleWindow
|
||||
{
|
||||
public static bool IsAttatched { get; private set; }
|
||||
private static IntPtr _cOut;
|
||||
|
||||
@@ -62,7 +62,7 @@ namespace BepInEx.Logging
|
||||
if (IsRedirectingConsole)
|
||||
Console.SetOut(this);
|
||||
else
|
||||
Console.SetOut(TextWriter.Null);
|
||||
Console.SetOut(Null);
|
||||
|
||||
Trace.Listeners.Add(traceListener);
|
||||
|
||||
@@ -76,7 +76,7 @@ namespace BepInEx.Logging
|
||||
{
|
||||
if (!_enabled)
|
||||
return;
|
||||
|
||||
|
||||
Console.SetOut(stdout);
|
||||
|
||||
Trace.Listeners.Remove(traceListener);
|
||||
|
||||
@@ -11,10 +11,8 @@ namespace BepInEx.Logging
|
||||
/// Logs entries using Unity specific outputs.
|
||||
/// </summary>
|
||||
public class UnityLogWriter : BaseLogger
|
||||
{
|
||||
private delegate void WriteStringToUnityLogDelegate(string s);
|
||||
|
||||
private static readonly WriteStringToUnityLogDelegate WriteStringToUnityLog;
|
||||
{
|
||||
private static readonly Action<string> WriteStringToUnityLog;
|
||||
|
||||
[DllImport("mono.dll", EntryPoint = "mono_lookup_internal_call")]
|
||||
private static extern IntPtr MonoLookupInternalCall(IntPtr gconstpointer);
|
||||
@@ -26,7 +24,7 @@ namespace BepInEx.Logging
|
||||
if (MonoLookupInternalCall(methodInfo.MethodHandle.Value) == IntPtr.Zero)
|
||||
continue;
|
||||
|
||||
WriteStringToUnityLog = (WriteStringToUnityLogDelegate) Delegate.CreateDelegate(typeof(WriteStringToUnityLogDelegate), methodInfo);
|
||||
WriteStringToUnityLog = (Action<string>) Delegate.CreateDelegate(typeof(Action<string>), methodInfo);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user