using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using UnityEngine; using UnityExplorer.UI.Inspectors.CacheObject.Views; using UnityExplorer.UI.Utility; namespace UnityExplorer.UI.Inspectors.CacheObject { // TODO some of this can be reused for CacheEnumerated / CacheKVP as well, just doing members for now. // Will put shared stuff in CacheObjectBase. public abstract class CacheMember : CacheObjectBase { public ReflectionInspector ParentInspector { get; internal set; } public bool AutoUpdateWanted { get; internal set; } public Type DeclaringType { get; protected set; } public string NameForFiltering { get; protected set; } public override bool HasArguments => Arguments?.Length > 0; public ParameterInfo[] Arguments { get; protected set; } public bool Evaluating { get; protected set; } public virtual void SetInspectorOwner(ReflectionInspector inspector, MemberInfo member) { this.ParentInspector = inspector; this.NameLabelText = SignatureHighlighter.ParseFullSyntax(member.DeclaringType, false, member); this.NameForFiltering = $"{member.DeclaringType.Name}.{member.Name}"; } protected abstract void TryEvaluate(); protected abstract void TrySetValue(object value); /// /// Evaluate when first shown (if ShouldAutoEvaluate), or else when Evaluate button is clicked. /// public void Evaluate() { TryEvaluate(); if (!Value.IsNullOrDestroyed()) Value = Value.TryCast(); ProcessOnEvaluate(); } public override void SetUserValue(object value) { // TODO unbox string, cast, etc TrySetValue(value); Evaluate(); } protected override void SetValueState(CacheObjectCell cell, bool valueActive, bool valueRichText, Color valueColor, bool typeLabelActive, bool toggleActive, bool inputActive, bool applyActive, bool inspectActive, bool subContentActive) { base.SetValueState(cell, valueActive, valueRichText, valueColor, typeLabelActive, toggleActive, inputActive, applyActive, inspectActive, subContentActive); (cell as CacheMemberCell).UpdateToggle.gameObject.SetActive(ShouldAutoEvaluate); } protected override bool SetCellEvaluateState(CacheObjectCell objectcell) { var cell = objectcell as CacheMemberCell; cell.EvaluateHolder.SetActive(!ShouldAutoEvaluate); if (!ShouldAutoEvaluate) { cell.UpdateToggle.gameObject.SetActive(false); cell.EvaluateButton.Button.gameObject.SetActive(true); if (HasArguments) cell.EvaluateButton.ButtonText.text = $"Evaluate ({Arguments.Length})"; else cell.EvaluateButton.ButtonText.text = "Evaluate"; } else { cell.UpdateToggle.gameObject.SetActive(true); cell.UpdateToggle.isOn = AutoUpdateWanted; } if (State == ValueState.NotEvaluated && !ShouldAutoEvaluate) { // todo evaluate buttons etc SetValueState(cell, true, true, Color.white, false, false, false, false, false, false); return true; } if (State == ValueState.NotEvaluated) Evaluate(); return false; } #region Cache Member Util public static bool CanProcessArgs(ParameterInfo[] parameters) { foreach (var param in parameters) { var pType = param.ParameterType; if (pType.IsByRef && pType.HasElementType) pType = pType.GetElementType(); if (pType != null && (pType.IsPrimitive || pType == typeof(string))) continue; else return false; } return true; } public static List GetCacheMembers(object inspectorTarget, Type _type, ReflectionInspector _inspector) { var list = new List(); var cachedSigs = new HashSet(); var types = ReflectionUtility.GetAllBaseTypes(_type); var flags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static; if (!_inspector.StaticOnly) flags |= BindingFlags.Instance; var infos = new List(); foreach (var declaringType in types) { var target = inspectorTarget; if (!_inspector.StaticOnly) target = target.TryCast(declaringType); infos.Clear(); infos.AddRange(declaringType.GetProperties(flags)); infos.AddRange(declaringType.GetFields(flags)); infos.AddRange(declaringType.GetMethods(flags)); foreach (var member in infos) { if (member.DeclaringType != declaringType) continue; TryCacheMember(member, list, cachedSigs, declaringType, _inspector); } } var typeList = types.ToList(); var sorted = new List(); sorted.AddRange(list.Where(it => it is CacheProperty) .OrderBy(it => typeList.IndexOf(it.DeclaringType)) .ThenBy(it => it.NameForFiltering)); sorted.AddRange(list.Where(it => it is CacheField) .OrderBy(it => typeList.IndexOf(it.DeclaringType)) .ThenBy(it => it.NameForFiltering)); sorted.AddRange(list.Where(it => it is CacheMethod) .OrderBy(it => typeList.IndexOf(it.DeclaringType)) .ThenBy(it => it.NameForFiltering)); return sorted; } private static void TryCacheMember(MemberInfo member, List list, HashSet cachedSigs, Type declaringType, ReflectionInspector _inspector, bool ignoreMethodBlacklist = false) { try { var sig = GetSig(member); if (IsBlacklisted(sig)) return; //ExplorerCore.Log($"Trying to cache member {sig}..."); //ExplorerCore.Log(member.DeclaringType.FullName + "." + member.Name); CacheMember cached; Type returnType; switch (member.MemberType) { case MemberTypes.Method: { var mi = member as MethodInfo; if (!ignoreMethodBlacklist && IsBlacklisted(mi)) return; var args = mi.GetParameters(); if (!CanProcessArgs(args)) return; sig += AppendArgsToSig(args); if (cachedSigs.Contains(sig)) return; cached = new CacheMethod() { MethodInfo = mi }; returnType = mi.ReturnType; break; } case MemberTypes.Property: { var pi = member as PropertyInfo; var args = pi.GetIndexParameters(); if (!CanProcessArgs(args)) return; if (!pi.CanRead && pi.CanWrite) { // write-only property, cache the set method instead. var setMethod = pi.GetSetMethod(true); if (setMethod != null) TryCacheMember(setMethod, list, cachedSigs, declaringType, _inspector, true); return; } sig += AppendArgsToSig(args); if (cachedSigs.Contains(sig)) return; cached = new CacheProperty() { PropertyInfo = pi }; returnType = pi.PropertyType; break; } case MemberTypes.Field: { var fi = member as FieldInfo; cached = new CacheField() { FieldInfo = fi }; returnType = fi.FieldType; break; } default: return; } cachedSigs.Add(sig); //cached.Initialize(_inspector, declaringType, member, returnType); cached.Initialize(returnType); cached.SetInspectorOwner(_inspector, member); list.Add(cached); } catch (Exception e) { ExplorerCore.LogWarning($"Exception caching member {member.DeclaringType.FullName}.{member.Name}!"); ExplorerCore.Log(e.ToString()); } } internal static string GetSig(MemberInfo member) => $"{member.DeclaringType.Name}.{member.Name}"; internal static string AppendArgsToSig(ParameterInfo[] args) { string ret = " ("; foreach (var param in args) ret += $"{param.ParameterType.Name} {param.Name}, "; ret += ")"; return ret; } // Blacklists private static readonly HashSet bl_typeAndMember = new HashSet { // these cause a crash in IL2CPP #if CPP "Type.DeclaringMethod", "Rigidbody2D.Cast", "Collider2D.Cast", "Collider2D.Raycast", "Texture2D.SetPixelDataImpl", "Camera.CalculateProjectionMatrixFromPhysicalProperties", #endif // These were deprecated a long time ago, still show up in some games for some reason "MonoBehaviour.allowPrefabModeInPlayMode", "MonoBehaviour.runInEditMode", "Component.animation", "Component.audio", "Component.camera", "Component.collider", "Component.collider2D", "Component.constantForce", "Component.hingeJoint", "Component.light", "Component.networkView", "Component.particleSystem", "Component.renderer", "Component.rigidbody", "Component.rigidbody2D", }; private static readonly HashSet bl_methodNameStartsWith = new HashSet { // these are redundant "get_", "set_", }; internal static bool IsBlacklisted(string sig) => bl_typeAndMember.Any(it => sig.Contains(it)); internal static bool IsBlacklisted(MethodInfo method) => bl_methodNameStartsWith.Any(it => method.Name.StartsWith(it)); #endregion } }