From 788bcf361db340f0e5c06679b19266a6898ef0de Mon Sep 17 00:00:00 2001 From: ManlyMarco <39247311+ManlyMarco@users.noreply.github.com> Date: Thu, 2 Oct 2025 13:43:42 +0200 Subject: [PATCH 1/6] Refactor inspector member collecting code --- .../RuntimeUnityEditor.Core.projitems | 1 + .../Inspector/Inspector.InspectorTab.cs | 211 +--------------- .../Windows/Inspector/MemberCollector.cs | 236 ++++++++++++++++++ 3 files changed, 242 insertions(+), 206 deletions(-) create mode 100644 RuntimeUnityEditor.Core/Windows/Inspector/MemberCollector.cs diff --git a/RuntimeUnityEditor.Core/RuntimeUnityEditor.Core.projitems b/RuntimeUnityEditor.Core/RuntimeUnityEditor.Core.projitems index fe394b5..c46ca27 100644 --- a/RuntimeUnityEditor.Core/RuntimeUnityEditor.Core.projitems +++ b/RuntimeUnityEditor.Core/RuntimeUnityEditor.Core.projitems @@ -87,6 +87,7 @@ + diff --git a/RuntimeUnityEditor.Core/Windows/Inspector/Inspector.InspectorTab.cs b/RuntimeUnityEditor.Core/Windows/Inspector/Inspector.InspectorTab.cs index 467566d..1c37a4c 100644 --- a/RuntimeUnityEditor.Core/Windows/Inspector/Inspector.InspectorTab.cs +++ b/RuntimeUnityEditor.Core/Windows/Inspector/Inspector.InspectorTab.cs @@ -39,7 +39,7 @@ public InspectorStackEntryBase CurrentStackItem public void Clear() { InspectorStack.Clear(); - CacheAllMembers(null); + _fieldCache.Clear(); } public void Pop() @@ -68,218 +68,17 @@ public void Push(InspectorStackEntryBase stackEntry) LoadStackEntry(stackEntry); } - private static IEnumerable MethodsToCacheEntries(object instance, Type ownerType, IEnumerable methodsToCheck) - { - var cacheItems = methodsToCheck - .Where(x => !x.IsConstructor && !x.IsSpecialName) - .Where(f => !f.IsDefined(typeof(CompilerGeneratedAttribute), false)) - .Where(x => x.Name != "MemberwiseClone" && x.Name != "obj_address") // Instant game crash - .Select(m => new MethodCacheEntry(instance, m, ownerType)).Cast(); - return cacheItems; - } - - private void CacheAllMembers(InstanceStackEntry entry) - { - _fieldCache.Clear(); - - var objectToOpen = entry?.Instance; - if (objectToOpen == null) return; - - var type = objectToOpen.GetType(); - - try - { - CallbackCacheEntry GetExportTexEntry(Texture texture) - { - return new CallbackCacheEntry("Export Texture to file", - "Encode the texture to a PNG and save it to a new file", - texture.SaveTextureToFileWithDialog); - } - - if (objectToOpen is Component cmp) - { - if (ObjectTreeViewer.Initialized) - { - _fieldCache.Add(new CallbackCacheEntry("Open in Scene Object Browser", - "Navigate to GameObject this Component is attached to", - () => ObjectTreeViewer.Instance.SelectAndShowObject(cmp.transform))); - } - - if (objectToOpen is UnityEngine.UI.Image img) - _fieldCache.Add(GetExportTexEntry(img.mainTexture)); - else if (objectToOpen is Renderer rend && MeshExport.CanExport(rend)) - { - _fieldCache.Add(new CallbackCacheEntry("Export mesh to .obj", "Save base mesh used by this renderer to file", () => MeshExport.ExportObj(rend, false, false))); - _fieldCache.Add(new CallbackCacheEntry("Export mesh to .obj (Baked)", "Bakes current pose into the exported mesh", () => MeshExport.ExportObj(rend, true, false))); - _fieldCache.Add(new CallbackCacheEntry("Export mesh to .obj (World)", "Bakes pose while keeping world position", () => MeshExport.ExportObj(rend, true, true))); - } - } - else if (objectToOpen is GameObject castedObj) - { - if (ObjectTreeViewer.Initialized) - { - _fieldCache.Add(new CallbackCacheEntry("Open in Scene Object Browser", - "Navigate to this object in the Scene Object Browser", - () => ObjectTreeViewer.Instance.SelectAndShowObject(castedObj.transform))); - } -#if !IL2CPP - _fieldCache.Add(new ReadonlyCacheEntry("Child objects", castedObj.transform.Cast().ToArray())); -#endif - _fieldCache.Add(new ReadonlyCacheEntry("Components", castedObj.AbstractGetAllComponents())); - } - else if (objectToOpen is Texture tex) - { - _fieldCache.Add(GetExportTexEntry(tex)); - } - - // If we somehow enter a string, this allows user to see what the string actually says - if (type == typeof(string)) - { - _fieldCache.Add(new ReadonlyCacheEntry("this", objectToOpen)); - } - else if (objectToOpen is Transform) - { - // Prevent the list overloads from listing subcomponents - } - else if (objectToOpen is IList list) - { - for (var i = 0; i < list.Count; i++) - _fieldCache.Add(new ListCacheEntry(list, i)); - } - else if (objectToOpen is IEnumerable enumerable) - { - _fieldCache.AddRange(enumerable.Cast() - .Select((x, y) => x is ICacheEntry ? x : new ReadonlyListCacheEntry(x, y)) - .Cast()); - } - else - { - // Needed for IL2CPP collections since they don't implement IEnumerable - // Can cause side effects if the object is not a real collection - var getEnumeratorM = type.GetMethod("GetEnumerator", AccessTools.all, null, Type.EmptyTypes, null); - if (getEnumeratorM != null) - { - try - { - var enumerator = getEnumeratorM.Invoke(objectToOpen, null); - if (enumerator != null) - { - var enumeratorType = enumerator.GetType(); - var moveNextM = enumeratorType.GetMethod("MoveNext", AccessTools.all, null, Type.EmptyTypes, null); - var currentP = enumeratorType.GetProperty("Current"); - if (moveNextM != null && currentP != null) - { - var count = 0; - while ((bool)moveNextM.Invoke(enumerator, null)) - { - var current = currentP.GetValue(enumerator, null); - _fieldCache.Add(new ReadonlyListCacheEntry(current, count)); - count++; - } - } - } - } - catch (Exception e) - { - RuntimeUnityEditorCore.Logger.Log(LogLevel.Warning, $"Failed to enumerate object \"{objectToOpen}\" ({type.FullName}) : {e}"); - } - } - } - - // No need if it's not a value type, only used to propagate changes back so it's redundant with classes - var parent = entry.Parent?.Type().IsValueType == true ? entry.Parent : null; - - // Instance members - _fieldCache.AddRange(type.GetAllFields(false) - .Where(f => !f.IsDefined(typeof(CompilerGeneratedAttribute), false)) - .Select(f => new FieldCacheEntry(objectToOpen, f, type, parent)).Cast()); - - var isRenderer = objectToOpen is Renderer; -#if IL2CPP - var isIl2cppType = objectToOpen is Il2CppSystem.Type; -#endif - _fieldCache.AddRange(type.GetAllProperties(false) - .Where(f => !f.IsDefined(typeof(CompilerGeneratedAttribute), false)) - .Select(p => - { - if (isRenderer) - { - // Prevent unintentionally creating local material instances when viewing renderers in inspector - if (p.Name == "material") - return new CallbackCacheEntry("material", "Local instance of sharedMaterial (create on entry)", () => ((Renderer)objectToOpen).material); - if (p.Name == "materials") - return new CallbackCacheEntry("materials", "Local instance of sharedMaterials (create on entry)", () => ((Renderer)objectToOpen).materials); - } -#if IL2CPP - else if (isIl2cppType) - { - // These two are dangerous to evaluate, they hard crash the game with access violation more often than not - if (p.Name == nameof(Il2CppSystem.Type.DeclaringType)) - return new CallbackCacheEntry(nameof(Il2CppSystem.Type.DeclaringType), "Skipped evaluation, click to enter (DANGER, MAY HARD CRASH)", () => ((Il2CppSystem.Type)objectToOpen).DeclaringType); - if (p.Name == nameof(Il2CppSystem.Type.DeclaringMethod)) - return new CallbackCacheEntry(nameof(Il2CppSystem.Type.DeclaringMethod), "Skipped evaluation, click to enter (DANGER, MAY HARD CRASH)", () => ((Il2CppSystem.Type)objectToOpen).DeclaringMethod); - } -#endif - - return (ICacheEntry)new PropertyCacheEntry(objectToOpen, p, type, parent); - })); - - _fieldCache.AddRange(type.GetAllEvents(false) - .Where(f => !f.IsDefined(typeof(CompilerGeneratedAttribute), false)) - .Select(p => new EventCacheEntry(objectToOpen, p, type)).Cast()); - - _fieldCache.AddRange(MethodsToCacheEntries(objectToOpen, type, type.GetAllMethods(false))); - - CacheStaticMembersHelper(type); - } - catch (Exception ex) - { - RuntimeUnityEditorCore.Logger.Log(LogLevel.Warning, "[Inspector] CacheFields crash: " + ex); - } - } - - private void CacheStaticMembers(StaticStackEntry entry) - { - _fieldCache.Clear(); - - if (entry?.StaticType == null) return; - - try - { - CacheStaticMembersHelper(entry.StaticType); - } - catch (Exception ex) - { - RuntimeUnityEditorCore.Logger.Log(LogLevel.Warning, "[Inspector] CacheFields crash: " + ex); - } - } - - private void CacheStaticMembersHelper(Type type) - { - _fieldCache.AddRange(type.GetAllFields(true) - .Where(f => !f.IsDefined(typeof(CompilerGeneratedAttribute), false)) - .Select(f => new FieldCacheEntry(null, f, type)).Cast()); - - _fieldCache.AddRange(type.GetAllProperties(true) - .Where(f => !f.IsDefined(typeof(CompilerGeneratedAttribute), false)) - .Select(p => new PropertyCacheEntry(null, p, type)).Cast()); - - _fieldCache.AddRange(type.GetAllEvents(true) - .Where(f => !f.IsDefined(typeof(CompilerGeneratedAttribute), false)) - .Select(p => new EventCacheEntry(null, p, type)).Cast()); - - _fieldCache.AddRange(MethodsToCacheEntries(null, type, type.GetAllMethods(true))); - } - private void LoadStackEntry(InspectorStackEntryBase stackEntry) { switch (stackEntry) { case InstanceStackEntry instanceStackEntry: - CacheAllMembers(instanceStackEntry); + _fieldCache.Clear(); + _fieldCache.AddRange(MemberCollector.CollectAllMembers(instanceStackEntry)); break; case StaticStackEntry staticStackEntry: - CacheStaticMembers(staticStackEntry); + _fieldCache.Clear(); + _fieldCache.AddRange(MemberCollector.CollectStaticMembers(staticStackEntry)); break; case null: _fieldCache.Clear(); diff --git a/RuntimeUnityEditor.Core/Windows/Inspector/MemberCollector.cs b/RuntimeUnityEditor.Core/Windows/Inspector/MemberCollector.cs new file mode 100644 index 0000000..80a181d --- /dev/null +++ b/RuntimeUnityEditor.Core/Windows/Inspector/MemberCollector.cs @@ -0,0 +1,236 @@ +using HarmonyLib; +using RuntimeUnityEditor.Core.Inspector.Entries; +using RuntimeUnityEditor.Core.ObjectTree; +using RuntimeUnityEditor.Core.Utils; +using RuntimeUnityEditor.Core.Utils.Abstractions; +using RuntimeUnityEditor.Core.Utils.ObjectDumper; +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using UnityEngine; +using Component = UnityEngine.Component; + +namespace RuntimeUnityEditor.Core.Inspector +{ + /// + /// Helper class for caching members (fields, properties, methods, events) for inspector objects. + /// + internal static class MemberCollector + { + public static ICollection CollectAllMembers(InstanceStackEntry entry) + { + var fieldCache = new List(); + var objectToOpen = entry?.Instance; + if (objectToOpen == null) return fieldCache; + + var type = objectToOpen.GetType(); + + //TODO + // - Handle il2cpp-side fields by making them act like normal fields + // - give il2cpp-side fields and methods a special color or something in inspector, show pointer address in tooltip + var staticFields = type.GetFields(BindingFlags.Static | BindingFlags.NonPublic).Where(x => x.IsInitOnly).ToList(); + var fieldPointerLookup = staticFields.Where(x => x.Name.StartsWith("NativeFieldInfoPtr_")) + .ToDictionary(x => x.Name.Replace("NativeFieldInfoPtr_", ""), x => x.GetValue(null)); + var methodPointerLookup = staticFields.Where(x => x.Name.StartsWith("NativeMethodInfoPtr_")) + .ToDictionary(x => x.Name.Replace("NativeMethodInfoPtr_", ""), x => x.GetValue(null)); + + try + { + + CallbackCacheEntry GetExportTexEntry(Texture texture) + { + return new CallbackCacheEntry("Export Texture to file", + "Encode the texture to a PNG and save it to a new file", + texture.SaveTextureToFileWithDialog); + } + + if (objectToOpen is Component cmp) + { + if (ObjectTreeViewer.Initialized) + { + fieldCache.Add(new CallbackCacheEntry("Open in Scene Object Browser", + "Navigate to GameObject this Component is attached to", + () => ObjectTreeViewer.Instance.SelectAndShowObject(cmp.transform))); + } + + if (objectToOpen is UnityEngine.UI.Image img) + fieldCache.Add(GetExportTexEntry(img.mainTexture)); + else if (objectToOpen is Renderer rend && MeshExport.CanExport(rend)) + { + fieldCache.Add(new CallbackCacheEntry("Export mesh to .obj", "Save base mesh used by this renderer to file", () => MeshExport.ExportObj(rend, false, false))); + fieldCache.Add(new CallbackCacheEntry("Export mesh to .obj (Baked)", "Bakes current pose into the exported mesh", () => MeshExport.ExportObj(rend, true, false))); + fieldCache.Add(new CallbackCacheEntry("Export mesh to .obj (World)", "Bakes pose while keeping world position", () => MeshExport.ExportObj(rend, true, true))); + } + } + else if (objectToOpen is GameObject castedObj) + { + if (ObjectTreeViewer.Initialized) + { + fieldCache.Add(new CallbackCacheEntry("Open in Scene Object Browser", + "Navigate to this object in the Scene Object Browser", + () => ObjectTreeViewer.Instance.SelectAndShowObject(castedObj.transform))); + } +#if !IL2CPP + fieldCache.Add(new ReadonlyCacheEntry("Child objects", castedObj.transform.Cast().ToArray())); +#endif + fieldCache.Add(new ReadonlyCacheEntry("Components", castedObj.AbstractGetAllComponents())); + } + else if (objectToOpen is Texture tex) + { + fieldCache.Add(GetExportTexEntry(tex)); + } + + // If we somehow enter a string, this allows user to see what the string actually says + if (type == typeof(string)) + { + fieldCache.Add(new ReadonlyCacheEntry("this", objectToOpen)); + } + else if (objectToOpen is Transform) + { + // Prevent the list overloads from listing subcomponents + } + else if (objectToOpen is IList list) + { + for (var i = 0; i < list.Count; i++) + fieldCache.Add(new ListCacheEntry(list, i)); + } + else if (objectToOpen is IEnumerable enumerable) + { + fieldCache.AddRange(enumerable.Cast() + .Select((x, y) => x is ICacheEntry ? x : new ReadonlyListCacheEntry(x, y)) + .Cast()); + } + else + { + // Needed for IL2CPP collections since they don't implement IEnumerable + // Can cause side effects if the object is not a real collection + var getEnumeratorM = type.GetMethod("GetEnumerator", AccessTools.all, null, Type.EmptyTypes, null); + if (getEnumeratorM != null) + { + try + { + var enumerator = getEnumeratorM.Invoke(objectToOpen, null); + if (enumerator != null) + { + var enumeratorType = enumerator.GetType(); + var moveNextM = enumeratorType.GetMethod("MoveNext", AccessTools.all, null, Type.EmptyTypes, null); + var currentP = enumeratorType.GetProperty("Current"); + if (moveNextM != null && currentP != null) + { + var count = 0; + while ((bool)moveNextM.Invoke(enumerator, null)) + { + var current = currentP.GetValue(enumerator, null); + fieldCache.Add(new ReadonlyListCacheEntry(current, count)); + count++; + } + } + } + } + catch (Exception e) + { + RuntimeUnityEditorCore.Logger.Log(LogLevel.Warning, $"Failed to enumerate object \"{objectToOpen}\" ({type.FullName}) : {e}"); + } + } + } + + // No need if it's not a value type, only used to propagate changes back so it's redundant with classes + var parent = entry.Parent?.Type().IsValueType == true ? entry.Parent : null; + + // Instance members + fieldCache.AddRange(type.GetAllFields(false) + .Where(f => !f.IsDefined(typeof(CompilerGeneratedAttribute), false)) + .Select(f => new FieldCacheEntry(objectToOpen, f, type, parent)).Cast()); + + var isRenderer = objectToOpen is Renderer; +#if IL2CPP + var isIl2cppType = objectToOpen is Il2CppSystem.Type; +#endif + fieldCache.AddRange(type.GetAllProperties(false) + .Where(f => !f.IsDefined(typeof(CompilerGeneratedAttribute), false)) + .Select(p => + { + if (isRenderer) + { + // Prevent unintentionally creating local material instances when viewing renderers in inspector + if (p.Name == "material") + return new CallbackCacheEntry("material", "Local instance of sharedMaterial (create on entry)", () => ((Renderer)objectToOpen).material); + if (p.Name == "materials") + return new CallbackCacheEntry("materials", "Local instance of sharedMaterials (create on entry)", () => ((Renderer)objectToOpen).materials); + } +#if IL2CPP + else if (isIl2cppType) + { + // These two are dangerous to evaluate, they hard crash the game with access violation more often than not + if (p.Name == nameof(Il2CppSystem.Type.DeclaringType)) + return new CallbackCacheEntry(nameof(Il2CppSystem.Type.DeclaringType), "Skipped evaluation, click to enter (DANGER, MAY HARD CRASH)", () => ((Il2CppSystem.Type)objectToOpen).DeclaringType); + if (p.Name == nameof(Il2CppSystem.Type.DeclaringMethod)) + return new CallbackCacheEntry(nameof(Il2CppSystem.Type.DeclaringMethod), "Skipped evaluation, click to enter (DANGER, MAY HARD CRASH)", () => ((Il2CppSystem.Type)objectToOpen).DeclaringMethod); + } +#endif + + return (ICacheEntry)new PropertyCacheEntry(objectToOpen, p, type, parent); + })); + + fieldCache.AddRange(type.GetAllEvents(false) + .Where(f => !f.IsDefined(typeof(CompilerGeneratedAttribute), false)) + .Select(p => new EventCacheEntry(objectToOpen, p, type)).Cast()); + + fieldCache.AddRange(MethodsToCacheEntries(objectToOpen, type, type.GetAllMethods(false))); + + fieldCache.AddRange(CacheStaticMembersHelper(type)); + + return fieldCache; + } + catch (Exception ex) + { + RuntimeUnityEditorCore.Logger.Log(LogLevel.Warning, "[Inspector] CacheFields crash: " + ex); + fieldCache.Clear(); + fieldCache.Add(new ReadonlyCacheEntry("Exception", ex)); + } + + return fieldCache; + } + + public static ICollection CollectStaticMembers(StaticStackEntry entry) + { + var fieldCache = new List(); + if (entry?.StaticType == null) return fieldCache; + fieldCache.AddRange(CacheStaticMembersHelper(entry.StaticType)); + return fieldCache; + } + + private static ICollection CacheStaticMembersHelper(Type type) + { + var fieldCache = new List(); + fieldCache.AddRange(type.GetAllFields(true) + .Where(f => !f.IsDefined(typeof(CompilerGeneratedAttribute), false)) + .Select(f => new FieldCacheEntry(null, f, type)).Cast()); + + fieldCache.AddRange(type.GetAllProperties(true) + .Where(f => !f.IsDefined(typeof(CompilerGeneratedAttribute), false)) + .Select(p => new PropertyCacheEntry(null, p, type)).Cast()); + + fieldCache.AddRange(type.GetAllEvents(true) + .Where(f => !f.IsDefined(typeof(CompilerGeneratedAttribute), false)) + .Select(p => new EventCacheEntry(null, p, type)).Cast()); + + fieldCache.AddRange(MethodsToCacheEntries(null, type, type.GetAllMethods(true))); + return fieldCache; + } + + private static IEnumerable MethodsToCacheEntries(object instance, Type ownerType, IEnumerable methodsToCheck) + { + var cacheItems = methodsToCheck + .Where(x => !x.IsConstructor && !x.IsSpecialName) + .Where(f => !f.IsDefined(typeof(CompilerGeneratedAttribute), false)) + .Where(x => x.Name != "MemberwiseClone" && x.Name != "obj_address") // Instant game crash + .Select(m => new MethodCacheEntry(instance, m, ownerType)).Cast(); + return cacheItems; + } + } +} From 6231a111d0c89ec12cc753bb634d3791c2b4a7c8 Mon Sep 17 00:00:00 2001 From: ManlyMarco <39247311+ManlyMarco@users.noreply.github.com> Date: Thu, 2 Oct 2025 14:52:28 +0200 Subject: [PATCH 2/6] wip --- .../Windows/Inspector/MemberCollector.cs | 88 ++++++++++++++++--- 1 file changed, 76 insertions(+), 12 deletions(-) diff --git a/RuntimeUnityEditor.Core/Windows/Inspector/MemberCollector.cs b/RuntimeUnityEditor.Core/Windows/Inspector/MemberCollector.cs index 80a181d..84c7b68 100644 --- a/RuntimeUnityEditor.Core/Windows/Inspector/MemberCollector.cs +++ b/RuntimeUnityEditor.Core/Windows/Inspector/MemberCollector.cs @@ -21,26 +21,85 @@ namespace RuntimeUnityEditor.Core.Inspector /// internal static class MemberCollector { + private static readonly Dictionary> _ptrLookup = new(); + internal sealed class Il2CppMemberInfo + { + /// + /// Prop getter if it's a property or field address if it's a field + /// + public FieldInfo Getter { get; } + /// + /// Always null for fields, prop setter if it's a property that has a setter + /// + public FieldInfo Setter { get; } + public string TrimmedName { get; } + public bool IsProperty { get; } + + public Il2CppMemberInfo(string trimmedName, FieldInfo getter, FieldInfo setter, bool isProperty) + { + Getter = getter; + Setter = setter; + IsProperty = isProperty; + TrimmedName = trimmedName; + } + } + + private static Dictionary GetPtrLookupTable(Type type) + { + // todo some way to clean up old entries? + if (_ptrLookup.TryGetValue(type, out var value)) + return value; + + // TODO redo to use memberinfo as key and figure out the target field for properties and methods via + // Il2CppInterop.Common.Il2CppInteropUtils.GetIl2CppMethodInfoPointerFieldForGeneratedMethod() + // probably enough to list fields and assume if it's not a field it's a method and use the method at time of collecting members not here + + var staticFields = type.GetFields(BindingFlags.Static | BindingFlags.NonPublic).Where(x => x.IsInitOnly).ToList(); + + value = staticFields.Where(x => x.Name.StartsWith("NativeFieldInfoPtr_")) + .Select(x => new Il2CppMemberInfo(x.Name.Replace("NativeFieldInfoPtr_", ""), x, null, false)) + .ToDictionary(x => x.TrimmedName, x => x); + + + bool IsFromProp(string name) => name.StartsWith("get_", StringComparison.Ordinal) || name.StartsWith("set_", StringComparison.Ordinal); + + var methodPointerLookup = staticFields.Where(x => x.Name.StartsWith("NativeMethodInfoPtr_", StringComparison.Ordinal)) + .Select(x => new { x, trim = x.Name.Replace("NativeMethodInfoPtr_", "") }) + .GroupBy(x => IsFromProp(x.trim) ? x.trim.Substring(4) : x.trim).ToList(); + + foreach (var gr in methodPointerLookup) + { + var entries = gr.ToArray(); + var isProp = IsFromProp(entries[0].trim); + if (isProp) + { + var get = entries.FirstOrDefault(x => x.trim.StartsWith("get_", StringComparison.Ordinal)); + var set = entries.FirstOrDefault(x => x.trim.StartsWith("set_", StringComparison.Ordinal)); + value.Add(gr.Key, new Il2CppMemberInfo(gr.Key, get?.x, set?.x, true)); + } + else + { + value.Add(gr.Key, new Il2CppMemberInfo(gr.Key, entries[0].x, null, false)); + } + } + + _ptrLookup[type] = value; + return value; + } + + //TODO + // - Handle il2cpp-side fields by making them act like normal fields + // - give il2cpp-side fields and methods a special color or something in inspector, show pointer address in tooltip public static ICollection CollectAllMembers(InstanceStackEntry entry) { - var fieldCache = new List(); + var fieldCache = new List(); var objectToOpen = entry?.Instance; if (objectToOpen == null) return fieldCache; var type = objectToOpen.GetType(); - //TODO - // - Handle il2cpp-side fields by making them act like normal fields - // - give il2cpp-side fields and methods a special color or something in inspector, show pointer address in tooltip - var staticFields = type.GetFields(BindingFlags.Static | BindingFlags.NonPublic).Where(x => x.IsInitOnly).ToList(); - var fieldPointerLookup = staticFields.Where(x => x.Name.StartsWith("NativeFieldInfoPtr_")) - .ToDictionary(x => x.Name.Replace("NativeFieldInfoPtr_", ""), x => x.GetValue(null)); - var methodPointerLookup = staticFields.Where(x => x.Name.StartsWith("NativeMethodInfoPtr_")) - .ToDictionary(x => x.Name.Replace("NativeMethodInfoPtr_", ""), x => x.GetValue(null)); - try { - CallbackCacheEntry GetExportTexEntry(Texture texture) { return new CallbackCacheEntry("Export Texture to file", @@ -75,7 +134,9 @@ CallbackCacheEntry GetExportTexEntry(Texture texture) () => ObjectTreeViewer.Instance.SelectAndShowObject(castedObj.transform))); } #if !IL2CPP - fieldCache.Add(new ReadonlyCacheEntry("Child objects", castedObj.transform.Cast().ToArray())); + fieldCache.Add(new ReadonlyCacheEntry("Child objects", castedObj.transform.Cast().ToArray())); +#else + fieldCache.Add(new ReadonlyCacheEntry("Child objects", castedObj.transform.CastToEnumerable().Select(x => x.Cast()).ToArray())); #endif fieldCache.Add(new ReadonlyCacheEntry("Components", castedObj.AbstractGetAllComponents())); } @@ -141,6 +202,8 @@ CallbackCacheEntry GetExportTexEntry(Texture texture) // No need if it's not a value type, only used to propagate changes back so it's redundant with classes var parent = entry.Parent?.Type().IsValueType == true ? entry.Parent : null; + var il2cppLookup = GetPtrLookupTable(type); + // Instance members fieldCache.AddRange(type.GetAllFields(false) .Where(f => !f.IsDefined(typeof(CompilerGeneratedAttribute), false)) @@ -233,4 +296,5 @@ private static IEnumerable MethodsToCacheEntries(object instance, T return cacheItems; } } + } From d31b32e5c1515700aec676deadb3a8ee71d0510b Mon Sep 17 00:00:00 2001 From: ManlyMarco <39247311+ManlyMarco@users.noreply.github.com> Date: Fri, 3 Oct 2025 01:33:44 +0200 Subject: [PATCH 3/6] It's working! --- .../Entries/Contents/CacheEntryBase.cs | 2 +- .../Entries/Contents/MethodCacheEntry.cs | 6 +- .../Windows/Inspector/Inspector.cs | 24 +- .../Windows/Inspector/MemberCollector.cs | 308 ++++++++++++++---- 4 files changed, 274 insertions(+), 66 deletions(-) diff --git a/RuntimeUnityEditor.Core/Windows/Inspector/Entries/Contents/CacheEntryBase.cs b/RuntimeUnityEditor.Core/Windows/Inspector/Entries/Contents/CacheEntryBase.cs index d6c2e07..1e5d80c 100644 --- a/RuntimeUnityEditor.Core/Windows/Inspector/Entries/Contents/CacheEntryBase.cs +++ b/RuntimeUnityEditor.Core/Windows/Inspector/Entries/Contents/CacheEntryBase.cs @@ -110,7 +110,7 @@ public string TypeName() } private bool? _canEnter; - private readonly GUIContent _nameContent; + private protected readonly GUIContent _nameContent; /// public virtual bool CanEnterValue() diff --git a/RuntimeUnityEditor.Core/Windows/Inspector/Entries/Contents/MethodCacheEntry.cs b/RuntimeUnityEditor.Core/Windows/Inspector/Entries/Contents/MethodCacheEntry.cs index c6beab6..ff2e5a2 100644 --- a/RuntimeUnityEditor.Core/Windows/Inspector/Entries/Contents/MethodCacheEntry.cs +++ b/RuntimeUnityEditor.Core/Windows/Inspector/Entries/Contents/MethodCacheEntry.cs @@ -25,7 +25,7 @@ public MethodCacheEntry(object instance, MethodInfo methodInfo, Type owner) ParameterString = GetParameterPreviewString(methodInfo); - _content = new GUIContent(_name,null, methodInfo.GetFancyDescription()); + _nameContent = new GUIContent(_name,null, methodInfo.GetFancyDescription()); } internal static string GetParameterPreviewString(MethodBase methodInfo) @@ -65,7 +65,7 @@ internal static string GetParameterPreviewString(MethodBase methodInfo) private readonly string _name; private readonly string _returnTypeName; - private readonly GUIContent _content; + private protected readonly GUIContent _nameContent; /// /// Name of the method. @@ -78,7 +78,7 @@ internal static string GetParameterPreviewString(MethodBase methodInfo) public string TypeName() => _returnTypeName; /// - public GUIContent GetNameContent() => _content; + public GUIContent GetNameContent() => _nameContent; /// /// Not supported for methods. diff --git a/RuntimeUnityEditor.Core/Windows/Inspector/Inspector.cs b/RuntimeUnityEditor.Core/Windows/Inspector/Inspector.cs index 32f3901..f799d9b 100644 --- a/RuntimeUnityEditor.Core/Windows/Inspector/Inspector.cs +++ b/RuntimeUnityEditor.Core/Windows/Inspector/Inspector.cs @@ -68,7 +68,8 @@ public string SearchString private bool _showMethods = true; private bool _showEvents = true; #if IL2CPP - private bool _showNative; + private bool _showNative = true; + private bool _showManaged = true; #endif private bool _showDeclaredOnly; private bool _showTooltips = true; @@ -196,7 +197,8 @@ protected override void DrawContents() _showMethods = GUILayout.Toggle(_showMethods, "Methods"); _showEvents = GUILayout.Toggle(_showEvents, "Events"); #if IL2CPP - _showNative = GUILayout.Toggle(_showNative, "Native"); + _showNative = GUILayout.Toggle(_showNative, new GUIContent("Native", null, "Display members from the IL2CPP runtime (i.e. the game code).")); + _showManaged = GUILayout.Toggle(_showManaged, new GUIContent("Managed", null, "Display members from the BepInEx's runtime (i.e. interop and plugin code).")); #endif _showDeclaredOnly = GUILayout.Toggle(_showDeclaredOnly, "Only declared"); @@ -390,14 +392,24 @@ private void DrawContentScrollView(InspectorTab tab) } visibleFieldsQuery = visibleFieldsQuery.Where(x => { +#if IL2CPP + if (IL2CPPFieldCacheEntry.IsIl2CppCacheEntry(x)) + { + if (!_showNative) + return false; + } + else + { + if (!_showManaged) + return false; + } + if (x is IL2CPPFieldCacheEntry cf) + return _showFields && (!_showDeclaredOnly || cf.IsDeclared); +#endif switch (x) { case PropertyCacheEntry p when !_showProperties || _showDeclaredOnly && !p.IsDeclared: -#if IL2CPP - case FieldCacheEntry f when !_showFields || _showDeclaredOnly && !f.IsDeclared || !_showNative && f.FieldInfo.IsStatic && f.Type() == typeof(IntPtr): -#else case FieldCacheEntry f when !_showFields || _showDeclaredOnly && !f.IsDeclared: -#endif case MethodCacheEntry m when !_showMethods || _showDeclaredOnly && !m.IsDeclared: case EventCacheEntry e when !_showEvents || _showDeclaredOnly && !e.IsDeclared: return false; diff --git a/RuntimeUnityEditor.Core/Windows/Inspector/MemberCollector.cs b/RuntimeUnityEditor.Core/Windows/Inspector/MemberCollector.cs index 84c7b68..cbd5a5e 100644 --- a/RuntimeUnityEditor.Core/Windows/Inspector/MemberCollector.cs +++ b/RuntimeUnityEditor.Core/Windows/Inspector/MemberCollector.cs @@ -7,12 +7,10 @@ using System; using System.Collections; using System.Collections.Generic; -using System.ComponentModel; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using UnityEngine; -using Component = UnityEngine.Component; namespace RuntimeUnityEditor.Core.Inspector { @@ -21,72 +19,81 @@ namespace RuntimeUnityEditor.Core.Inspector /// internal static class MemberCollector { - private static readonly Dictionary> _ptrLookup = new(); - internal sealed class Il2CppMemberInfo - { - /// - /// Prop getter if it's a property or field address if it's a field - /// - public FieldInfo Getter { get; } - /// - /// Always null for fields, prop setter if it's a property that has a setter - /// - public FieldInfo Setter { get; } - public string TrimmedName { get; } - public bool IsProperty { get; } - - public Il2CppMemberInfo(string trimmedName, FieldInfo getter, FieldInfo setter, bool isProperty) - { - Getter = getter; - Setter = setter; - IsProperty = isProperty; - TrimmedName = trimmedName; - } - } +#if IL2CPP + private static readonly Dictionary> _ptrLookup = new(); - private static Dictionary GetPtrLookupTable(Type type) + private static Dictionary GetPtrLookupTable(Type type) { // todo some way to clean up old entries? if (_ptrLookup.TryGetValue(type, out var value)) return value; - // TODO redo to use memberinfo as key and figure out the target field for properties and methods via - // Il2CppInterop.Common.Il2CppInteropUtils.GetIl2CppMethodInfoPointerFieldForGeneratedMethod() - // probably enough to list fields and assume if it's not a field it's a method and use the method at time of collecting members not here + value = new Dictionary(); var staticFields = type.GetFields(BindingFlags.Static | BindingFlags.NonPublic).Where(x => x.IsInitOnly).ToList(); - value = staticFields.Where(x => x.Name.StartsWith("NativeFieldInfoPtr_")) - .Select(x => new Il2CppMemberInfo(x.Name.Replace("NativeFieldInfoPtr_", ""), x, null, false)) - .ToDictionary(x => x.TrimmedName, x => x); + var fieldPtrs = staticFields.Where(x => x.Name.StartsWith("NativeFieldInfoPtr_")) + .Select(x => new { trimmed = x.Name.Substring("NativeFieldInfoPtr_".Length), ptrF = x }); + var usedFields = new HashSet(); - bool IsFromProp(string name) => name.StartsWith("get_", StringComparison.Ordinal) || name.StartsWith("set_", StringComparison.Ordinal); + foreach (var fieldPtr in fieldPtrs) + { + var targetFieldName = fieldPtr.trimmed; + // Fields are props in il2cpp interop + var targetField = type.GetProperty(targetFieldName, AccessTools.all); + if (targetField != null) + { + value[targetField] = fieldPtr.ptrF; - var methodPointerLookup = staticFields.Where(x => x.Name.StartsWith("NativeMethodInfoPtr_", StringComparison.Ordinal)) - .Select(x => new { x, trim = x.Name.Replace("NativeMethodInfoPtr_", "") }) - .GroupBy(x => IsFromProp(x.trim) ? x.trim.Substring(4) : x.trim).ToList(); + var getMethod = targetField.GetGetMethod(); + if (getMethod != null) usedFields.Add(getMethod); + var setMethod = targetField.GetSetMethod(); + if (setMethod != null) usedFields.Add(setMethod); + } + } - foreach (var gr in methodPointerLookup) + foreach (var propertyInfo in type.GetAllProperties(true)) { - var entries = gr.ToArray(); - var isProp = IsFromProp(entries[0].trim); - if (isProp) + // It's a field + if (value.ContainsKey(propertyInfo)) + continue; + + var getMethod = propertyInfo.GetGetMethod(true); + if (getMethod != null && getMethod.GetMethodBody() != null) { - var get = entries.FirstOrDefault(x => x.trim.StartsWith("get_", StringComparison.Ordinal)); - var set = entries.FirstOrDefault(x => x.trim.StartsWith("set_", StringComparison.Ordinal)); - value.Add(gr.Key, new Il2CppMemberInfo(gr.Key, get?.x, set?.x, true)); + var ptr = Il2CppInterop.Common.Il2CppInteropUtils.GetIl2CppMethodInfoPointerFieldForGeneratedMethod(getMethod); + if (ptr != null) + value[getMethod] = ptr; } - else + + var setMethod = propertyInfo.GetSetMethod(); + if (setMethod != null && setMethod.GetMethodBody() != null) { - value.Add(gr.Key, new Il2CppMemberInfo(gr.Key, entries[0].x, null, false)); + var ptr = Il2CppInterop.Common.Il2CppInteropUtils.GetIl2CppMethodInfoPointerFieldForGeneratedMethod(setMethod); + if (ptr != null) + value[setMethod] = ptr; + } + } + + foreach (var methodInfo in type.GetAllMethods(true)) + { + if (value.ContainsKey(methodInfo) || usedFields.Contains(methodInfo)) + continue; + + if (methodInfo.GetMethodBody() != null) + { + var ptr = Il2CppInterop.Common.Il2CppInteropUtils.GetIl2CppMethodInfoPointerFieldForGeneratedMethod(methodInfo); + if (ptr != null) + value[methodInfo] = ptr; } } _ptrLookup[type] = value; return value; } - +#endif + //TODO // - Handle il2cpp-side fields by making them act like normal fields // - give il2cpp-side fields and methods a special color or something in inspector, show pointer address in tooltip @@ -202,16 +209,16 @@ CallbackCacheEntry GetExportTexEntry(Texture texture) // No need if it's not a value type, only used to propagate changes back so it's redundant with classes var parent = entry.Parent?.Type().IsValueType == true ? entry.Parent : null; - var il2cppLookup = GetPtrLookupTable(type); // Instance members fieldCache.AddRange(type.GetAllFields(false) .Where(f => !f.IsDefined(typeof(CompilerGeneratedAttribute), false)) - .Select(f => new FieldCacheEntry(objectToOpen, f, type, parent)).Cast()); + .Select(f => (ICacheEntry)new FieldCacheEntry(objectToOpen, f, type, parent))); var isRenderer = objectToOpen is Renderer; #if IL2CPP var isIl2cppType = objectToOpen is Il2CppSystem.Type; + var il2cppLookup = GetPtrLookupTable(type); #endif fieldCache.AddRange(type.GetAllProperties(false) .Where(f => !f.IsDefined(typeof(CompilerGeneratedAttribute), false)) @@ -234,6 +241,9 @@ CallbackCacheEntry GetExportTexEntry(Texture texture) if (p.Name == nameof(Il2CppSystem.Type.DeclaringMethod)) return new CallbackCacheEntry(nameof(Il2CppSystem.Type.DeclaringMethod), "Skipped evaluation, click to enter (DANGER, MAY HARD CRASH)", () => ((Il2CppSystem.Type)objectToOpen).DeclaringMethod); } + + if (IL2CPPFieldCacheEntry.TryGetIl2CppCacheEntry(objectToOpen, type, p, il2cppLookup, out var result)) + return result; #endif return (ICacheEntry)new PropertyCacheEntry(objectToOpen, p, type, parent); @@ -241,7 +251,14 @@ CallbackCacheEntry GetExportTexEntry(Texture texture) fieldCache.AddRange(type.GetAllEvents(false) .Where(f => !f.IsDefined(typeof(CompilerGeneratedAttribute), false)) - .Select(p => new EventCacheEntry(objectToOpen, p, type)).Cast()); + .Select(p => + { +#if IL2CPP + if (IL2CPPFieldCacheEntry.TryGetIl2CppCacheEntry(objectToOpen, type, p, il2cppLookup, out var result)) + return result; +#endif + return new EventCacheEntry(objectToOpen, p, type); + }).Cast()); fieldCache.AddRange(MethodsToCacheEntries(objectToOpen, type, type.GetAllMethods(false))); @@ -253,7 +270,7 @@ CallbackCacheEntry GetExportTexEntry(Texture texture) { RuntimeUnityEditorCore.Logger.Log(LogLevel.Warning, "[Inspector] CacheFields crash: " + ex); fieldCache.Clear(); - fieldCache.Add(new ReadonlyCacheEntry("Exception", ex)); + fieldCache.Add(new ReadonlyCacheEntry("Exception", ex.ToString())); } return fieldCache; @@ -269,18 +286,38 @@ public static ICollection CollectStaticMembers(StaticStackEntry ent private static ICollection CacheStaticMembersHelper(Type type) { +#if IL2CPP + var il2cppLookup = GetPtrLookupTable(type); +#endif var fieldCache = new List(); fieldCache.AddRange(type.GetAllFields(true) - .Where(f => !f.IsDefined(typeof(CompilerGeneratedAttribute), false)) - .Select(f => new FieldCacheEntry(null, f, type)).Cast()); + .Where(f => !f.IsDefined(typeof(CompilerGeneratedAttribute), false)) +#if IL2CPP + .Where(f => !f.Name.StartsWith("NativeFieldInfoPtr_") && !f.Name.StartsWith("NativeMethodInfoPtr_")) +#endif + .Select(f => (ICacheEntry)new FieldCacheEntry(null, f, type))); fieldCache.AddRange(type.GetAllProperties(true) - .Where(f => !f.IsDefined(typeof(CompilerGeneratedAttribute), false)) - .Select(p => new PropertyCacheEntry(null, p, type)).Cast()); + .Where(f => !f.IsDefined(typeof(CompilerGeneratedAttribute), false)) + .Select(p => + { +#if IL2CPP + if (IL2CPPFieldCacheEntry.TryGetIl2CppCacheEntry(null, type, p, il2cppLookup, out var result)) + return result; +#endif + return (ICacheEntry)new PropertyCacheEntry(null, p, type); + })); fieldCache.AddRange(type.GetAllEvents(true) - .Where(f => !f.IsDefined(typeof(CompilerGeneratedAttribute), false)) - .Select(p => new EventCacheEntry(null, p, type)).Cast()); + .Where(f => !f.IsDefined(typeof(CompilerGeneratedAttribute), false)) + .Select(p => + { +#if IL2CPP + if (IL2CPPFieldCacheEntry.TryGetIl2CppCacheEntry(null, type, p, il2cppLookup, out var result)) + return result; +#endif + return (ICacheEntry)new EventCacheEntry(null, p, type); + })); fieldCache.AddRange(MethodsToCacheEntries(null, type, type.GetAllMethods(true))); return fieldCache; @@ -288,13 +325,172 @@ private static ICollection CacheStaticMembersHelper(Type type) private static IEnumerable MethodsToCacheEntries(object instance, Type ownerType, IEnumerable methodsToCheck) { +#if IL2CPP + var il2cppLookup = GetPtrLookupTable(ownerType); +#endif var cacheItems = methodsToCheck +#if IL2CPP // TODO: Events are not implemented in il2cpp interop, they show up as separate add/remove/raise methods + .Where(x => !x.IsConstructor && (!x.IsSpecialName || x.Name.StartsWith("add_") || x.Name.StartsWith("raise_") || x.Name.StartsWith("remove_"))) +#else .Where(x => !x.IsConstructor && !x.IsSpecialName) +#endif .Where(f => !f.IsDefined(typeof(CompilerGeneratedAttribute), false)) .Where(x => x.Name != "MemberwiseClone" && x.Name != "obj_address") // Instant game crash - .Select(m => new MethodCacheEntry(instance, m, ownerType)).Cast(); + .Select(m => + { +#if IL2CPP + if (il2cppLookup.TryGetValue(m, out var ptr)) + return (ICacheEntry)new IL2CPPMethodCacheEntry(instance, m, ownerType, ptr); +#endif + return (ICacheEntry)new MethodCacheEntry(instance, m, ownerType); + }); return cacheItems; } } +#if IL2CPP + /// + public class IL2CPPFieldCacheEntry : PropertyCacheEntry + { + public FieldInfo PtrField { get; } + + /// + public IL2CPPFieldCacheEntry(object ins, PropertyInfo p, Type owner, FieldInfo ptrField) : base(ins, p, owner) + { + PtrField = ptrField; + _nameContent.text = $"C-F/{_nameContent.text}"; + _nameContent.tooltip = $"IL2CPP Field (ptr={SafeGetPtr(owner, ptrField)})\n\n{_nameContent.tooltip}"; + } + + /// + public IL2CPPFieldCacheEntry(object ins, PropertyInfo p, Type owner, FieldInfo ptrField, ICacheEntry parent) : base(ins, p, owner, parent) + { + PtrField = ptrField; + _nameContent.text = $"C-F/{_nameContent.text}"; + _nameContent.tooltip = $"IL2CPP Field (ptr={SafeGetPtr(owner, ptrField)})\n\n{_nameContent.tooltip}"; + } + + + internal static bool TryGetIl2CppCacheEntry(object instance, Type type, EventInfo p, Dictionary lookup, out ICacheEntry result) + { + FieldInfo ptrAdd = null; + FieldInfo ptrRaise = null; + FieldInfo ptrRemove = null; + var addMethod = p.GetAddMethod(true); + if (addMethod != null) lookup.TryGetValue(addMethod, out ptrAdd); + var raiseMethod = p.GetRaiseMethod(true); + if (raiseMethod != null) lookup.TryGetValue(raiseMethod, out ptrRaise); + var removeMethod = p.GetRemoveMethod(true); + if (removeMethod != null) lookup.TryGetValue(removeMethod, out ptrRemove); + if (ptrAdd != null || ptrRaise != null || ptrRemove != null) + { + result = (ICacheEntry)new IL2CPPEventCacheEntry(instance, p, type, ptrAdd, ptrRaise, ptrRemove); + return true; + } + + result = null; + return false; + } + + internal static bool TryGetIl2CppCacheEntry(object instance, Type type, PropertyInfo p, Dictionary lookup, out ICacheEntry result) + { + if (lookup.TryGetValue(p, out var ptr)) + { + result = (ICacheEntry)new IL2CPPFieldCacheEntry(instance, p, type, ptr); + return true; + } + + FieldInfo ptrGet = null; + FieldInfo ptrSet = null; + var getMethod = p.GetGetMethod(true); + if (getMethod != null) lookup.TryGetValue(getMethod, out ptrGet); + var setMethod = p.GetSetMethod(true); + if (setMethod != null) lookup.TryGetValue(setMethod, out ptrSet); + if (ptrGet != null || ptrSet != null) + { + result = (ICacheEntry)new IL2CPPPropertyCacheEntry(instance, p, type, ptrGet, ptrSet); + return true; + } + + result = null; + return false; + } + + internal static object SafeGetPtr(Type owner, FieldInfo ptrField) + { + if (ptrField == null) return "null"; + if (owner.ContainsGenericParameters) + return "???"; + try + { + return ptrField.GetValue(null); + } + catch + { + return "error"; + } + } + + internal static bool IsIl2CppCacheEntry(ICacheEntry entry) + { + return entry is IL2CPPFieldCacheEntry || entry is IL2CPPPropertyCacheEntry || entry is IL2CPPMethodCacheEntry || entry is IL2CPPEventCacheEntry; + } + } + + /// + public class IL2CPPPropertyCacheEntry : PropertyCacheEntry + { + public FieldInfo PtrFieldGet { get; } + public FieldInfo PtrFieldSet { get; } + + /// + public IL2CPPPropertyCacheEntry(object ins, PropertyInfo p, Type owner, FieldInfo ptrFieldGet, FieldInfo ptrFieldSet) : base(ins, p, owner) + { + PtrFieldGet = ptrFieldGet; + PtrFieldSet = ptrFieldSet; + _nameContent.text = $"C-P/{_nameContent.text}"; + _nameContent.tooltip = $"IL2CPP Property (getPtr={IL2CPPFieldCacheEntry.SafeGetPtr(owner, ptrFieldGet)}, setPtr={IL2CPPFieldCacheEntry.SafeGetPtr(owner, ptrFieldSet)})\n\n{_nameContent.tooltip}"; + } + /// + public IL2CPPPropertyCacheEntry(object ins, PropertyInfo p, Type owner, FieldInfo ptrFieldGet, FieldInfo ptrFieldSet, ICacheEntry parent) : base(ins, p, owner, parent) + { + PtrFieldGet = ptrFieldGet; + PtrFieldSet = ptrFieldSet; + _nameContent.text = $"C-P/{_nameContent.text}"; + _nameContent.tooltip = $"IL2CPP Property (getPtr={IL2CPPFieldCacheEntry.SafeGetPtr(owner, ptrFieldGet)}, setPtr={IL2CPPFieldCacheEntry.SafeGetPtr(owner, ptrFieldSet)})\n\n{_nameContent.tooltip}"; + } + } + + /// + public class IL2CPPMethodCacheEntry : MethodCacheEntry + { + public FieldInfo PtrField { get; } + + /// + public IL2CPPMethodCacheEntry(object instance, MethodInfo methodInfo, Type owner, FieldInfo ptrField) : base(instance, methodInfo, owner) + { + PtrField = ptrField; + _nameContent.text = $"C-M/{_nameContent.text}"; + _nameContent.tooltip = $"IL2CPP Method (ptr={IL2CPPFieldCacheEntry.SafeGetPtr(owner, ptrField)})\n\n{_nameContent.tooltip}"; + } + } + + /// + /// TODO: This does nothing so far because events are not implemented in il2cpp interop (they show up as separate add/remove/raise methods). Maybe combine them back into events? + public class IL2CPPEventCacheEntry : EventCacheEntry + { + public FieldInfo PtrFieldAdd { get; } + public FieldInfo PtrFieldRaise { get; } + public FieldInfo PtrFieldRemove { get; } + /// + public IL2CPPEventCacheEntry(object ins, EventInfo e, Type owner, FieldInfo ptrFieldAdd, FieldInfo ptrFieldRaise, FieldInfo ptrFieldRemove) : base(ins, e, owner) + { + PtrFieldAdd = ptrFieldAdd; + PtrFieldRaise = ptrFieldRaise; + PtrFieldRemove = ptrFieldRemove; + _nameContent.text = $"C-E/{_nameContent.text}"; + _nameContent.tooltip = $"IL2CPP Event (addPtr={IL2CPPFieldCacheEntry.SafeGetPtr(owner, ptrFieldAdd)}, raisePtr={IL2CPPFieldCacheEntry.SafeGetPtr(owner, ptrFieldRaise)}, removePtr={IL2CPPFieldCacheEntry.SafeGetPtr(owner, ptrFieldRemove)})\n\n{_nameContent.tooltip}"; + } + } +#endif } From 3cff8912a12c5c55355a18a4e2c56c4afb8087b0 Mon Sep 17 00:00:00 2001 From: ManlyMarco <39247311+ManlyMarco@users.noreply.github.com> Date: Fri, 3 Oct 2025 02:08:52 +0200 Subject: [PATCH 4/6] REfactoring --- .../RuntimeUnityEditor.Core.projitems | 4 + .../IL2CPP/MemberCollector.IL2CPP.cs | 225 ++++++++++++++++ .../Windows/Inspector/Inspector.cs | 13 +- .../Windows/Inspector/MemberCollector.cs | 245 +----------------- 4 files changed, 254 insertions(+), 233 deletions(-) create mode 100644 RuntimeUnityEditor.Core/Windows/Inspector/IL2CPP/MemberCollector.IL2CPP.cs diff --git a/RuntimeUnityEditor.Core/RuntimeUnityEditor.Core.projitems b/RuntimeUnityEditor.Core/RuntimeUnityEditor.Core.projitems index c46ca27..c5494fe 100644 --- a/RuntimeUnityEditor.Core/RuntimeUnityEditor.Core.projitems +++ b/RuntimeUnityEditor.Core/RuntimeUnityEditor.Core.projitems @@ -88,6 +88,7 @@ + @@ -107,4 +108,7 @@ + + + \ No newline at end of file diff --git a/RuntimeUnityEditor.Core/Windows/Inspector/IL2CPP/MemberCollector.IL2CPP.cs b/RuntimeUnityEditor.Core/Windows/Inspector/IL2CPP/MemberCollector.IL2CPP.cs new file mode 100644 index 0000000..dc9f87b --- /dev/null +++ b/RuntimeUnityEditor.Core/Windows/Inspector/IL2CPP/MemberCollector.IL2CPP.cs @@ -0,0 +1,225 @@ +#if IL2CPP +using HarmonyLib; +using RuntimeUnityEditor.Core.Inspector.Entries; +using RuntimeUnityEditor.Core.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace RuntimeUnityEditor.Core.Inspector.IL2CPP; + +public class IL2CPPCacheEntryHelper +{ + private static readonly Dictionary> _ptrLookup = new(); + + public static Dictionary GetPtrLookupTable(Type type) + { + // todo some way to clean up old entries? + if (_ptrLookup.TryGetValue(type, out var value)) + return value; + + value = new Dictionary(); + + var staticFields = type.GetFields(BindingFlags.Static | BindingFlags.NonPublic).Where(x => x.IsInitOnly).ToList(); + + var fieldPtrs = staticFields.Where(x => x.Name.StartsWith("NativeFieldInfoPtr_")) + .Select(x => new { trimmed = x.Name.Substring("NativeFieldInfoPtr_".Length), ptrF = x }); + + var usedFields = new HashSet(); + + foreach (var fieldPtr in fieldPtrs) + { + var targetFieldName = fieldPtr.trimmed; + // Fields are props in il2cpp interop + var targetField = type.GetProperty(targetFieldName, AccessTools.all); + if (targetField != null) + { + value[targetField] = fieldPtr.ptrF; + + var getMethod = targetField.GetGetMethod(); + if (getMethod != null) usedFields.Add(getMethod); + var setMethod = targetField.GetSetMethod(); + if (setMethod != null) usedFields.Add(setMethod); + } + } + + foreach (var propertyInfo in type.GetAllProperties(true)) + { + // It's a field + if (value.ContainsKey(propertyInfo)) + continue; + + var getMethod = propertyInfo.GetGetMethod(true); + if (getMethod != null && getMethod.GetMethodBody() != null) + { + var ptr = Il2CppInterop.Common.Il2CppInteropUtils.GetIl2CppMethodInfoPointerFieldForGeneratedMethod(getMethod); + if (ptr != null) + value[getMethod] = ptr; + } + + var setMethod = propertyInfo.GetSetMethod(); + if (setMethod != null && setMethod.GetMethodBody() != null) + { + var ptr = Il2CppInterop.Common.Il2CppInteropUtils.GetIl2CppMethodInfoPointerFieldForGeneratedMethod(setMethod); + if (ptr != null) + value[setMethod] = ptr; + } + } + + foreach (var methodInfo in type.GetAllMethods(true)) + { + if (value.ContainsKey(methodInfo) || usedFields.Contains(methodInfo)) + continue; + + if (methodInfo.GetMethodBody() != null) + { + var ptr = Il2CppInterop.Common.Il2CppInteropUtils.GetIl2CppMethodInfoPointerFieldForGeneratedMethod(methodInfo); + if (ptr != null) + value[methodInfo] = ptr; + } + } + + _ptrLookup[type] = value; + return value; + } + + public static bool TryGetIl2CppCacheEntry(object instance, Type type, EventInfo p, Dictionary lookup, out ICacheEntry result) + { + FieldInfo ptrAdd = null; + FieldInfo ptrRaise = null; + FieldInfo ptrRemove = null; + var addMethod = p.GetAddMethod(true); + if (addMethod != null) lookup.TryGetValue(addMethod, out ptrAdd); + var raiseMethod = p.GetRaiseMethod(true); + if (raiseMethod != null) lookup.TryGetValue(raiseMethod, out ptrRaise); + var removeMethod = p.GetRemoveMethod(true); + if (removeMethod != null) lookup.TryGetValue(removeMethod, out ptrRemove); + if (ptrAdd != null || ptrRaise != null || ptrRemove != null) + { + result = new IL2CPPEventCacheEntry(instance, p, type, ptrAdd, ptrRaise, ptrRemove); + return true; + } + + result = null; + return false; + } + + public static bool TryGetIl2CppCacheEntry(object instance, Type type, PropertyInfo p, Dictionary lookup, out ICacheEntry result) + { + if (lookup.TryGetValue(p, out var ptr)) + { + result = new IL2CPPFieldCacheEntry(instance, p, type, ptr); + return true; + } + + FieldInfo ptrGet = null; + FieldInfo ptrSet = null; + var getMethod = p.GetGetMethod(true); + if (getMethod != null) lookup.TryGetValue(getMethod, out ptrGet); + var setMethod = p.GetSetMethod(true); + if (setMethod != null) lookup.TryGetValue(setMethod, out ptrSet); + if (ptrGet != null || ptrSet != null) + { + result = new IL2CPPPropertyCacheEntry(instance, p, type, ptrGet, ptrSet); + return true; + } + + result = null; + return false; + } + + public static object SafeGetPtr(Type owner, FieldInfo ptrField) + { + if (ptrField == null) return "null"; + if (owner.ContainsGenericParameters) + return "???"; + try + { + return ptrField.GetValue(null); + } + catch + { + return "error"; + } + } + + internal static bool IsIl2CppCacheEntry(ICacheEntry entry) + { + return entry is IL2CPPFieldCacheEntry || entry is IL2CPPPropertyCacheEntry || entry is IL2CPPMethodCacheEntry || entry is IL2CPPEventCacheEntry; + } +} + +/// +public class IL2CPPFieldCacheEntry : PropertyCacheEntry +{ + public FieldInfo PtrField { get; } + + /// + public IL2CPPFieldCacheEntry(object ins, PropertyInfo p, Type owner, FieldInfo ptrField) : base(ins, p, owner) + { + PtrField = ptrField; + _nameContent.tooltip = $"IL2CPP Field (ptr={IL2CPPCacheEntryHelper.SafeGetPtr(owner, ptrField)})\n\n{_nameContent.tooltip}"; + } + + /// + public IL2CPPFieldCacheEntry(object ins, PropertyInfo p, Type owner, FieldInfo ptrField, ICacheEntry parent) : base(ins, p, owner, parent) + { + PtrField = ptrField; + _nameContent.tooltip = $"IL2CPP Field (ptr={IL2CPPCacheEntryHelper.SafeGetPtr(owner, ptrField)})\n\n{_nameContent.tooltip}"; + } +} + +/// +public class IL2CPPPropertyCacheEntry : PropertyCacheEntry +{ + public FieldInfo PtrFieldGet { get; } + public FieldInfo PtrFieldSet { get; } + + /// + public IL2CPPPropertyCacheEntry(object ins, PropertyInfo p, Type owner, FieldInfo ptrFieldGet, FieldInfo ptrFieldSet) : base(ins, p, owner) + { + PtrFieldGet = ptrFieldGet; + PtrFieldSet = ptrFieldSet; + _nameContent.tooltip = $"IL2CPP Property (getPtr={IL2CPPCacheEntryHelper.SafeGetPtr(owner, ptrFieldGet)}, setPtr={IL2CPPCacheEntryHelper.SafeGetPtr(owner, ptrFieldSet)})\n\n{_nameContent.tooltip}"; + } + /// + public IL2CPPPropertyCacheEntry(object ins, PropertyInfo p, Type owner, FieldInfo ptrFieldGet, FieldInfo ptrFieldSet, ICacheEntry parent) : base(ins, p, owner, parent) + { + PtrFieldGet = ptrFieldGet; + PtrFieldSet = ptrFieldSet; + _nameContent.tooltip = $"IL2CPP Property (getPtr={IL2CPPCacheEntryHelper.SafeGetPtr(owner, ptrFieldGet)}, setPtr={IL2CPPCacheEntryHelper.SafeGetPtr(owner, ptrFieldSet)})\n\n{_nameContent.tooltip}"; + } +} + +/// +public class IL2CPPMethodCacheEntry : MethodCacheEntry +{ + public FieldInfo PtrField { get; } + + /// + public IL2CPPMethodCacheEntry(object instance, MethodInfo methodInfo, Type owner, FieldInfo ptrField) : base(instance, methodInfo, owner) + { + PtrField = ptrField; + _nameContent.tooltip = $"IL2CPP Method (ptr={IL2CPPCacheEntryHelper.SafeGetPtr(owner, ptrField)})\n\n{_nameContent.tooltip}"; + } +} + +/// +/// TODO: This does nothing so far because events are not implemented in il2cpp interop (they show up as separate add/remove/raise methods). Maybe combine them back into events? +public class IL2CPPEventCacheEntry : EventCacheEntry +{ + public FieldInfo PtrFieldAdd { get; } + public FieldInfo PtrFieldRaise { get; } + public FieldInfo PtrFieldRemove { get; } + /// + public IL2CPPEventCacheEntry(object ins, EventInfo e, Type owner, FieldInfo ptrFieldAdd, FieldInfo ptrFieldRaise, FieldInfo ptrFieldRemove) : base(ins, e, owner) + { + PtrFieldAdd = ptrFieldAdd; + PtrFieldRaise = ptrFieldRaise; + PtrFieldRemove = ptrFieldRemove; + _nameContent.tooltip = $"IL2CPP Event (addPtr={IL2CPPCacheEntryHelper.SafeGetPtr(owner, ptrFieldAdd)}, raisePtr={IL2CPPCacheEntryHelper.SafeGetPtr(owner, ptrFieldRaise)}, removePtr={IL2CPPCacheEntryHelper.SafeGetPtr(owner, ptrFieldRemove)})\n\n{_nameContent.tooltip}"; + } +} + +#endif diff --git a/RuntimeUnityEditor.Core/Windows/Inspector/Inspector.cs b/RuntimeUnityEditor.Core/Windows/Inspector/Inspector.cs index f799d9b..ed24b35 100644 --- a/RuntimeUnityEditor.Core/Windows/Inspector/Inspector.cs +++ b/RuntimeUnityEditor.Core/Windows/Inspector/Inspector.cs @@ -2,6 +2,9 @@ using System.Collections.Generic; using System.Linq; using RuntimeUnityEditor.Core.Inspector.Entries; +#if IL2CPP +using RuntimeUnityEditor.Core.Inspector.IL2CPP; +#endif using RuntimeUnityEditor.Core.Utils; using RuntimeUnityEditor.Core.Utils.Abstractions; using UnityEngine; @@ -70,6 +73,7 @@ public string SearchString #if IL2CPP private bool _showNative = true; private bool _showManaged = true; + private readonly Color _il2CPPMemberColor = new(1f, 1f, 0.4f); #endif private bool _showDeclaredOnly; private bool _showTooltips = true; @@ -89,6 +93,10 @@ private void DrawVariableNameEnterButton(ICacheEntry field) var canEnterValue = field.CanEnterValue(); var val = field.GetValue(); +#if IL2CPP + if(IL2CPPCacheEntryHelper.IsIl2CppCacheEntry(field)) + GUI.color = _il2CPPMemberColor; +#endif if (GUILayout.Button(field.GetNameContent(), canEnterValue ? _alignedButtonStyle : _alignedButtonStyleUnclickable, _inspectorNameWidth)) { if (IMGUIUtils.IsMouseRightClick()) @@ -106,6 +114,7 @@ private void DrawVariableNameEnterButton(ICacheEntry field) } } } + GUI.color = Color.white; } /// @@ -197,7 +206,9 @@ protected override void DrawContents() _showMethods = GUILayout.Toggle(_showMethods, "Methods"); _showEvents = GUILayout.Toggle(_showEvents, "Events"); #if IL2CPP + GUI.color = _il2CPPMemberColor; _showNative = GUILayout.Toggle(_showNative, new GUIContent("Native", null, "Display members from the IL2CPP runtime (i.e. the game code).")); + GUI.color = Color.white; _showManaged = GUILayout.Toggle(_showManaged, new GUIContent("Managed", null, "Display members from the BepInEx's runtime (i.e. interop and plugin code).")); #endif _showDeclaredOnly = GUILayout.Toggle(_showDeclaredOnly, "Only declared"); @@ -393,7 +404,7 @@ private void DrawContentScrollView(InspectorTab tab) visibleFieldsQuery = visibleFieldsQuery.Where(x => { #if IL2CPP - if (IL2CPPFieldCacheEntry.IsIl2CppCacheEntry(x)) + if (IL2CPPCacheEntryHelper.IsIl2CppCacheEntry(x)) { if (!_showNative) return false; diff --git a/RuntimeUnityEditor.Core/Windows/Inspector/MemberCollector.cs b/RuntimeUnityEditor.Core/Windows/Inspector/MemberCollector.cs index cbd5a5e..d75bc7d 100644 --- a/RuntimeUnityEditor.Core/Windows/Inspector/MemberCollector.cs +++ b/RuntimeUnityEditor.Core/Windows/Inspector/MemberCollector.cs @@ -11,6 +11,9 @@ using System.Reflection; using System.Runtime.CompilerServices; using UnityEngine; +#if IL2CPP +using RuntimeUnityEditor.Core.Inspector.IL2CPP; +#endif namespace RuntimeUnityEditor.Core.Inspector { @@ -19,84 +22,6 @@ namespace RuntimeUnityEditor.Core.Inspector /// internal static class MemberCollector { -#if IL2CPP - private static readonly Dictionary> _ptrLookup = new(); - - private static Dictionary GetPtrLookupTable(Type type) - { - // todo some way to clean up old entries? - if (_ptrLookup.TryGetValue(type, out var value)) - return value; - - value = new Dictionary(); - - var staticFields = type.GetFields(BindingFlags.Static | BindingFlags.NonPublic).Where(x => x.IsInitOnly).ToList(); - - var fieldPtrs = staticFields.Where(x => x.Name.StartsWith("NativeFieldInfoPtr_")) - .Select(x => new { trimmed = x.Name.Substring("NativeFieldInfoPtr_".Length), ptrF = x }); - - var usedFields = new HashSet(); - - foreach (var fieldPtr in fieldPtrs) - { - var targetFieldName = fieldPtr.trimmed; - // Fields are props in il2cpp interop - var targetField = type.GetProperty(targetFieldName, AccessTools.all); - if (targetField != null) - { - value[targetField] = fieldPtr.ptrF; - - var getMethod = targetField.GetGetMethod(); - if (getMethod != null) usedFields.Add(getMethod); - var setMethod = targetField.GetSetMethod(); - if (setMethod != null) usedFields.Add(setMethod); - } - } - - foreach (var propertyInfo in type.GetAllProperties(true)) - { - // It's a field - if (value.ContainsKey(propertyInfo)) - continue; - - var getMethod = propertyInfo.GetGetMethod(true); - if (getMethod != null && getMethod.GetMethodBody() != null) - { - var ptr = Il2CppInterop.Common.Il2CppInteropUtils.GetIl2CppMethodInfoPointerFieldForGeneratedMethod(getMethod); - if (ptr != null) - value[getMethod] = ptr; - } - - var setMethod = propertyInfo.GetSetMethod(); - if (setMethod != null && setMethod.GetMethodBody() != null) - { - var ptr = Il2CppInterop.Common.Il2CppInteropUtils.GetIl2CppMethodInfoPointerFieldForGeneratedMethod(setMethod); - if (ptr != null) - value[setMethod] = ptr; - } - } - - foreach (var methodInfo in type.GetAllMethods(true)) - { - if (value.ContainsKey(methodInfo) || usedFields.Contains(methodInfo)) - continue; - - if (methodInfo.GetMethodBody() != null) - { - var ptr = Il2CppInterop.Common.Il2CppInteropUtils.GetIl2CppMethodInfoPointerFieldForGeneratedMethod(methodInfo); - if (ptr != null) - value[methodInfo] = ptr; - } - } - - _ptrLookup[type] = value; - return value; - } -#endif - - //TODO - // - Handle il2cpp-side fields by making them act like normal fields - // - give il2cpp-side fields and methods a special color or something in inspector, show pointer address in tooltip public static ICollection CollectAllMembers(InstanceStackEntry entry) { var fieldCache = new List(); @@ -218,7 +143,7 @@ CallbackCacheEntry GetExportTexEntry(Texture texture) var isRenderer = objectToOpen is Renderer; #if IL2CPP var isIl2cppType = objectToOpen is Il2CppSystem.Type; - var il2cppLookup = GetPtrLookupTable(type); + var il2cppLookup = IL2CPPCacheEntryHelper.GetPtrLookupTable(type); #endif fieldCache.AddRange(type.GetAllProperties(false) .Where(f => !f.IsDefined(typeof(CompilerGeneratedAttribute), false)) @@ -242,7 +167,7 @@ CallbackCacheEntry GetExportTexEntry(Texture texture) return new CallbackCacheEntry(nameof(Il2CppSystem.Type.DeclaringMethod), "Skipped evaluation, click to enter (DANGER, MAY HARD CRASH)", () => ((Il2CppSystem.Type)objectToOpen).DeclaringMethod); } - if (IL2CPPFieldCacheEntry.TryGetIl2CppCacheEntry(objectToOpen, type, p, il2cppLookup, out var result)) + if (IL2CPPCacheEntryHelper.TryGetIl2CppCacheEntry(objectToOpen, type, p, il2cppLookup, out var result)) return result; #endif @@ -254,7 +179,7 @@ CallbackCacheEntry GetExportTexEntry(Texture texture) .Select(p => { #if IL2CPP - if (IL2CPPFieldCacheEntry.TryGetIl2CppCacheEntry(objectToOpen, type, p, il2cppLookup, out var result)) + if (IL2CPPCacheEntryHelper.TryGetIl2CppCacheEntry(objectToOpen, type, p, il2cppLookup, out var result)) return result; #endif return new EventCacheEntry(objectToOpen, p, type); @@ -287,7 +212,7 @@ public static ICollection CollectStaticMembers(StaticStackEntry ent private static ICollection CacheStaticMembersHelper(Type type) { #if IL2CPP - var il2cppLookup = GetPtrLookupTable(type); + var il2cppLookup = IL2CPPCacheEntryHelper.GetPtrLookupTable(type); #endif var fieldCache = new List(); fieldCache.AddRange(type.GetAllFields(true) @@ -302,7 +227,7 @@ private static ICollection CacheStaticMembersHelper(Type type) .Select(p => { #if IL2CPP - if (IL2CPPFieldCacheEntry.TryGetIl2CppCacheEntry(null, type, p, il2cppLookup, out var result)) + if (IL2CPPCacheEntryHelper.TryGetIl2CppCacheEntry(null, type, p, il2cppLookup, out var result)) return result; #endif return (ICacheEntry)new PropertyCacheEntry(null, p, type); @@ -313,7 +238,7 @@ private static ICollection CacheStaticMembersHelper(Type type) .Select(p => { #if IL2CPP - if (IL2CPPFieldCacheEntry.TryGetIl2CppCacheEntry(null, type, p, il2cppLookup, out var result)) + if (IL2CPPCacheEntryHelper.TryGetIl2CppCacheEntry(null, type, p, il2cppLookup, out var result)) return result; #endif return (ICacheEntry)new EventCacheEntry(null, p, type); @@ -326,10 +251,11 @@ private static ICollection CacheStaticMembersHelper(Type type) private static IEnumerable MethodsToCacheEntries(object instance, Type ownerType, IEnumerable methodsToCheck) { #if IL2CPP - var il2cppLookup = GetPtrLookupTable(ownerType); + var il2cppLookup = IL2CPPCacheEntryHelper.GetPtrLookupTable(ownerType); #endif var cacheItems = methodsToCheck -#if IL2CPP // TODO: Events are not implemented in il2cpp interop, they show up as separate add/remove/raise methods +#if IL2CPP + // TODO: Events are not implemented in il2cpp interop, they show up as separate add/remove/raise methods .Where(x => !x.IsConstructor && (!x.IsSpecialName || x.Name.StartsWith("add_") || x.Name.StartsWith("raise_") || x.Name.StartsWith("remove_"))) #else .Where(x => !x.IsConstructor && !x.IsSpecialName) @@ -348,149 +274,4 @@ private static IEnumerable MethodsToCacheEntries(object instance, T } } -#if IL2CPP - /// - public class IL2CPPFieldCacheEntry : PropertyCacheEntry - { - public FieldInfo PtrField { get; } - - /// - public IL2CPPFieldCacheEntry(object ins, PropertyInfo p, Type owner, FieldInfo ptrField) : base(ins, p, owner) - { - PtrField = ptrField; - _nameContent.text = $"C-F/{_nameContent.text}"; - _nameContent.tooltip = $"IL2CPP Field (ptr={SafeGetPtr(owner, ptrField)})\n\n{_nameContent.tooltip}"; - } - - /// - public IL2CPPFieldCacheEntry(object ins, PropertyInfo p, Type owner, FieldInfo ptrField, ICacheEntry parent) : base(ins, p, owner, parent) - { - PtrField = ptrField; - _nameContent.text = $"C-F/{_nameContent.text}"; - _nameContent.tooltip = $"IL2CPP Field (ptr={SafeGetPtr(owner, ptrField)})\n\n{_nameContent.tooltip}"; - } - - - internal static bool TryGetIl2CppCacheEntry(object instance, Type type, EventInfo p, Dictionary lookup, out ICacheEntry result) - { - FieldInfo ptrAdd = null; - FieldInfo ptrRaise = null; - FieldInfo ptrRemove = null; - var addMethod = p.GetAddMethod(true); - if (addMethod != null) lookup.TryGetValue(addMethod, out ptrAdd); - var raiseMethod = p.GetRaiseMethod(true); - if (raiseMethod != null) lookup.TryGetValue(raiseMethod, out ptrRaise); - var removeMethod = p.GetRemoveMethod(true); - if (removeMethod != null) lookup.TryGetValue(removeMethod, out ptrRemove); - if (ptrAdd != null || ptrRaise != null || ptrRemove != null) - { - result = (ICacheEntry)new IL2CPPEventCacheEntry(instance, p, type, ptrAdd, ptrRaise, ptrRemove); - return true; - } - - result = null; - return false; - } - - internal static bool TryGetIl2CppCacheEntry(object instance, Type type, PropertyInfo p, Dictionary lookup, out ICacheEntry result) - { - if (lookup.TryGetValue(p, out var ptr)) - { - result = (ICacheEntry)new IL2CPPFieldCacheEntry(instance, p, type, ptr); - return true; - } - - FieldInfo ptrGet = null; - FieldInfo ptrSet = null; - var getMethod = p.GetGetMethod(true); - if (getMethod != null) lookup.TryGetValue(getMethod, out ptrGet); - var setMethod = p.GetSetMethod(true); - if (setMethod != null) lookup.TryGetValue(setMethod, out ptrSet); - if (ptrGet != null || ptrSet != null) - { - result = (ICacheEntry)new IL2CPPPropertyCacheEntry(instance, p, type, ptrGet, ptrSet); - return true; - } - - result = null; - return false; - } - - internal static object SafeGetPtr(Type owner, FieldInfo ptrField) - { - if (ptrField == null) return "null"; - if (owner.ContainsGenericParameters) - return "???"; - try - { - return ptrField.GetValue(null); - } - catch - { - return "error"; - } - } - - internal static bool IsIl2CppCacheEntry(ICacheEntry entry) - { - return entry is IL2CPPFieldCacheEntry || entry is IL2CPPPropertyCacheEntry || entry is IL2CPPMethodCacheEntry || entry is IL2CPPEventCacheEntry; - } - } - - /// - public class IL2CPPPropertyCacheEntry : PropertyCacheEntry - { - public FieldInfo PtrFieldGet { get; } - public FieldInfo PtrFieldSet { get; } - - /// - public IL2CPPPropertyCacheEntry(object ins, PropertyInfo p, Type owner, FieldInfo ptrFieldGet, FieldInfo ptrFieldSet) : base(ins, p, owner) - { - PtrFieldGet = ptrFieldGet; - PtrFieldSet = ptrFieldSet; - _nameContent.text = $"C-P/{_nameContent.text}"; - _nameContent.tooltip = $"IL2CPP Property (getPtr={IL2CPPFieldCacheEntry.SafeGetPtr(owner, ptrFieldGet)}, setPtr={IL2CPPFieldCacheEntry.SafeGetPtr(owner, ptrFieldSet)})\n\n{_nameContent.tooltip}"; - } - /// - public IL2CPPPropertyCacheEntry(object ins, PropertyInfo p, Type owner, FieldInfo ptrFieldGet, FieldInfo ptrFieldSet, ICacheEntry parent) : base(ins, p, owner, parent) - { - PtrFieldGet = ptrFieldGet; - PtrFieldSet = ptrFieldSet; - _nameContent.text = $"C-P/{_nameContent.text}"; - _nameContent.tooltip = $"IL2CPP Property (getPtr={IL2CPPFieldCacheEntry.SafeGetPtr(owner, ptrFieldGet)}, setPtr={IL2CPPFieldCacheEntry.SafeGetPtr(owner, ptrFieldSet)})\n\n{_nameContent.tooltip}"; - } - } - - /// - public class IL2CPPMethodCacheEntry : MethodCacheEntry - { - public FieldInfo PtrField { get; } - - /// - public IL2CPPMethodCacheEntry(object instance, MethodInfo methodInfo, Type owner, FieldInfo ptrField) : base(instance, methodInfo, owner) - { - PtrField = ptrField; - _nameContent.text = $"C-M/{_nameContent.text}"; - _nameContent.tooltip = $"IL2CPP Method (ptr={IL2CPPFieldCacheEntry.SafeGetPtr(owner, ptrField)})\n\n{_nameContent.tooltip}"; - } - } - - /// - /// TODO: This does nothing so far because events are not implemented in il2cpp interop (they show up as separate add/remove/raise methods). Maybe combine them back into events? - public class IL2CPPEventCacheEntry : EventCacheEntry - { - public FieldInfo PtrFieldAdd { get; } - public FieldInfo PtrFieldRaise { get; } - public FieldInfo PtrFieldRemove { get; } - /// - public IL2CPPEventCacheEntry(object ins, EventInfo e, Type owner, FieldInfo ptrFieldAdd, FieldInfo ptrFieldRaise, FieldInfo ptrFieldRemove) : base(ins, e, owner) - { - PtrFieldAdd = ptrFieldAdd; - PtrFieldRaise = ptrFieldRaise; - PtrFieldRemove = ptrFieldRemove; - _nameContent.text = $"C-E/{_nameContent.text}"; - _nameContent.tooltip = $"IL2CPP Event (addPtr={IL2CPPFieldCacheEntry.SafeGetPtr(owner, ptrFieldAdd)}, raisePtr={IL2CPPFieldCacheEntry.SafeGetPtr(owner, ptrFieldRaise)}, removePtr={IL2CPPFieldCacheEntry.SafeGetPtr(owner, ptrFieldRemove)})\n\n{_nameContent.tooltip}"; - } - } -#endif -} +} \ No newline at end of file From 68abb5dffdd24c7f89df838ad4fffb05e3a051a3 Mon Sep 17 00:00:00 2001 From: ManlyMarco <39247311+ManlyMarco@users.noreply.github.com> Date: Fri, 3 Oct 2025 02:09:31 +0200 Subject: [PATCH 5/6] Lighter --- RuntimeUnityEditor.Core/Windows/Inspector/Inspector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RuntimeUnityEditor.Core/Windows/Inspector/Inspector.cs b/RuntimeUnityEditor.Core/Windows/Inspector/Inspector.cs index ed24b35..4bc987e 100644 --- a/RuntimeUnityEditor.Core/Windows/Inspector/Inspector.cs +++ b/RuntimeUnityEditor.Core/Windows/Inspector/Inspector.cs @@ -73,7 +73,7 @@ public string SearchString #if IL2CPP private bool _showNative = true; private bool _showManaged = true; - private readonly Color _il2CPPMemberColor = new(1f, 1f, 0.4f); + private readonly Color _il2CPPMemberColor = new(1f, 1f, 0.6f); #endif private bool _showDeclaredOnly; private bool _showTooltips = true; From 2a8a7e5d0264eb108b77b4bdf4438bccde0b940e Mon Sep 17 00:00:00 2001 From: ManlyMarco <39247311+ManlyMarco@users.noreply.github.com> Date: Fri, 3 Oct 2025 02:20:03 +0200 Subject: [PATCH 6/6] Update RuntimeUnityEditor.Core/Windows/Inspector/Inspector.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- RuntimeUnityEditor.Core/Windows/Inspector/Inspector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RuntimeUnityEditor.Core/Windows/Inspector/Inspector.cs b/RuntimeUnityEditor.Core/Windows/Inspector/Inspector.cs index 4bc987e..61675d4 100644 --- a/RuntimeUnityEditor.Core/Windows/Inspector/Inspector.cs +++ b/RuntimeUnityEditor.Core/Windows/Inspector/Inspector.cs @@ -94,7 +94,7 @@ private void DrawVariableNameEnterButton(ICacheEntry field) var canEnterValue = field.CanEnterValue(); var val = field.GetValue(); #if IL2CPP - if(IL2CPPCacheEntryHelper.IsIl2CppCacheEntry(field)) + if (IL2CPPCacheEntryHelper.IsIl2CppCacheEntry(field)) GUI.color = _il2CPPMemberColor; #endif if (GUILayout.Button(field.GetNameContent(), canEnterValue ? _alignedButtonStyle : _alignedButtonStyleUnclickable, _inspectorNameWidth))