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 t = field.FieldType; try { if (t.GetCustomAttribute() != null || typeof(ScriptableObject).IsAssignableFrom(t)) continue; var valueCurrent = current != null ? field.GetValue(current) : null; var valueDefault = defaults != null ? field.GetValue(defaults) : null; if (t == typeof(string)) { var stringCurrent = (string)valueCurrent; var stringDefault = (string)valueDefault; if (stringCurrent != stringDefault) { diff[field.Name] = stringCurrent; } } else if (t.IsPrimitive || t.IsEnum) { if (!valueCurrent.Equals(valueDefault)) diff[field.Name] = ConvertPrimitiveWithInvariants(valueCurrent); } else if (t.IsArray && valueCurrent is IList valueCurrentList) { if (AreArraysDifferent(valueCurrentList, valueDefault as IList)) diff[field.Name] = valueCurrentList.DumpValues(); } else if (t.IsClass || t.IsValueType) { if (valueCurrent is IEnumerable ea) continue; // List not supported var subDiff = GetDiffAsDictionary(t, valueCurrent, valueDefault); 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 string ConvertPrimitiveWithInvariants(object obj) { if (obj is IConvertible convertible) return convertible.ToString(CultureInfo.InvariantCulture); return obj.ToString(); } static string[] ToStringArray(Dictionary diff) { var changedSettings = new string[diff.Count]; int i = 0; foreach (var d in diff) changedSettings[i++] = $@"{{""{d.Key}"":""{d.Value}""}}"; return changedSettings; } /// /// 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)); var type = current.GetType(); Dictionary diff; if (compareAndSimplifyWithDefault) { if (typeof(UnityEngine.Object).IsAssignableFrom(typeof(T))) { var instance = ScriptableObject.CreateInstance(type); diff = GetDiffAsDictionary(type, current, instance); ScriptableObject.DestroyImmediate(instance); } else { diff = GetDiffAsDictionary(type, current, new T()); } } else { diff = DumpValues(type, current); } 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 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); } } }