diff --git a/RuntimeUnityEditor.Core/RuntimeUnityEditor.Core.projitems b/RuntimeUnityEditor.Core/RuntimeUnityEditor.Core.projitems index fe394b5..c5494fe 100644 --- a/RuntimeUnityEditor.Core/RuntimeUnityEditor.Core.projitems +++ b/RuntimeUnityEditor.Core/RuntimeUnityEditor.Core.projitems @@ -87,6 +87,8 @@ + + @@ -106,4 +108,7 @@ + + + \ No newline at end of file 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/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.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/Inspector.cs b/RuntimeUnityEditor.Core/Windows/Inspector/Inspector.cs index 32f3901..61675d4 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; @@ -68,7 +71,9 @@ public string SearchString private bool _showMethods = true; private bool _showEvents = true; #if IL2CPP - private bool _showNative; + private bool _showNative = true; + private bool _showManaged = true; + private readonly Color _il2CPPMemberColor = new(1f, 1f, 0.6f); #endif private bool _showDeclaredOnly; private bool _showTooltips = true; @@ -88,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()) @@ -105,6 +114,7 @@ private void DrawVariableNameEnterButton(ICacheEntry field) } } } + GUI.color = Color.white; } /// @@ -196,7 +206,10 @@ protected override void DrawContents() _showMethods = GUILayout.Toggle(_showMethods, "Methods"); _showEvents = GUILayout.Toggle(_showEvents, "Events"); #if IL2CPP - _showNative = GUILayout.Toggle(_showNative, "Native"); + 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"); @@ -390,14 +403,24 @@ private void DrawContentScrollView(InspectorTab tab) } visibleFieldsQuery = visibleFieldsQuery.Where(x => { +#if IL2CPP + if (IL2CPPCacheEntryHelper.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 new file mode 100644 index 0000000..d75bc7d --- /dev/null +++ b/RuntimeUnityEditor.Core/Windows/Inspector/MemberCollector.cs @@ -0,0 +1,277 @@ +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.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using UnityEngine; +#if IL2CPP +using RuntimeUnityEditor.Core.Inspector.IL2CPP; +#endif + +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(); + + 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())); +#else + fieldCache.Add(new ReadonlyCacheEntry("Child objects", castedObj.transform.CastToEnumerable().Select(x => x.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 => (ICacheEntry)new FieldCacheEntry(objectToOpen, f, type, parent))); + + var isRenderer = objectToOpen is Renderer; +#if IL2CPP + var isIl2cppType = objectToOpen is Il2CppSystem.Type; + var il2cppLookup = IL2CPPCacheEntryHelper.GetPtrLookupTable(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); + } + + if (IL2CPPCacheEntryHelper.TryGetIl2CppCacheEntry(objectToOpen, type, p, il2cppLookup, out var result)) + return result; +#endif + + return (ICacheEntry)new PropertyCacheEntry(objectToOpen, p, type, parent); + })); + + fieldCache.AddRange(type.GetAllEvents(false) + .Where(f => !f.IsDefined(typeof(CompilerGeneratedAttribute), false)) + .Select(p => + { +#if IL2CPP + if (IL2CPPCacheEntryHelper.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))); + + 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.ToString())); + } + + 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) + { +#if IL2CPP + var il2cppLookup = IL2CPPCacheEntryHelper.GetPtrLookupTable(type); +#endif + var fieldCache = new List(); + fieldCache.AddRange(type.GetAllFields(true) + .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 => + { +#if IL2CPP + if (IL2CPPCacheEntryHelper.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 => + { +#if IL2CPP + if (IL2CPPCacheEntryHelper.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; + } + + private static IEnumerable MethodsToCacheEntries(object instance, Type ownerType, IEnumerable methodsToCheck) + { +#if IL2CPP + 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 + .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 => + { +#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; + } + } + +} \ No newline at end of file