Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## 1.3.3

- Read native generic enumerable battle targets through their iterator interfaces, preserving values previously lost to collection-reader errors.
- Check populated and empty native enumerables during startup before enabling combat capture.
- Include native exception details when startup reader checks fail.
- Keep BepInEx and Il2CppInterop build versions separate from the capture release version, including in CI environments.

## 1.3.2

- Preserve events when the writer queue fills by waiting for space instead of discarding them.
Expand Down
3 changes: 2 additions & 1 deletion Sources/AtelierCapture/AtelierCapture.csproj
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Version>1.3.2</Version>
<Version>1.3.3</Version>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<RuntimeDirectory Condition="'$(RuntimeDirectory)' == ''">$(MSBuildThisFileDirectory)..\BepInEx-master\bin\Unity.IL2CPP</RuntimeDirectory>
Expand Down
2 changes: 1 addition & 1 deletion Sources/AtelierCapture/Battle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ internal static void Install(Harmony harmony)
foreach (var hook in configured.Where(h => !string.IsNullOrEmpty(h.Role)))
Skipped[hook.Role] = disabledReason;
Plugin.ReportFailure("combat_readers", ex);
Plugin.Log.LogError($"Combat capture disabled: native reader check failed ({disabledReason}). API capture remains enabled. Use the matching plugin and HarmonySupport DLLs from the package.");
Plugin.Log.LogError($"Combat capture disabled: native reader check failed ({disabledReason}). API capture remains enabled. Use the matching plugin and HarmonySupport DLLs from the package.\n{cause}");
return;
}
Plugin.Log.LogInfo("Native collection, nullable allocation, scalar, and tuple capture checks passed.");
Expand Down
42 changes: 41 additions & 1 deletion Sources/AtelierCapture/Snapshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,15 @@ internal static void CheckReaders()
list.Add(17);
list.Add(29);
var view = new Il2CppSystem.Collections.Generic.IList<int>(list.Pointer);
foreach (var source in new Il2CppObjectBase[] { list, view })
var enumerable = new Il2CppSystem.Collections.Generic.IEnumerable<int>(list.Pointer);
foreach (var source in new Il2CppObjectBase[] { list, view, enumerable })
{
var values = Collections.GetOrAdd(source.GetType(), CollectionReader)(source).Take(3).Cast<int>();
if (!values.SequenceEqual(new[] { 17, 29 })) throw new InvalidDataException("Native list reader failed");
}
list.Clear();
if (Collections.GetOrAdd(enumerable.GetType(), CollectionReader)(enumerable).Any())
throw new InvalidDataException("Empty native enumerable reader failed");
var map = new Il2CppSystem.Collections.Generic.Dictionary<int, int>();
map.Add(7, 11);
map.Add(13, 19);
Expand Down Expand Up @@ -66,6 +70,13 @@ internal static void CheckReaders()
if (!Equals(tupleFields.Single(f => f.Name == "m_Item1").Get!(tuple), 23)
|| !Equals(tupleFields.Single(f => f.Name == "m_Item2").Get!(tuple), true))
throw new InvalidDataException("Native tuple reader failed");
var references = new Il2CppSystem.Collections.Generic.List<Il2CppSystem.Tuple<int, bool>>();
references.Add(tuple);
var referenceView = new Il2CppSystem.Collections.Generic.IEnumerable<Il2CppSystem.Tuple<int, bool>>(references.Pointer);
var referenceItems = Collections.GetOrAdd(referenceView.GetType(), CollectionReader)(referenceView).Take(2)
.Cast<Il2CppSystem.Tuple<int, bool>>().ToArray();
if (referenceItems.Length != 1 || referenceItems[0].Pointer != tuple.Pointer)
throw new InvalidDataException("Native enumerable reference changed");
if (System.Text.Json.JsonSerializer.Serialize(new Snapshot().Read(tuple, 11))
!= System.Text.Json.JsonSerializer.Serialize(new Snapshot().Read(tuple)))
throw new InvalidDataException("Scalar record was depth limited");
Expand Down Expand Up @@ -199,6 +210,14 @@ private object Collection(object value, Type type, int id, int depth)
var get = type.GetMethod("GetEnumerator", Type.EmptyTypes)
?? throw new NotSupportedException("Collection has no concrete enumerator");
var iterator = get.ReturnType;
if (iterator.IsGenericType && iterator.GetGenericTypeDefinition() == typeof(Il2CppSystem.Collections.Generic.IEnumerator<>))
{
var openInterface = NativeGetter<object>(get);
var moveInterface = NativeGetter<bool>(typeof(Il2CppSystem.Collections.IEnumerator).GetMethod("MoveNext")!);
var readInterface = NativeGetter<object?>(iterator.GetProperty("Current")!.GetMethod!);
var closeInterface = NativeGetter<object?>(typeof(Il2CppSystem.IDisposable).GetMethod("Dispose")!);
return value => Enumerate(openInterface(value), moveInterface, readInterface, closeInterface);
}
var next = iterator.GetMethod("MoveNext", Type.EmptyTypes)
?? throw new NotSupportedException("Enumerator has no concrete MoveNext");
var current = iterator.GetProperty("Current")!.GetMethod!;
Expand All @@ -225,6 +244,27 @@ private static Func<object, T> Getter<T>(MethodInfo method)
return Expression.Lambda<Func<object, T>>(body, value).Compile();
}

