Skip to content
660 changes: 660 additions & 0 deletions Il2CppInterop.Common/AssemblyIATHooker.cs

Large diffs are not rendered by default.

11 changes: 6 additions & 5 deletions Il2CppInterop.Runtime/IL2CPP.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Text.RegularExpressions;
using Il2CppInterop.Common;
using Il2CppInterop.Common.Attributes;
using Il2CppInterop.Runtime.Injection;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is an unnecessary using.

using Il2CppInterop.Runtime.InteropTypes;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using Il2CppInterop.Runtime.Runtime;
Expand All @@ -18,7 +19,7 @@ namespace Il2CppInterop.Runtime;

public static unsafe class IL2CPP
{
private static readonly Dictionary<string, IntPtr> ourImagesMap = new();
private static readonly Dictionary<string, IntPtr> il2CppImagesMap = new();

static IL2CPP()
{
Expand All @@ -35,24 +36,24 @@ static IL2CPP()
{
var image = il2cpp_assembly_get_image(assemblies[i]);
var name = Marshal.PtrToStringAnsi(il2cpp_image_get_name(image));
ourImagesMap[name] = image;
il2CppImagesMap[name] = image;
}
}

internal static IntPtr GetIl2CppImage(string name)
{
if (ourImagesMap.ContainsKey(name)) return ourImagesMap[name];
if (il2CppImagesMap.ContainsKey(name)) return il2CppImagesMap[name];
return IntPtr.Zero;
}

internal static IntPtr[] GetIl2CppImages()
{
return ourImagesMap.Values.ToArray();
return il2CppImagesMap.Values.ToArray();
}

public static IntPtr GetIl2CppClass(string assemblyName, string namespaze, string className)
{
if (!ourImagesMap.TryGetValue(assemblyName, out var image))
if (!il2CppImagesMap.TryGetValue(assemblyName, out var image))
{
Logger.Instance.LogError("Assembly {AssemblyName} is not registered in il2cpp", assemblyName);
return IntPtr.Zero;
Expand Down
53 changes: 53 additions & 0 deletions Il2CppInterop.Runtime/Injection/AssemblyInjectorComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using Il2CppInterop.Common.Host;
using Il2CppInterop.Runtime.Injection;

namespace Il2CppInterop.Runtime
{
internal static class AssemblyListComponentExtensions
{
public static T AddAssemblyInjector<T, TProvider>(this T host)
where T : BaseHost
where TProvider : IAssemblyListProvider, new()
{
host.AddComponent(new AssemblyInjectorComponent(new TProvider()));
return host;
}
}

public interface IAssemblyListProvider
{
public IEnumerable<string> GetAssemblyList();
}

public class AssemblyInjectorComponent : IHostComponent
{
private static IAssemblyListProvider s_assemblyListProvider;

public AssemblyInjectorComponent(IAssemblyListProvider assemblyListProvider) => s_assemblyListProvider = assemblyListProvider;

public static IEnumerable<string> ModAssemblies
{
get
{
if (s_assemblyListProvider == null)
{
throw new InvalidOperationException("Mod Assembly Injector is not initialized! Initialize the host before using Mod Assembly Injector!");
}

return s_assemblyListProvider.GetAssemblyList();
}
}

public void Dispose()
{
s_assemblyListProvider = null;
}

public void Start()
{
InjectorHelpers.EarlySetup();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;

namespace Il2CppInterop.Runtime.Injection
{
internal class GameManagersAssemblyListFile : IAssemblyListFile
{
private List<string> _assemblies = new List<string>();
private string originalFile;
private string newFile;

public bool IsTargetFile(string originalFilePath)
{
return originalFilePath.Contains("globalgamemanagers");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

globalgamemanagers doesn't always exist.

}

public void Setup(string originalFilePath)
{
if (originalFile != null) return;
originalFile = originalFilePath;
}

public void AddAssembly(string name)
{
_assemblies.Add(name);
}

public string GetOrCreateNewFile()
{
if (newFile != null) return newFile;

newFile = Path.GetTempFileName();
CreateModifiedFile();
return newFile;
}

private void CreateModifiedFile()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am strongly against a solution that requires modifying game files.

Copy link
Contributor Author

@limoka limoka Feb 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if we don't actually touch original files? This PR uses hooks at runtime to provide files when the game requests them. Nothing is modified on disk.

Also sadly the only other solution would be patching native Unity code (not lib il2cpp), and according to author of FixPluginSerialization (mono version of this kind of solution), it's an awful idea: https://discord.com/channels/623153565053222947/985596870229385256/1113056578485092382

{
using var outputStream = File.Open(newFile, FileMode.Create);
using var outputWriter = new BinaryWriter(outputStream, Encoding.ASCII, false);
using var inputStream = File.Open(originalFile, FileMode.Open);
using var inputReader = new BinaryReader(inputStream, Encoding.ASCII, false);

// Assembly list always starts with UnityEngine.dll
var startPos = SeekFirstName(inputStream, inputReader);
if (startPos == -1)
{
throw new Exception("Failed to find start of assembly list in globalgamemanagers file!");
}

inputStream.Position = 0;
startPos -= 8;

for (var i = 0; i < startPos; i++)
{
outputWriter.Write(inputReader.ReadByte());
}

var assemblyCount = inputReader.ReadInt32();
List<string> newAssemblyList = new List<string>(assemblyCount + _assemblies.Count);
List<int> newAssemblyTypes = new List<int>(assemblyCount + _assemblies.Count);
for (var i = 0; i < assemblyCount; i++)
{
newAssemblyList.Add(ReadString(inputReader));
}

assemblyCount = inputReader.ReadInt32();
for (var i = 0; i < assemblyCount; i++)
{
newAssemblyTypes.Add(inputReader.ReadInt32());
}

newAssemblyList.AddRange(_assemblies);
newAssemblyTypes.AddRange(_assemblies.Select(_ => 16));

outputWriter.Write(newAssemblyList.Count);
foreach (var assemblyName in newAssemblyList)
{
WriteString(outputWriter, assemblyName);
}

outputWriter.Write(newAssemblyTypes.Count);
foreach (var assemblyType in newAssemblyTypes)
{
outputWriter.Write(assemblyType);
}

while (inputStream.Position < inputStream.Length)
{
outputWriter.Write(inputReader.ReadByte());
}
}

private static void WriteString(BinaryWriter outputWriter, string @string)
{
outputWriter.Write(@string.Length);
var paddedLenth = (int)(Math.Ceiling(@string.Length / 4f) * 4f);
for (int i = 0; i < paddedLenth; i++)
{
if (i < @string.Length)
outputWriter.Write(@string[i]);
else
outputWriter.Write((byte)0);
}
}

private static string ReadString(BinaryReader inputReader)
{
var length = inputReader.ReadInt32();
var paddedLenth = (int)(Math.Ceiling(length / 4f) * 4f);
StringBuilder sb = new StringBuilder(length);
for (var j = 0; j < paddedLenth; j++)
{
var c = inputReader.ReadChar();
if (j < length)
sb.Append(c);
}

return sb.ToString();
}

private static long SeekFirstName(FileStream inputStream, BinaryReader inputReader)
{
while (inputStream.Position < inputStream.Length)
{
var currentPos = inputStream.Position;
var firstChar = inputReader.ReadChar();
if (firstChar != 'U') continue;

var nextString = new string(inputReader.ReadChars(14));
if (!nextString.Equals("nityEngine.dll")) continue;

return currentPos;
}

return -1;
}
}
}
10 changes: 10 additions & 0 deletions Il2CppInterop.Runtime/Injection/AssemblyList/IAssemblyListFile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Il2CppInterop.Runtime.Injection
{
internal interface IAssemblyListFile
{
public bool IsTargetFile(string originalFilePath);
public void Setup(string originalFilePath);
public void AddAssembly(string name);
public string GetOrCreateNewFile();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System;
using System.IO;
using System.Text.Json.Nodes;

namespace Il2CppInterop.Runtime.Injection
{
internal class JSONAssemblyListFile : IAssemblyListFile
{
private JsonNode node;
private JsonArray names;
private JsonArray types;

private string newFile;

public void Setup(string originalFilePath)
{
if (node != null) return;

node = JsonNode.Parse(File.ReadAllText(originalFilePath));
names = node["names"].AsArray();
types = node["types"].AsArray();
}

public bool IsTargetFile(string originalFilePath)
{
return originalFilePath.Contains("ScriptingAssemblies.json");
}

public void AddAssembly(string name)
{
names.Add(name);
types.Add(16);
}

public string GetOrCreateNewFile()
{
if (!string.IsNullOrEmpty(newFile)) return newFile;

var newJson = node.ToJsonString();
newFile = Path.GetTempFileName();

File.WriteAllText(newFile, newJson);
return newFile;
}
}
}
8 changes: 7 additions & 1 deletion Il2CppInterop.Runtime/Injection/ClassInjector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,13 @@ public static void RegisterTypeInIl2Cpp(Type type, RegisterTypeOptions options)
var interfaceFunctionCount = interfaces.Sum(i => i.MethodCount);
var classPointer = UnityVersionHandler.NewClass(baseClassPointer.VtableCount + interfaceFunctionCount);

classPointer.Image = InjectorHelpers.InjectedImage.ImagePointer;
classPointer.Image = InjectorHelpers.DefaultInjectedImage.ImagePointer;

if (InjectorHelpers.TryGetInjectedImageForAssembly(type.Assembly, out var imagePtr))
{
classPointer.Image = (Il2CppImage*)imagePtr;
}

classPointer.Parent = baseClassPointer.ClassPointer;
classPointer.ElementClass = classPointer.Class = classPointer.CastClass = classPointer.ClassPointer;
classPointer.NativeSize = -1;
Expand Down
8 changes: 7 additions & 1 deletion Il2CppInterop.Runtime/Injection/EnumInjector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,13 @@ public static void RegisterEnumInIl2Cpp(Type type, bool logSuccess = true)
UnityVersionHandler.Wrap(
(Il2CppClass*)Il2CppClassPointerStore.GetNativeClassPointer(Enum.GetUnderlyingType(type)));

il2cppEnum.Image = InjectorHelpers.InjectedImage.ImagePointer;
il2cppEnum.Image = InjectorHelpers.DefaultInjectedImage.ImagePointer;

if (InjectorHelpers.TryGetInjectedImageForAssembly(type.Assembly, out var imagePtr))
{
il2cppEnum.Image = (Il2CppImage*)imagePtr;
}

il2cppEnum.Class = il2cppEnum.CastClass = il2cppEnum.ElementClass = elementClass.ClassPointer;
il2cppEnum.Parent = baseEnum.ClassPointer;
il2cppEnum.ActualSize = il2cppEnum.InstanceSize =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System;
using System.Runtime.InteropServices;
using Il2CppInterop.Runtime.Runtime;

namespace Il2CppInterop.Runtime.Injection.Hooks
{
internal unsafe class API_il2cpp_domain_get_assemblies_hook : Hook<API_il2cpp_domain_get_assemblies_hook.MethodDelegate>
{
public override MethodDelegate GetDetour() => Hook;
public override string TargetMethodName => "il2cpp_domain_get_assemblies";

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate IntPtr MethodDelegate(IntPtr domain, long* size);

private IntPtr currentDataPtr = IntPtr.Zero;
private int lastAssemblyListSize = -1;

private IntPtr Hook(IntPtr domain, long* size)
{
IntPtr assemblyArrayPtr = Original(domain, size);

if (InjectorHelpers.InjectedImages.Count > 0)
{
CreateCustomAssemblyList((int)*size, assemblyArrayPtr);

*size = lastAssemblyListSize;
return currentDataPtr;
}

return assemblyArrayPtr;
}

private void CreateCustomAssemblyList(int origSize, IntPtr assemblyArrayPtr)
{
var newSize = origSize + InjectorHelpers.InjectedImages.Count;
if (lastAssemblyListSize == newSize) return;

Il2CppAssembly** oldArray = (Il2CppAssembly**)assemblyArrayPtr;

if (currentDataPtr != IntPtr.Zero)
Marshal.FreeHGlobal(currentDataPtr);

currentDataPtr = Marshal.AllocHGlobal(newSize * sizeof(Il2CppSystem.IntPtr));
Il2CppAssembly** newArray = (Il2CppAssembly**)currentDataPtr;

int i;

for (i = 0; i < origSize; i++)
newArray[i] = oldArray[i];

i = origSize;
foreach (IntPtr imagePtr in InjectorHelpers.InjectedImages.Values)
{
var image = UnityVersionHandler.Wrap((Il2CppImage*)imagePtr);
newArray[i] = image.Assembly;
i++;
}

lastAssemblyListSize = newSize;
}

public override IntPtr FindTargetMethod()
{
return InjectorHelpers.GetIl2CppExport(nameof(IL2CPP.il2cpp_domain_get_assemblies));
}
}
}
Loading
Loading