Files
BepInEx/BepInEx.IL2CPP/ProxyAssemblyGenerator.cs
2021-12-04 22:52:08 +02:00

303 lines
14 KiB
C#

using System;
using System.Collections.Generic;
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 Mono.Cecil;
using UnhollowerBaseLib;
using AssemblyUnhollowerRunner = AssemblyUnhollower.Program;
using Logger = BepInEx.Logging.Logger;
namespace BepInEx.IL2CPP
{
internal static class ProxyAssemblyGenerator
{
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.Il2CppDumper,
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 ManualLogSource Il2cppDumperLogger = null;
public static string GameAssemblyPath => Path.Combine(Paths.GameRootPath, "GameAssembly.dll");
private static string HashPath => Path.Combine(Preloader.IL2CPPUnhollowedPath, "assembly-hash.txt");
private static string UnityBaseLibsDirectory => Path.Combine(Paths.BepInExRootPath, "unity-libs");
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(AssemblyUnhollowerRunner).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()
{
if (!Directory.Exists(Preloader.IL2CPPUnhollowedPath))
return true;
if (!File.Exists(HashPath))
return true;
if (ComputeHash() != File.ReadAllText(HashPath))
{
Preloader.Log.LogInfo("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 runner = new AppDomainRunner();
runner.Setup(Paths.ExecutablePath, Preloader.IL2CPPUnhollowedPath, Paths.BepInExRootPath,
Paths.ManagedPath);
runner.GenerateAssembliesInternal(new AppDomainListener(), Preloader.UnityVersion.ToString(3),
ConfigIl2CppDumperType.Value);
File.WriteAllText(HashPath, ComputeHash());
}
internal enum IL2CPPDumperType
{
Il2CppDumper,
Cpp2IL
}
private class AppDomainListener
{
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);
}
private class AppDomainRunner
{
public void Setup(string executablePath, string unhollowedPath, string bepinPath, string managedPath)
{
Paths.SetExecutablePath(executablePath, bepinPath, managedPath);
Preloader.IL2CPPUnhollowedPath = unhollowedPath;
AppDomain.CurrentDomain.AddCecilPlatformAssemblies(Paths.ManagedPath);
AppDomain.CurrentDomain.AddCecilPlatformAssemblies(Path.GetDirectoryName(Paths.ManagedPath));
AppDomain.CurrentDomain.AddCecilPlatformAssemblies(UnityBaseLibsDirectory);
}
public 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);
string metadataPath = Path.Combine(Paths.GameRootPath,
$"{Paths.ProcessName}_Data",
"il2cpp_data",
"Metadata",
"global-metadata.dat");
List<AssemblyDefinition> sourceAssemblies;
var stopwatch = new System.Diagnostics.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();
}
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.BepInExRootPath, "corlib-libs", "mscorlib.dll"),
Source = sourceAssemblies,
OutputDir = Preloader.IL2CPPUnhollowedPath,
UnityBaseLibsDir = Directory.Exists(UnityBaseLibsDirectory) ? UnityBaseLibsDirectory : null,
NoCopyUnhollowerLibs = true
};
string renameMapLocation = Path.Combine(Paths.BepInExRootPath, "DeobfuscationMap.csv.gz");
if (File.Exists(renameMapLocation))
{
listener.DoPreloaderLog("Parsing deobfuscation rename mappings", LogLevel.Info);
using var fileStream =
new FileStream(renameMapLocation, FileMode.Open, FileAccess.Read, FileShare.Read);
using var gzipStream = new GZipStream(fileStream, CompressionMode.Decompress);
using var reader = new StreamReader(gzipStream, Encoding.UTF8, false);
while (!reader.EndOfStream)
{
var line = reader.ReadLine();
if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#"))
continue;
var mapping = line.Split(';');
if (mapping.Length != 2)
continue;
unhollowerOptions.RenameMap[mapping[0]] = mapping[1];
}
}
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
{
AssemblyUnhollowerRunner.Main(unhollowerOptions);
}
catch (Exception e)
{
listener.DoUnhollowerLog($"Exception while unhollowing: {e}", LogLevel.Error);
}
sourceAssemblies.Do(x => x.Dispose());
}
}
}
}