private static unsafe Func<object, T> NativeGetter<T>(MethodInfo method)
{
var field = Il2CppInteropUtils.GetIl2CppMethodInfoPointerFieldForGeneratedMethod(method)
?? throw new MissingMethodException($"Native method pointer unavailable: {method.Name}");
var read = method.ReturnType == typeof(void) ? null : BoxedReader(method.ReturnType);
return value =>
{
var native = (Il2CppObjectBase)value;
var pointer = native.Pointer;
var target = IL2CPP.il2cpp_object_get_virtual_method(pointer, (IntPtr)field.GetValue(null)!);
if (target == IntPtr.Zero) throw new MissingMethodException(method.Name);
// Generated interface calls do not unbox value-type implementations.
var receiver = IL2CPP.il2cpp_class_is_valuetype(native.ObjectClass) ? IL2CPP.il2cpp_object_unbox(pointer) : pointer;
var exception = IntPtr.Zero;
var result = IL2CPP.il2cpp_runtime_invoke(target, receiver, null, ref exception);
GC.KeepAlive(native);
Il2CppException.RaiseExceptionIfNecessary(exception);
return read is null ? default! : (T)read(result)!;
};
}

private static Field[] NativeFields(Type type) => type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Where(p => p.PropertyType != typeof(string) && p.GetIndexParameters().Length == 0
&& p.GetMethod is { IsStatic: false } method && IsNative(method))
Expand Down
4 changes: 2 additions & 2 deletions Sources/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ dotnet build Sources/BepInEx/Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.I
"-p:SolutionDir=$runtimeSource/" "-p:BuildDir=$captureBuild/manual/BepInEx" `
"-p:BuildArtifactsDirectory=$captureBuild/manual/BepInEx/" `
"-p:UnityReferenceDirectory=$captureBuild/dependencies" `
-p:GeneratePackageOnBuild=false -p:VersionSuffix=be.788-capture.1
-p:GeneratePackageOnBuild=false -p:Version=6.0.0-be.788-capture.1
```

The runtime DLLs are written to `.work/manual/BepInEx/Unity.IL2CPP/`.
Expand All @@ -52,7 +52,7 @@ The runtime DLLs are written to `.work/manual/BepInEx/Unity.IL2CPP/`.
dotnet build Sources/Il2CppInterop/Il2CppInterop.HarmonySupport/Il2CppInterop.HarmonySupport.csproj -c Release `
"-p:BuildArtifactsDirectory=$captureBuild/manual/Il2CppInterop/" `
"-p:Il2CppReferenceDirectory=$captureBuild/dependencies" `
-p:GeneratePackageOnBuild=false -p:VersionSuffix=capture.2
-p:GeneratePackageOnBuild=false -p:Version=1.5.3-capture.2
```

The DLL is written to `.work/manual/Il2CppInterop/bin/Il2CppInterop.HarmonySupport/net6.0/Il2CppInterop.HarmonySupport.dll`.
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.3.2
1.3.3
4 changes: 2 additions & 2 deletions scripts/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,14 @@ def main():
build(bepinex / "Runtimes/Unity/BepInEx.Unity.IL2CPP/BepInEx.Unity.IL2CPP.csproj",
SolutionDir=str(bepinex) + "/", BuildDir=str(compiled / "BepInEx"),
BuildArtifactsDirectory=str(compiled / "BepInEx") + "/",
UnityReferenceDirectory=dependencies, VersionSuffix="be.788-capture.1", SourceRevisionId=BEPINEX_COMMIT)
UnityReferenceDirectory=dependencies, Version="6.0.0-be.788-capture.1", SourceRevisionId=BEPINEX_COMMIT)
runtime = stage / "BepInEx/core"
for path in (compiled / "BepInEx/Unity.IL2CPP").glob("*.dll"):
shutil.copyfile(path, runtime / path.name)

build(interop / "Il2CppInterop.HarmonySupport/Il2CppInterop.HarmonySupport.csproj",
BuildArtifactsDirectory=str(compiled / "Il2CppInterop") + "/",
Il2CppReferenceDirectory=dependencies, VersionSuffix="capture.2", SourceRevisionId=INTEROP_COMMIT)
Il2CppReferenceDirectory=dependencies, Version="1.5.3-capture.2", SourceRevisionId=INTEROP_COMMIT)
shutil.copyfile(compiled / "Il2CppInterop/bin/Il2CppInterop.HarmonySupport/net6.0/Il2CppInterop.HarmonySupport.dll",
runtime / "Il2CppInterop.HarmonySupport.dll")

Expand Down
Loading