mirror of
https://github.com/BepInEx/BepInEx.git
synced 2026-10-20 01:46:13 +00:00
400 lines
17 KiB
C#
400 lines
17 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.IO.Compression;
|
|
using System.Net.Http;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using AssemblyUnhollower;
|
|
using BepInEx.Configuration;
|
|
using BepInEx.Logging;
|
|
using Cpp2IL.Core;
|
|
using HarmonyLib;
|
|
using Il2CppDumper;
|
|
using LibCpp2IL;
|
|
using Mono.Cecil;
|
|
using UnhollowerBaseLib;
|
|
using Logger = BepInEx.Logging.Logger;
|
|
|
|
namespace BepInEx.IL2CPP;
|
|
|
|
internal static class ProxyAssemblyGenerator
|
|
{
|
|
private static readonly ConfigEntry<bool> ConfigUpdateUnhollowedAssemblies =
|
|
ConfigFile.CoreConfig.Bind("IL2CPP",
|
|
"UpdateUnhollowedAssemblies",
|
|
true,
|
|
new StringBuilder()
|
|
.AppendLine("Whether to run Il2CppAssemblyUnhollower automatically to generate Il2Cpp support assemblies when they are outdated.")
|
|
.AppendLine("If disabled assemblies in `BepInEx/unhollowed` won't be updated between game or BepInEx updates!")
|
|
.ToString());
|
|
|
|
private static readonly ConfigEntry<string> ConfigUnityBaseLibrariesSource = ConfigFile.CoreConfig.Bind(
|
|
"IL2CPP", "UnityBaseLibrariesSource",
|
|
"http://unity.bepinex.dev/libraries/{VERSION}.zip",
|
|
new StringBuilder()
|
|
.AppendLine("URL to the ZIP of managed Unity base libraries.")
|
|
.AppendLine("The base libraries are used by Il2CppUnhollower to generate unhollowed Unity assemblies")
|
|
.AppendLine("The URL template MUST use HTTP.")
|
|
.AppendLine("The URL can include {VERSION} template which will be replaced with the game's Unity engine version")
|
|
.ToString());
|
|
|
|
private static readonly ConfigEntry<IL2CPPDumperType> ConfigIl2CppDumperType = ConfigFile.CoreConfig.Bind(
|
|
"IL2CPP", "Il2CppDumperType",
|
|
IL2CPPDumperType.Cpp2IL,
|
|
new StringBuilder()
|
|
.AppendLine("The IL2CPP metadata dumper tool to use when generating dummy assemblies for Il2CppAssemblyUnhollower.")
|
|
.AppendLine("Il2CppDumper - Default. The traditional choice that has been used by BepInEx.")
|
|
.AppendLine("Cpp2IL - Experimental, may provide better results than Il2CppDumper. Required for use with BepInEx.MelonLoader.Loader.")
|
|
.ToString());
|
|
|
|
private static readonly ConfigEntry<bool> ConfigDumpDummyAssemblies = ConfigFile.CoreConfig.Bind(
|
|
"IL2CPP", "DumpDummyAssemblies",
|
|
false,
|
|
"If enabled, BepInEx will save dummy assemblies generated by an il2cpp dumper into BepInEx/dummy.");
|
|
|
|
private static readonly ConfigEntry<string> UnhollowedAssembliesPath = ConfigFile.CoreConfig.Bind(
|
|
"IL2CPP", "UnhollowedAssembliesPath",
|
|
"{BepInEx}",
|
|
new StringBuilder()
|
|
.AppendLine("The path to the folder where unhollowed assemblies are stored.")
|
|
.AppendLine("Supports the following placeholders:")
|
|
.AppendLine("{BepInEx} - Path to the BepInEx folder.")
|
|
.AppendLine("{ProcessName} - Name of the current process")
|
|
.ToString());
|
|
|
|
private static ManualLogSource Il2cppDumperLogger;
|
|
|
|
private static string unhollowedBasePath;
|
|
|
|
public static string GameAssemblyPath => Path.Combine(Paths.GameRootPath, "GameAssembly.dll");
|
|
|
|
private static string HashPath => Path.Combine(Preloader.IL2CPPUnhollowedPath, "assembly-hash.txt");
|
|
|
|
private static string UnhollowedBasePath
|
|
{
|
|
get
|
|
{
|
|
if (unhollowedBasePath != null)
|
|
return unhollowedBasePath;
|
|
var path = Utility.GetCommandLineArgValue("--unhollowed-path") ?? UnhollowedAssembliesPath.Value;
|
|
unhollowedBasePath = path.Replace("{BepInEx}", Paths.BepInExRootPath)
|
|
.Replace("{ProcessName}", Paths.ProcessName);
|
|
return unhollowedBasePath;
|
|
}
|
|
}
|
|
|
|
private static string UnityBaseLibsDirectory => Path.Combine(UnhollowedBasePath, "unity-libs");
|
|
|
|
internal static string IL2CPPUnhollowedPath => Path.Combine(UnhollowedBasePath, "unhollowed");
|
|
|
|
private static string ComputeHash()
|
|
{
|
|
using var md5 = MD5.Create();
|
|
|
|
static void HashFile(ICryptoTransform hash, string file)
|
|
{
|
|
const int defaultCopyBufferSize = 81920;
|
|
using var fs = File.OpenRead(file);
|
|
var buffer = new byte[defaultCopyBufferSize];
|
|
int read;
|
|
while ((read = fs.Read(buffer)) > 0)
|
|
hash.TransformBlock(buffer, 0, read, buffer, 0);
|
|
}
|
|
|
|
static void HashString(ICryptoTransform hash, string str)
|
|
{
|
|
var buffer = Encoding.UTF8.GetBytes(str);
|
|
hash.TransformBlock(buffer, 0, buffer.Length, buffer, 0);
|
|
}
|
|
|
|
HashFile(md5, GameAssemblyPath);
|
|
|
|
if (Directory.Exists(UnityBaseLibsDirectory))
|
|
foreach (var file in Directory.EnumerateFiles(UnityBaseLibsDirectory, "*.dll",
|
|
SearchOption.TopDirectoryOnly))
|
|
{
|
|
HashString(md5, Path.GetFileName(file));
|
|
HashFile(md5, file);
|
|
}
|
|
|
|
// Hash some common dependencies as they can affect output
|
|
HashString(md5, typeof(UnhollowedAssemblyGenerator).Assembly.GetName().Version.ToString());
|
|
HashString(md5, typeof(Cpp2IlApi).Assembly.GetName().Version.ToString());
|
|
HashString(md5, typeof(Il2CppDumper.Il2CppDumper).Assembly.GetName().Version.ToString());
|
|
|
|
md5.TransformFinalBlock(new byte[0], 0, 0);
|
|
|
|
return Utility.ByteArrayToString(md5.Hash);
|
|
}
|
|
|
|
public static bool CheckIfGenerationRequired()
|
|
{
|
|
static bool NeedGenerationOrSkip()
|
|
{
|
|
if (!ConfigUpdateUnhollowedAssemblies.Value)
|
|
{
|
|
var hash = ComputeHash();
|
|
Preloader.Log
|
|
.Log(LogLevel.Warning,
|
|
$"Unhollowed assemblies are possibly out of date. To disable this message, create file {HashPath} with the following contents: {hash}");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
if (!Directory.Exists(Preloader.IL2CPPUnhollowedPath))
|
|
return true;
|
|
|
|
if (!File.Exists(HashPath))
|
|
return NeedGenerationOrSkip();
|
|
|
|
if (ComputeHash() != File.ReadAllText(HashPath) && NeedGenerationOrSkip())
|
|
{
|
|
Preloader.Log.Log(LogLevel.Info, "Detected outdated proxy assemblies, will regenerate them now");
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public static void GenerateAssemblies()
|
|
{
|
|
Il2cppDumperLogger ??= Logger.CreateLogSource(ConfigIl2CppDumperType.Value == IL2CPPDumperType.Il2CppDumper
|
|
? "Il2CppDumper"
|
|
: "Cpp2IL");
|
|
|
|
var domain = AppDomainHelper.CreateDomain("GeneratorDomain", new AppDomainHelper.AppDomainSetup
|
|
{
|
|
ApplicationBase = Paths.BepInExAssemblyDirectory
|
|
});
|
|
|
|
var runner =
|
|
(AppDomainRunner) AppDomainHelper.CreateInstanceAndUnwrap(domain,
|
|
typeof(AppDomainRunner).Assembly.FullName,
|
|
typeof(AppDomainRunner).FullName);
|
|
|
|
runner.Setup(Paths.ExecutablePath, Paths.BepInExRootPath, Paths.ManagedPath);
|
|
var ok = runner.GenerateAssemblies(new AppDomainListener(), Preloader.UnityVersion.ToString(3),
|
|
ConfigIl2CppDumperType.Value);
|
|
|
|
AppDomain.Unload(domain);
|
|
|
|
if (ok)
|
|
File.WriteAllText(HashPath, ComputeHash());
|
|
}
|
|
|
|
internal enum IL2CPPDumperType
|
|
{
|
|
Il2CppDumper,
|
|
Cpp2IL
|
|
}
|
|
|
|
private static class AppDomainHelper
|
|
{
|
|
static AppDomainHelper()
|
|
{
|
|
var appDomain = typeof(AppDomain);
|
|
var evidenceType = typeof(AppDomain).Assembly.GetType("System.Security.Policy.Evidence");
|
|
AppDomainSetupType = typeof(AppDomain).Assembly.GetType("System.AppDomainSetup");
|
|
CreateDomainInternal =
|
|
MethodInvoker.GetHandler(AccessTools.Method(appDomain, "CreateDomain",
|
|
new[]
|
|
{
|
|
typeof(string), evidenceType, AppDomainSetupType
|
|
}));
|
|
CreateInstanceAndUnwrap =
|
|
AccessTools.MethodDelegate<Func<AppDomain, string, string, object>>(AccessTools.Method(appDomain,
|
|
nameof(CreateInstanceAndUnwrap), new[] { typeof(string), typeof(string) }));
|
|
}
|
|
|
|
public static Func<AppDomain, string, string, object> CreateInstanceAndUnwrap { get; }
|
|
private static FastInvokeHandler CreateDomainInternal { get; }
|
|
private static Type AppDomainSetupType { get; }
|
|
|
|
public static AppDomain CreateDomain(string name, AppDomainSetup setup)
|
|
{
|
|
var realSetup = AccessTools.CreateInstance(AppDomainSetupType);
|
|
Traverse.IterateProperties(setup, realSetup, (pSrc, pTgt) => pTgt.SetValue(pSrc.GetValue()));
|
|
return CreateDomainInternal(null, name, null, realSetup) as AppDomain;
|
|
}
|
|
|
|
public class AppDomainSetup
|
|
{
|
|
public string ApplicationBase { get; set; }
|
|
}
|
|
}
|
|
|
|
[Serializable]
|
|
private class AppDomainListener : MarshalByRefObject
|
|
{
|
|
public void DoPreloaderLog(object data, LogLevel level) => Preloader.Log.Log(level, data);
|
|
|
|
public void DoDumperLog(object data, LogLevel level) => Il2cppDumperLogger.Log(level, data);
|
|
|
|
public void DoUnhollowerLog(object data, LogLevel level) => Preloader.UnhollowerLog.Log(level, data);
|
|
}
|
|
|
|
[Serializable]
|
|
private class AppDomainRunner : MarshalByRefObject
|
|
{
|
|
public void Setup(string executablePath, string bepinPath, string managedPath)
|
|
{
|
|
Paths.SetExecutablePath(executablePath, bepinPath, managedPath);
|
|
AppDomain.CurrentDomain.AddCecilPlatformAssemblies(Paths.ManagedPath);
|
|
AppDomain.CurrentDomain.AddCecilPlatformAssemblies(UnityBaseLibsDirectory);
|
|
}
|
|
|
|
public bool GenerateAssemblies(AppDomainListener listener,
|
|
string unityVersion,
|
|
IL2CPPDumperType dumperType)
|
|
{
|
|
try
|
|
{
|
|
GenerateAssembliesInternal(listener, unityVersion, dumperType);
|
|
return true;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
listener.DoUnhollowerLog($"Failed to generate unhollowed assemblies: {e}", LogLevel.Fatal);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private void GenerateAssembliesInternal(AppDomainListener listener,
|
|
string unityVersion,
|
|
IL2CPPDumperType dumperType)
|
|
{
|
|
var source =
|
|
ConfigUnityBaseLibrariesSource.Value.Replace("{VERSION}", unityVersion);
|
|
|
|
if (!string.IsNullOrEmpty(source))
|
|
{
|
|
listener.DoPreloaderLog("Downloading unity base libraries", LogLevel.Message);
|
|
|
|
Directory.CreateDirectory(UnityBaseLibsDirectory);
|
|
Directory.EnumerateFiles(UnityBaseLibsDirectory, "*.dll").Do(File.Delete);
|
|
|
|
using var httpClient = new HttpClient();
|
|
using var zipStream = httpClient.GetStreamAsync(source).GetAwaiter().GetResult();
|
|
using var zipArchive = new ZipArchive(zipStream, ZipArchiveMode.Read);
|
|
|
|
listener.DoPreloaderLog("Extracting downloaded unity base libraries", LogLevel.Message);
|
|
zipArchive.ExtractToDirectory(UnityBaseLibsDirectory);
|
|
}
|
|
|
|
listener.DoPreloaderLog("Generating Il2CppUnhollower assemblies", LogLevel.Message);
|
|
|
|
Directory.CreateDirectory(Preloader.IL2CPPUnhollowedPath);
|
|
Directory.EnumerateFiles(Preloader.IL2CPPUnhollowedPath, "*.dll").Do(File.Delete);
|
|
|
|
var metadataPath = Path.Combine(Paths.GameRootPath,
|
|
$"{Paths.ProcessName}_Data",
|
|
"il2cpp_data",
|
|
"Metadata",
|
|
"global-metadata.dat");
|
|
|
|
List<AssemblyDefinition> sourceAssemblies;
|
|
|
|
var stopwatch = new Stopwatch();
|
|
stopwatch.Start();
|
|
|
|
if (dumperType == IL2CPPDumperType.Il2CppDumper)
|
|
{
|
|
listener.DoPreloaderLog("Generating Il2CppDumper intermediate assemblies", LogLevel.Info);
|
|
|
|
Il2CppDumper.Il2CppDumper.Init(GameAssemblyPath,
|
|
metadataPath,
|
|
new Config
|
|
{
|
|
GenerateStruct = false,
|
|
GenerateDummyDll = true
|
|
},
|
|
s => listener.DoDumperLog(s, LogLevel.Debug),
|
|
out var metadata,
|
|
out var il2Cpp);
|
|
|
|
var executor = new Il2CppExecutor(metadata, il2Cpp);
|
|
var dummy = new DummyAssemblyGenerator(executor, true);
|
|
sourceAssemblies = dummy.Assemblies;
|
|
}
|
|
else // if (dumperType == IL2CPPDumperType.Cpp2IL)
|
|
{
|
|
Cpp2IL.Core.Logger.VerboseLog += (message, s) =>
|
|
listener.DoDumperLog($"[{s}] {message.Trim()}", LogLevel.Debug);
|
|
Cpp2IL.Core.Logger.InfoLog += (message, s) =>
|
|
listener.DoDumperLog($"[{s}] {message.Trim()}", LogLevel.Info);
|
|
Cpp2IL.Core.Logger.WarningLog += (message, s) =>
|
|
listener.DoDumperLog($"[{s}] {message.Trim()}", LogLevel.Warning);
|
|
Cpp2IL.Core.Logger.ErrorLog += (message, s) =>
|
|
listener.DoDumperLog($"[{s}] {message.Trim()}", LogLevel.Error);
|
|
|
|
var cpp2IlUnityVersion =
|
|
Cpp2IlApi.DetermineUnityVersion(Paths.ExecutablePath,
|
|
Path.Combine(Paths.GameRootPath, $"{Paths.ProcessName}_Data"));
|
|
|
|
Cpp2IlApi.InitializeLibCpp2Il(GameAssemblyPath, metadataPath, cpp2IlUnityVersion, false);
|
|
|
|
sourceAssemblies = Cpp2IlApi.MakeDummyDLLs();
|
|
|
|
Cpp2IlApi.RunAttributeRestorationForAllAssemblies(null,
|
|
parallel: LibCpp2IlMain.MetadataVersion >= 29 ||
|
|
LibCpp2IlMain.Binary!.InstructionSet is
|
|
InstructionSet.X86_32
|
|
or InstructionSet.X86_64);
|
|
|
|
Cpp2IlApi.DisposeAndCleanupAll();
|
|
}
|
|
|
|
stopwatch.Stop();
|
|
listener.DoDumperLog("Total time: " + stopwatch.Elapsed, LogLevel.Info);
|
|
|
|
if (ConfigDumpDummyAssemblies.Value)
|
|
{
|
|
var dummyPath = Path.Combine(Paths.BepInExRootPath, "dummy");
|
|
Directory.CreateDirectory(dummyPath);
|
|
|
|
foreach (var assemblyDefinition in sourceAssemblies)
|
|
assemblyDefinition.Write(Path.Combine(dummyPath, assemblyDefinition.Name.Name + ".dll"));
|
|
}
|
|
|
|
var unhollowerOptions = new UnhollowerOptions
|
|
{
|
|
GameAssemblyPath = GameAssemblyPath,
|
|
MscorlibPath = Path.Combine(Paths.ManagedPath, "mscorlib.dll"),
|
|
Source = sourceAssemblies,
|
|
OutputDir = Preloader.IL2CPPUnhollowedPath,
|
|
UnityBaseLibsDir = Directory.Exists(UnityBaseLibsDirectory) ? UnityBaseLibsDirectory : null,
|
|
NoCopyUnhollowerLibs = true
|
|
};
|
|
|
|
var renameMapLocation = Path.Combine(Paths.BepInExRootPath, "DeobfuscationMap.csv.gz");
|
|
if (File.Exists(renameMapLocation))
|
|
{
|
|
listener.DoPreloaderLog("Parsing deobfuscation rename mappings", LogLevel.Info);
|
|
unhollowerOptions.ReadRenameMap(renameMapLocation);
|
|
}
|
|
|
|
listener.DoPreloaderLog("Executing Il2CppUnhollower generator", LogLevel.Info);
|
|
|
|
LogSupport.InfoHandler += s => listener.DoUnhollowerLog(s.Trim(), LogLevel.Info);
|
|
LogSupport.WarningHandler += s => listener.DoUnhollowerLog(s.Trim(), LogLevel.Warning);
|
|
LogSupport.TraceHandler += s => listener.DoUnhollowerLog(s.Trim(), LogLevel.Debug);
|
|
LogSupport.ErrorHandler += s => listener.DoUnhollowerLog(s.Trim(), LogLevel.Error);
|
|
|
|
try
|
|
{
|
|
UnhollowedAssemblyGenerator.GenerateUnhollowedAssemblies(unhollowerOptions);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
listener.DoUnhollowerLog($"Exception while unhollowing: {e}", LogLevel.Error);
|
|
}
|
|
|
|
sourceAssemblies.Do(x => x.Dispose());
|
|
}
|
|
}
|
|
}
|