using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Reflection; using UnityEngine; using UnityEngine.Analytics; using UnityEngine.Pool; namespace UnityEditor.Rendering { /// /// Set of utilities for analytics /// public static class AnalyticsUtils { const string k_VendorKey = "unity.srp"; internal static void SendData(IAnalytic analytic) { EditorAnalytics.SendAnalytic(analytic); } /// /// Gets a list of the serializable fields of the given type /// /// The type to get fields that are serialized. /// If obsolete fields are taken into account /// The collection of that are serialized for this type public static IEnumerable GetSerializableFields(this Type type, bool removeObsolete = false) { var members = type.GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic); if (type.BaseType != null && type.BaseType != typeof(object)) { foreach (FieldInfo field in type.BaseType.GetSerializableFields()) { yield return field; } } foreach (var member in members) { if (member.MemberType != MemberTypes.Field && member.MemberType != MemberTypes.Property) { continue; } if (member.DeclaringType != type || member is not FieldInfo field) { continue; } if (removeObsolete && member.GetCustomAttribute() != null) continue; if (field.IsPublic) { if (member.GetCustomAttribute() != null) continue; yield return field; } else { if (member.GetCustomAttribute() != null) yield return field; } } } static bool AreArraysDifferent(IList a, IList b) { if ((a == null) && (b == null)) return false; if ((a == null) ^ (b == null)) return true; if (a.Count != b.Count) return true; for (int i = 0; i < a.Count; i++) { if (!a[i].Equals(b[i])) return true; } return false; } static string DumpValues(this IList list) { using (ListPool.Get(out var tempList)) { for (int i = 0; i < list.Count; i++) { tempList.Add(list[i] != null ? list[i].ToString() : "null"); } var arrayValues = string.Join(",", tempList); return $"[{arrayValues}]"; } } static Dictionary DumpValues(Type type, object current) { var diff = new Dictionary(); foreach (var field in type.GetSerializableFields(removeObsolete: true)) { var t = field.FieldType; try { if (typeof(ScriptableObject).IsAssignableFrom(t)) continue; var valueCurrent = current != null ? field.GetValue(current) : null; if (t == typeof(string)) { var stringCurrent = (string)valueCurrent; diff[field.Name] = stringCurrent; } else if (t.IsPrimitive || t.IsEnum) { diff[field.Name] = ConvertPrimitiveWithInvariants(valueCurrent); } else if (t.IsArray && valueCurrent is IList valueCurrentList) { diff[field.Name] = valueCurrentList.DumpValues(); } else if (t.IsClass || t.IsValueType) { if (valueCurrent is IEnumerable ea) continue; // List not supported var subDiff = DumpValues(t, valueCurrent); foreach (var d in subDiff) { diff[field.Name + "." + d.Key] = d.Value; } } } catch (Exception ex) { Debug.LogError($"Exception found while parsing {field}, {ex}"); } } return diff; } static Dictionary GetDiffAsDictionary(Type type, object current, object defaults) { var diff = new Dictionary(); foreach (var field in type.GetSerializableFields()) { var fieldType = field.FieldType; if (!IsFieldIgnored(fieldType)) AddDiff(current, defaults, field, fieldType, diff); } return diff; } private static void AddDiff(object current, object defaults, FieldInfo field, Type fieldType, Dictionary diff) { try { var valueCurrent = current != null ? field.GetValue(current) : null; var valueDefault = defaults != null ? field.GetValue(defaults) : null; AddIfDifferent(field, fieldType, diff, valueCurrent, valueDefault); } catch (Exception ex) { Debug.LogError($"Exception found while parsing {field}, {ex}"); } } private static void AddIfDifferent(FieldInfo field, Type fieldType, Dictionary diff, object valueCurrent, object valueDefault) { if (!AreValuesEqual(fieldType, valueCurrent, valueDefault)) { if (IsComplexType(fieldType)) { var subDiff = GetDiffAsDictionary(fieldType, valueCurrent, valueDefault); foreach (var d in subDiff) { diff[$"{field.Name}.{d.Key}"] = d.Value; } } else { diff[field.Name] = ConvertValueToString(valueCurrent); } } } static bool IsFieldIgnored(Type fieldType) { return fieldType.GetCustomAttribute() != null || typeof(ScriptableObject).IsAssignableFrom(fieldType); } internal static bool AreValuesEqual(Type fieldType, object valueCurrent, object valueDefault) { if (fieldType == typeof(string)) return (string)valueCurrent == (string)valueDefault; if (fieldType.IsPrimitive || fieldType.IsEnum) return valueCurrent.Equals(valueDefault); if (fieldType.IsArray && valueCurrent is IList currentList) return !AreArraysDifferent(currentList, valueDefault as IList); if (valueCurrent == null && valueDefault == null) return true; return valueDefault?.Equals(valueCurrent) ?? valueCurrent?.Equals(null) ?? false; } internal static bool IsComplexType(Type fieldType) { // Primitive types and enums are not considered complex if (fieldType.IsPrimitive || fieldType.IsEnum) return false; // String is considered a primitive type for our purposes if (fieldType == typeof(string)) return false; // Arrays can be converted to string easy without sub-elements if (fieldType.IsArray) return false; // Value types (structs) that are not primitive are considered complex // Classes are considered complex types return fieldType.IsValueType || fieldType.IsClass; } static string ConvertValueToString(object value) { if (value == null) return null; if (value is IList list) return list.DumpValues(); return ConvertPrimitiveWithInvariants(value); } static string ConvertPrimitiveWithInvariants(object obj) { if (obj is IConvertible convertible) return convertible.ToString(CultureInfo.InvariantCulture); return obj.ToString(); } static string[] ToStringArray(Dictionary diff, string format = null) { var changedSettings = new string[diff.Count]; if (string.IsNullOrEmpty(format)) format = @"{{""{0}"":""{1}""}}"; int i = 0; foreach (var d in diff) changedSettings[i++] = string.Format(format, d.Key, d.Value); return changedSettings; } private static string[] EnumerableToNestedColumn([DisallowNull] this IEnumerable collection) { using (ListPool.Get(out var tmp)) { foreach (var element in collection) { string[] elementColumns = ToStringArray(DumpValues(element.GetType(), element), @"""{0}"":""{1}"""); tmp.Add("{" + string.Join(", ", elementColumns) + "}"); } return tmp.ToArray(); } } private static string[] ToNestedColumnSimplify([DisallowNull] this T current) where T : new() { var type = current.GetType(); if (typeof(UnityEngine.Object).IsAssignableFrom(typeof(T))) { var instance = ScriptableObject.CreateInstance(type); ToStringArray(GetDiffAsDictionary(type, current, instance)); ScriptableObject.DestroyImmediate(instance); } return ToStringArray(GetDiffAsDictionary(type, current, new T())); } /// /// Obtains the Serialized fields and values in form of nested columns for BigQuery /// https://cloud.google.com/bigquery/docs/nested-repeated /// /// The given type /// The current object to obtain the fields and values. /// If a comparison against the default value must be done. /// The nested columns in form of {key.nestedKey : value} /// Throws an exception if current parameter is null. public static string[] ToNestedColumn([DisallowNull] this T current, bool compareAndSimplifyWithDefault = false) where T : new() { if (current == null) throw new ArgumentNullException(nameof(current)); if (current is IEnumerable currentAsEnumerable) return EnumerableToNestedColumn(currentAsEnumerable); if (compareAndSimplifyWithDefault) return ToNestedColumnSimplify(current); return ToStringArray(DumpValues(current.GetType(), current)); } /// /// Obtains the Serialized fields and values in form of nested columns for BigQuery /// https://cloud.google.com/bigquery/docs/nested-repeated /// /// The given type /// The current object to obtain the fields and values. /// The default instance to compare values /// The nested columns in form of {key.nestedKey : value} /// Throws an exception if the current or defaultInstance parameters are null. public static string[] ToNestedColumn([DisallowNull] this T current, T defaultInstance) { if (current == null) throw new ArgumentNullException(nameof(current)); if (defaultInstance == null) throw new ArgumentNullException(nameof(defaultInstance)); var type = current.GetType(); Dictionary diff = GetDiffAsDictionary(type, current, defaultInstance); return ToStringArray(diff); } /// /// Obtains the Serialized fields and values in form of nested columns for BigQuery /// https://cloud.google.com/bigquery/docs/nested-repeated /// /// The given type /// The current object to obtain the fields and values. /// The default object /// If a comparison against the default value must be done. /// The nested columns in form of {key.nestedKey : value} /// Throws an exception if the current parameter is null. public static string[] ToNestedColumnWithDefault([DisallowNull] this T current, [DisallowNull] T defaultObject, bool compareAndSimplifyWithDefault = false) { if (current == null) throw new ArgumentNullException(nameof(current)); var type = current.GetType(); Dictionary diff = (compareAndSimplifyWithDefault) ? GetDiffAsDictionary(type, current, defaultObject) : DumpValues(type, current); return ToStringArray(diff); } } }