From 0193a479aaa399bb07a08ecd4971a6d5835be8e2 Mon Sep 17 00:00:00 2001 From: Jaymie Date: Sun, 11 Oct 2026 01:08:32 -0400 Subject: [PATCH] Fix native enumerable battle capture and runtime build versions --- CHANGELOG.md | 7 ++++ Sources/AtelierCapture/AtelierCapture.csproj | 3 +- Sources/AtelierCapture/Battle.cs | 2 +- Sources/AtelierCapture/Snapshot.cs | 42 +++++++++++++++++++- Sources/README.md | 4 +- VERSION | 2 +- scripts/build.py | 4 +- 7 files changed, 56 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae73fbc..4ab07b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/Sources/AtelierCapture/AtelierCapture.csproj b/Sources/AtelierCapture/AtelierCapture.csproj index 1500881..b33423b 100644 --- a/Sources/AtelierCapture/AtelierCapture.csproj +++ b/Sources/AtelierCapture/AtelierCapture.csproj @@ -1,8 +1,9 @@ net6.0 - 1.3.2 + 1.3.3 enable + true enable latest $(MSBuildThisFileDirectory)..\BepInEx-master\bin\Unity.IL2CPP diff --git a/Sources/AtelierCapture/Battle.cs b/Sources/AtelierCapture/Battle.cs index c613b5f..8fdb00e 100644 --- a/Sources/AtelierCapture/Battle.cs +++ b/Sources/AtelierCapture/Battle.cs @@ -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."); diff --git a/Sources/AtelierCapture/Snapshot.cs b/Sources/AtelierCapture/Snapshot.cs index 00c96af..b000b0e 100644 --- a/Sources/AtelierCapture/Snapshot.cs +++ b/Sources/AtelierCapture/Snapshot.cs @@ -30,11 +30,15 @@ internal static void CheckReaders() list.Add(17); list.Add(29); var view = new Il2CppSystem.Collections.Generic.IList(list.Pointer); - foreach (var source in new Il2CppObjectBase[] { list, view }) + var enumerable = new Il2CppSystem.Collections.Generic.IEnumerable(list.Pointer); + foreach (var source in new Il2CppObjectBase[] { list, view, enumerable }) { var values = Collections.GetOrAdd(source.GetType(), CollectionReader)(source).Take(3).Cast(); 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(); map.Add(7, 11); map.Add(13, 19); @@ -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>(); + references.Add(tuple); + var referenceView = new Il2CppSystem.Collections.Generic.IEnumerable>(references.Pointer); + var referenceItems = Collections.GetOrAdd(referenceView.GetType(), CollectionReader)(referenceView).Take(2) + .Cast>().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"); @@ -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(get); + var moveInterface = NativeGetter(typeof(Il2CppSystem.Collections.IEnumerator).GetMethod("MoveNext")!); + var readInterface = NativeGetter(iterator.GetProperty("Current")!.GetMethod!); + var closeInterface = NativeGetter(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!; @@ -225,6 +244,27 @@ private static Func Getter(MethodInfo method) return Expression.Lambda>(body, value).Compile(); } + private static unsafe Func NativeGetter(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)) diff --git a/Sources/README.md b/Sources/README.md index 3a60bf6..87a7137 100644 --- a/Sources/README.md +++ b/Sources/README.md @@ -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/`. @@ -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`. diff --git a/VERSION b/VERSION index 1892b92..31e5c84 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.3.2 +1.3.3 diff --git a/scripts/build.py b/scripts/build.py index f3feda2..a529801 100644 --- a/scripts/build.py +++ b/scripts/build.py @@ -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")