// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. //#define DEBUG_OBJECT_SNAPSHOT_COMPARISION using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using FlaxEditor.Scripting; using FlaxEngine; using FlaxEngine.Json; using FlaxEngine.Utilities; namespace FlaxEditor.Utilities { /// /// Helper class to gather object snapshots and compare them using reflection. /// public sealed class ObjectSnapshot { private readonly List _members; private readonly List _values; /// /// The object type. /// public readonly ScriptType ObjectType; private ObjectSnapshot(ScriptType type, List values, List members) { ObjectType = type; _values = values; _members = members; } private struct TypeEntry { public MemberInfoPath Path; public int SubEntriesCount; public TypeEntry(MemberInfoPath membersPath, int subEntriesCount) { Path = membersPath; SubEntriesCount = subEntriesCount; } } private static readonly Type[] AttributesIgnoreList = { typeof(NonSerializedAttribute), typeof(NoSerializeAttribute), typeof(NoUndoAttribute), }; private static void GetEntries(MemberInfoPath.Entry member, Stack membersPath, List result, List values, Stack refStack, ScriptType memberType, object memberValue) { membersPath.Push(member); var path = new MemberInfoPath(membersPath); var beforeCount = result.Count; // Check if record object sub members (skip Flax objects) // It's used for ref types but not null types and with checking cyclic references if ((memberType.IsClass || memberType.IsArray || typeof(IList).IsAssignableFrom(memberType.Type) || typeof(IDictionary).IsAssignableFrom(memberType.Type)) && memberValue != null && !refStack.Contains(memberValue)) { if (memberType.IsArray && !ScriptType.FlaxObject.IsAssignableFrom(memberType.GetElementType())) { // Array var array = (Array)memberValue; var elementType = memberType.GetElementType(); var length = array.Length; refStack.Push(memberValue); for (int i = 0; i < length; i++) { var value = array.GetValue(i); GetEntries(new MemberInfoPath.Entry(member.Member, i), membersPath, result, values, refStack, elementType, value); } refStack.Pop(); } else if (typeof(IList).IsAssignableFrom(memberType.Type) && !ScriptType.FlaxObject.IsAssignableFrom(memberType.GetElementType())) { // List var list = (IList)memberValue; var genericArguments = memberType.GetGenericArguments(); var elementType = new ScriptType(genericArguments[0]); var count = list.Count; refStack.Push(memberValue); for (int i = 0; i < count; i++) { var value = list[i]; GetEntries(new MemberInfoPath.Entry(member.Member, i), membersPath, result, values, refStack, elementType, value); } refStack.Pop(); } else if (typeof(IDictionary).IsAssignableFrom(memberType.Type)) { // Dictionary var dictionary = (IDictionary)memberValue; var genericArguments = memberType.GetGenericArguments(); var valueType = new ScriptType(genericArguments[1]); foreach (var key in dictionary.Keys) { var value = dictionary[key]; GetEntries(new MemberInfoPath.Entry(member.Member, key), membersPath, result, values, refStack, valueType, value); } } else if (memberType.IsClass && !ScriptType.FlaxObject.IsAssignableFrom(memberType)) { // Object refStack.Push(memberValue); GetEntries(memberValue, membersPath, memberType, result, values, refStack); refStack.Pop(); } } var afterCount = result.Count; result.Add(new TypeEntry(path, afterCount - beforeCount)); var memberValueType = TypeUtils.GetObjectType(memberValue); if (memberValue != null && memberValueType.IsStructure) { var json = JsonSerializer.Serialize(memberValue); var clone = JsonSerializer.Deserialize(json, memberValueType.Type); values.Add(clone); } else { values.Add(memberValue); } membersPath.Pop(); } private static ScriptType GetMemberType(ScriptType memberType, object memberValue) { // Try using the type of the actual value (eg. Control property that has value set to Button - capture all button properties, not just control properties) if (memberValue != null) { return TypeUtils.GetObjectType(memberValue); } return memberType; } private static void GetEntries(object instance, Stack membersPath, ScriptType type, List result, List values, Stack refStack) { // Note: this should match Flax serialization rules and attributes (see ExtendedDefaultContractResolver) var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); var properties = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); for (int i = 0; i < fields.Length; i++) { var f = fields[i]; var attributes = f.GetAttributes(); // Serialize non-public fields only with a proper attribute if (!f.IsPublic && !attributes.Any(x => x is SerializeAttribute)) continue; // Check if has attribute to skip serialization bool noSerialize = false; foreach (var attribute in attributes) { if (AttributesIgnoreList.Contains(attribute.GetType())) { noSerialize = true; break; } } if (noSerialize) continue; var memberValue = f.GetValue(instance); GetEntries(new MemberInfoPath.Entry(f), membersPath, result, values, refStack, GetMemberType(f.ValueType, memberValue), memberValue); } for (int i = 0; i < properties.Length; i++) { var p = properties[i]; // Serialize only properties with read/write if (!p.HasGet || !p.HasSet) continue; var attributes = p.GetAttributes(); // Serialize non-public properties only with a proper attribute if (!p.IsPublic && !attributes.Any(x => x is SerializeAttribute)) continue; // Check if has attribute to skip serialization bool noSerialize = false; foreach (var attribute in attributes) { if (AttributesIgnoreList.Contains(attribute.GetType())) { noSerialize = true; break; } } if (noSerialize) continue; var memberValue = p.GetValue(instance); GetEntries(new MemberInfoPath.Entry(p), membersPath, result, values, refStack, GetMemberType(p.ValueType, memberValue), memberValue); } } private static List GetMembers(object instance, ScriptType type, out List values) { values = new List(); var result = new List(); var membersPath = new Stack(8); var refsStack = new Stack(8); refsStack.Push(instance); GetEntries(instance, membersPath, type, result, values, refsStack); return result; } /// /// Captures the snapshot of the object. /// /// The object to capture. /// The object snapshot. public static ObjectSnapshot CaptureSnapshot(object obj) { if (obj == null) throw new ArgumentNullException(); var type = TypeUtils.GetObjectType(obj); var members = GetMembers(obj, type, out var values); #if DEBUG_OBJECT_SNAPSHOT_COMPARISION Debug.Logger.LogHandler.LogWrite(LogType.Warning, "-------------- CaptureSnapshot: " + type + " --------------"); for (int i = 0; i < values.Count; i++) Debug.Logger.LogHandler.LogWrite(LogType.Warning, members[i].Path.Path + " = " + (values[i] ?? "")); #endif return new ObjectSnapshot(type, values, members); } /// /// Gets a list of MemberComparison values that represent the fields and/or properties /// that differ between the given object and the captured state. /// /// The object to compare. /// The collection of modified properties. public List Compare(object obj) { if (obj == null) throw new ArgumentNullException(); if (ObjectType != TypeUtils.GetObjectType(obj)) throw new ArgumentException("Given object must be the same type as captured object."); var list = new List(); #if DEBUG_OBJECT_SNAPSHOT_COMPARISION Debug.Logger.LogHandler.LogWrite(LogType.Warning, "-------------- Comparison --------------"); #endif for (int i = _members.Count - 1; i >= 0; i--) { var m = _members[i]; var xValue = _values[i]; var yValue = m.Path.GetLastValue(obj); #if DEBUG_OBJECT_SNAPSHOT_COMPARISION Debug.Logger.LogHandler.LogWrite(LogType.Warning, "Compare: " + (new MemberComparison(m.Path, xValue, yValue))); #endif if (!JsonSerializer.ValueEquals(xValue, yValue)) { #if DEBUG_OBJECT_SNAPSHOT_COMPARISION Debug.Logger.LogHandler.LogWrite(LogType.Warning, "Diff on: " + (new MemberComparison(m.Path, xValue, yValue))); #endif list.Add(new MemberComparison(m.Path, xValue, yValue)); // Value changed, skip sub entries compare i -= m.SubEntriesCount; } } #if DEBUG_OBJECT_SNAPSHOT_COMPARISION Debug.Logger.LogHandler.LogWrite(LogType.Warning, "-------------- End --------------"); #endif return list; } } }