| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308 | // Serialization // Copyright 2023-2024 Kybernetik //#if UNITY_EDITORusing System;using System.Collections;using System.Collections.Generic;using System.Reflection;using System.Text;using UnityEditor;using UnityEngine;using Object = UnityEngine.Object;// Shared File Last Modified: 2023-08-16namespace Animancer.Editor// namespace InspectorGadgets.Editor// namespace UltEvents.Editor{    /// <summary>[Editor-Only] The possible states for a function in a <see cref="GenericMenu"/>.</summary>    public enum MenuFunctionState    {        /************************************************************************************************************************/        /// <summary>Displayed normally.</summary>        Normal,        /// <summary>Has a check mark next to it to show that it is selected.</summary>        Selected,        /// <summary>Greyed out and unusable.</summary>        Disabled,        /************************************************************************************************************************/    }    /// <summary>[Editor-Only] Various serialization utilities.</summary>    public static partial class Serialization    {        /************************************************************************************************************************/        #region Public Static API        /************************************************************************************************************************/        /// <summary>The text used in a <see cref="SerializedProperty.propertyPath"/> to denote array elements.</summary>        public const string            ArrayDataPrefix = ".Array.data[",            ArrayDataSuffix = "]";        /// <summary>Bindings for Public and Non-Public Instance members.</summary>        public const BindingFlags            InstanceBindings = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;        /************************************************************************************************************************/        /// <summary>Returns a user friendly version of the <see cref="SerializedProperty.propertyPath"/>.</summary>        public static string GetFriendlyPath(this SerializedProperty property)            => property.propertyPath.Replace(ArrayDataPrefix, "[");        /************************************************************************************************************************/        #region Get Value        /************************************************************************************************************************/        /// <summary>Gets the value of the specified <see cref="SerializedProperty"/>.</summary>        public static object GetValue(this SerializedProperty property, object targetObject)        {            if (property.hasMultipleDifferentValues &&                property.serializedObject.targetObject != targetObject as Object)            {                property = new SerializedObject(targetObject as Object).FindProperty(property.propertyPath);            }            return property.propertyType switch            {                SerializedPropertyType.Boolean => property.boolValue,                SerializedPropertyType.Float => property.floatValue,                SerializedPropertyType.String => property.stringValue,                SerializedPropertyType.Integer or                SerializedPropertyType.Character or                SerializedPropertyType.LayerMask or                SerializedPropertyType.ArraySize => property.intValue,                SerializedPropertyType.Vector2 => property.vector2Value,                SerializedPropertyType.Vector3 => property.vector3Value,                SerializedPropertyType.Vector4 => property.vector4Value,                SerializedPropertyType.Quaternion => property.quaternionValue,                SerializedPropertyType.Color => property.colorValue,                SerializedPropertyType.AnimationCurve => property.animationCurveValue,                SerializedPropertyType.Rect => property.rectValue,                SerializedPropertyType.Bounds => property.boundsValue,                SerializedPropertyType.Vector2Int => property.vector2IntValue,                SerializedPropertyType.Vector3Int => property.vector3IntValue,                SerializedPropertyType.RectInt => property.rectIntValue,                SerializedPropertyType.BoundsInt => property.boundsIntValue,                SerializedPropertyType.ObjectReference => property.objectReferenceValue,                SerializedPropertyType.ExposedReference => property.exposedReferenceValue,                SerializedPropertyType.FixedBufferSize => property.fixedBufferSize,                SerializedPropertyType.Gradient => property.GetGradientValue(),                SerializedPropertyType.ManagedReference => property.managedReferenceValue,                // Enum would be complex because enumValueIndex can't be cast directly.                _ => GetAccessor(property)?.GetValue(targetObject),            };        }        /************************************************************************************************************************/        /// <summary>Gets the value of the <see cref="SerializedProperty"/>.</summary>        public static object GetValue(this SerializedProperty property)            => GetValue(property, property.serializedObject.targetObject);        /// <summary>Gets the value of the <see cref="SerializedProperty"/>.</summary>        public static T GetValue<T>(this SerializedProperty property)            => (T)GetValue(property);        /// <summary>Gets the value of the <see cref="SerializedProperty"/>.</summary>        public static void GetValue<T>(this SerializedProperty property, out T value)            => value = (T)GetValue(property);        /************************************************************************************************************************/        /// <summary>Gets the value of the <see cref="SerializedProperty"/> for each of its target objects.</summary>        public static T[] GetValues<T>(this SerializedProperty property)        {            try            {                var targetObjects = property.serializedObject.targetObjects;                var values = new T[targetObjects.Length];                for (int i = 0; i < values.Length; i++)                {                    values[i] = (T)GetValue(property, targetObjects[i]);                }                return values;            }            catch            {                return null;            }        }        /************************************************************************************************************************/        /// <summary>Is the value of the `property` the same as the default serialized value for its type?</summary>        public static bool IsDefaultValueByType(SerializedProperty property)        {            if (property.hasMultipleDifferentValues)                return false;            switch (property.propertyType)            {                case SerializedPropertyType.Boolean: return property.boolValue == default;                case SerializedPropertyType.Float: return property.floatValue == default;                case SerializedPropertyType.String: return property.stringValue == "";                case SerializedPropertyType.Integer:                case SerializedPropertyType.Character:                case SerializedPropertyType.LayerMask:                case SerializedPropertyType.ArraySize:                    return property.intValue == default;                case SerializedPropertyType.Vector2: return property.vector2Value.Equals(default);                case SerializedPropertyType.Vector3: return property.vector3Value.Equals(default);                case SerializedPropertyType.Vector4: return property.vector4Value.Equals(default);                case SerializedPropertyType.Quaternion: return property.quaternionValue.Equals(default);                case SerializedPropertyType.Color: return property.colorValue.Equals(default);                case SerializedPropertyType.AnimationCurve: return property.animationCurveValue == default;                case SerializedPropertyType.Rect: return property.rectValue == default;                case SerializedPropertyType.Bounds: return property.boundsValue == default;                case SerializedPropertyType.Vector2Int: return property.vector2IntValue == default;                case SerializedPropertyType.Vector3Int: return property.vector3IntValue == default;                case SerializedPropertyType.RectInt: return property.rectIntValue.Equals(default);                case SerializedPropertyType.BoundsInt: return property.boundsIntValue == default;                case SerializedPropertyType.ObjectReference: return property.objectReferenceValue == default;                case SerializedPropertyType.ExposedReference: return property.exposedReferenceValue == default;                case SerializedPropertyType.ManagedReference: return property.managedReferenceValue == default;                case SerializedPropertyType.FixedBufferSize: return property.fixedBufferSize == default;                case SerializedPropertyType.Enum: return property.enumValueIndex == default;                case SerializedPropertyType.Gradient:                case SerializedPropertyType.Generic:                default:                    if (property.isArray)                        return property.arraySize == default;                    var depth = property.depth;                    property = property.Copy();                    var enterChildren = true;                    while (property.Next(enterChildren) && property.depth > depth)                    {                        enterChildren = false;                        if (!IsDefaultValueByType(property))                            return false;                    }                    return true;            }        }        /************************************************************************************************************************/        #endregion        /************************************************************************************************************************/        #region Set Value        /************************************************************************************************************************/        /// <summary>Sets the value of the specified <see cref="SerializedProperty"/>.</summary>        public static void SetValue(this SerializedProperty property, object targetObject, object value)        {            switch (property.propertyType)            {                case SerializedPropertyType.Boolean: property.boolValue = (bool)value; break;                case SerializedPropertyType.Float: property.floatValue = (float)value; break;                case SerializedPropertyType.String: property.stringValue = (string)value; break;                case SerializedPropertyType.Integer:                case SerializedPropertyType.Character:                case SerializedPropertyType.LayerMask:                case SerializedPropertyType.ArraySize:                    property.intValue = (int)value; break;                case SerializedPropertyType.Vector2: property.vector2Value = (Vector2)value; break;                case SerializedPropertyType.Vector3: property.vector3Value = (Vector3)value; break;                case SerializedPropertyType.Vector4: property.vector4Value = (Vector4)value; break;                case SerializedPropertyType.Quaternion: property.quaternionValue = (Quaternion)value; break;                case SerializedPropertyType.Color: property.colorValue = (Color)value; break;                case SerializedPropertyType.AnimationCurve: property.animationCurveValue = (AnimationCurve)value; break;                case SerializedPropertyType.Rect: property.rectValue = (Rect)value; break;                case SerializedPropertyType.Bounds: property.boundsValue = (Bounds)value; break;                case SerializedPropertyType.Vector2Int: property.vector2IntValue = (Vector2Int)value; break;                case SerializedPropertyType.Vector3Int: property.vector3IntValue = (Vector3Int)value; break;                case SerializedPropertyType.RectInt: property.rectIntValue = (RectInt)value; break;                case SerializedPropertyType.BoundsInt: property.boundsIntValue = (BoundsInt)value; break;                case SerializedPropertyType.ObjectReference: property.objectReferenceValue = (Object)value; break;                case SerializedPropertyType.ExposedReference: property.exposedReferenceValue = (Object)value; break;                case SerializedPropertyType.ManagedReference: property.managedReferenceValue = value; break;                case SerializedPropertyType.FixedBufferSize:                    throw new InvalidOperationException($"{nameof(SetValue)} failed:" +                        $" {nameof(SerializedProperty)}.{nameof(SerializedProperty.fixedBufferSize)} is read-only.");                case SerializedPropertyType.Gradient: property.SetGradientValue((Gradient)value); break;                case SerializedPropertyType.Enum:// Would be complex because enumValueIndex can't be cast directly.                case SerializedPropertyType.Generic:                default:                    var accessor = GetAccessor(property);                    accessor?.SetValue(targetObject, value);                    break;            }        }        /************************************************************************************************************************/        /// <summary>Sets the value of the <see cref="SerializedProperty"/>.</summary>        public static void SetValue(this SerializedProperty property, object value)        {            switch (property.propertyType)            {                case SerializedPropertyType.Boolean: property.boolValue = (bool)value; break;                case SerializedPropertyType.Float: property.floatValue = (float)value; break;                case SerializedPropertyType.Integer: property.intValue = (int)value; break;                case SerializedPropertyType.String: property.stringValue = (string)value; break;                case SerializedPropertyType.Vector2: property.vector2Value = (Vector2)value; break;                case SerializedPropertyType.Vector3: property.vector3Value = (Vector3)value; break;                case SerializedPropertyType.Vector4: property.vector4Value = (Vector4)value; break;                case SerializedPropertyType.Quaternion: property.quaternionValue = (Quaternion)value; break;                case SerializedPropertyType.Color: property.colorValue = (Color)value; break;                case SerializedPropertyType.AnimationCurve: property.animationCurveValue = (AnimationCurve)value; break;                case SerializedPropertyType.Rect: property.rectValue = (Rect)value; break;                case SerializedPropertyType.Bounds: property.boundsValue = (Bounds)value; break;                case SerializedPropertyType.Vector2Int: property.vector2IntValue = (Vector2Int)value; break;                case SerializedPropertyType.Vector3Int: property.vector3IntValue = (Vector3Int)value; break;                case SerializedPropertyType.RectInt: property.rectIntValue = (RectInt)value; break;                case SerializedPropertyType.BoundsInt: property.boundsIntValue = (BoundsInt)value; break;                case SerializedPropertyType.ObjectReference: property.objectReferenceValue = (Object)value; break;                case SerializedPropertyType.ExposedReference: property.exposedReferenceValue = (Object)value; break;                case SerializedPropertyType.ArraySize: property.intValue = (int)value; break;                case SerializedPropertyType.FixedBufferSize:                    throw new InvalidOperationException($"{nameof(SetValue)} failed:" +                        $" {nameof(SerializedProperty)}.{nameof(SerializedProperty.fixedBufferSize)} is read-only.");                case SerializedPropertyType.Generic:                case SerializedPropertyType.Enum:                case SerializedPropertyType.LayerMask:                case SerializedPropertyType.Gradient:                case SerializedPropertyType.Character:                default:                    var accessor = GetAccessor(property);                    if (accessor != null)                    {                        var targets = property.serializedObject.targetObjects;                        for (int i = 0; i < targets.Length; i++)                        {                            accessor.SetValue(targets[i], value);                        }                    }                    break;            }        }        /************************************************************************************************************************/        /// <summary>        /// Resets the value of the <see cref="SerializedProperty"/> to the default value of its type and all its field        /// types, ignoring values set by constructors or field initializers.        /// </summary>        /// <remarks>        /// If you want to run constructors and field initializers, you can call        /// <see cref="PropertyAccessor.ResetValue"/> instead.        /// </remarks>        public static void ResetValue(SerializedProperty property)        {            switch (property.propertyType)            {                case SerializedPropertyType.Boolean: property.boolValue = default; break;                case SerializedPropertyType.Float: property.floatValue = default; break;                case SerializedPropertyType.String: property.stringValue = ""; break;                case SerializedPropertyType.Integer:                case SerializedPropertyType.Character:                case SerializedPropertyType.LayerMask:                case SerializedPropertyType.ArraySize:                    property.intValue = default;                    break;                case SerializedPropertyType.Vector2: property.vector2Value = default; break;                case SerializedPropertyType.Vector3: property.vector3Value = default; break;                case SerializedPropertyType.Vector4: property.vector4Value = default; break;                case SerializedPropertyType.Quaternion: property.quaternionValue = default; break;                case SerializedPropertyType.Color: property.colorValue = default; break;                case SerializedPropertyType.AnimationCurve: property.animationCurveValue = default; break;                case SerializedPropertyType.Rect: property.rectValue = default; break;                case SerializedPropertyType.Bounds: property.boundsValue = default; break;                case SerializedPropertyType.Vector2Int: property.vector2IntValue = default; break;                case SerializedPropertyType.Vector3Int: property.vector3IntValue = default; break;                case SerializedPropertyType.RectInt: property.rectIntValue = default; break;                case SerializedPropertyType.BoundsInt: property.boundsIntValue = default; break;                case SerializedPropertyType.ObjectReference: property.objectReferenceValue = default; break;                case SerializedPropertyType.ExposedReference: property.exposedReferenceValue = default; break;                case SerializedPropertyType.Enum: property.enumValueIndex = default; break;                case SerializedPropertyType.Gradient:                case SerializedPropertyType.FixedBufferSize:                case SerializedPropertyType.Generic:                default:                    if (property.isArray)                    {                        property.arraySize = default;                        break;                    }                    var depth = property.depth;                    property = property.Copy();                    var enterChildren = true;                    while (property.Next(enterChildren) && property.depth > depth)                    {                        enterChildren = false;                        ResetValue(property);                    }                    break;            }        }        /************************************************************************************************************************/        /// <summary>Copies the value of `from` into `to` (including all nested properties).</summary>        public static float CopyValueFrom(this SerializedProperty to, SerializedProperty from)        {            from = from.Copy();            var fromPath = from.propertyPath;            var pathPrefixLength = fromPath.Length + 1;            var depth = from.depth;            var copyCount = 0;            var totalCount = 0;            StringBuilder issues = null;            do            {                while (from.propertyType == SerializedPropertyType.Generic)                    if (!from.Next(true))                        goto LogResults;                SerializedProperty toRelative;                var relativePath = from.propertyPath;                if (relativePath.Length <= pathPrefixLength)                {                    toRelative = to;                }                else                {                    relativePath = relativePath[pathPrefixLength..];                    toRelative = to.FindPropertyRelative(relativePath);                }                if (!from.hasMultipleDifferentValues &&                    toRelative != null &&                    toRelative.propertyType == from.propertyType &&                    toRelative.type == from.type)                {                    // GetValue and SetValue currently access the underlying field for enums, but we need the stored value.                    if (toRelative.propertyType == SerializedPropertyType.Enum)                        toRelative.enumValueIndex = from.enumValueIndex;                    else                        toRelative.SetValue(from.GetValue());                    copyCount++;                }                else                {                    issues ??= new();                    issues.AppendLine()                        .Append(" - ");                    if (from.hasMultipleDifferentValues)                    {                        issues                            .Append("The selected objects have different values for '")                            .Append(relativePath)                            .Append("'.");                    }                    else if (toRelative == null)                    {                        issues                            .Append("No property '")                            .Append(relativePath)                            .Append("' exists relative to '")                            .Append(to.propertyPath)                            .Append("'.");                    }                    else if (toRelative.propertyType != from.propertyType)                    {                        issues                            .Append("The type of '")                            .Append(toRelative.propertyPath)                            .Append("' was '")                            .Append(toRelative.propertyType)                            .Append("' but should be '")                            .Append(from.propertyType)                            .Append("'.");                    }                    else if (toRelative.type != from.type)                    {                        issues                            .Append("The type of '")                            .Append(toRelative.propertyPath)                            .Append("' was '")                            .Append(toRelative.type)                            .Append("' but should be '")                            .Append(from.type)                            .Append("'.");                    }                    else// This should never happen.                    {                        issues                            .Append(" - Unknown issue with '")                            .Append(relativePath)                            .Append("'.");                    }                }                totalCount++;            }            while (from.Next(false) && from.depth > depth);            LogResults:            if (copyCount < totalCount)                Debug.Log($"Copied {copyCount} / {totalCount} values from '{fromPath}' to '{to.propertyPath}': {issues}");            return (float)copyCount / totalCount;        }        /************************************************************************************************************************/        #endregion        /************************************************************************************************************************/        #region Gradients        /************************************************************************************************************************/        private static PropertyInfo _GradientValue;        /// <summary><c>SerializedProperty.gradientValue</c> is internal.</summary>        private static PropertyInfo GradientValue        {            get            {                if (_GradientValue == null)                    _GradientValue = typeof(SerializedProperty).GetProperty("gradientValue", InstanceBindings);                return _GradientValue;            }        }        /// <summary>Gets the <see cref="Gradient"/> value from a <see cref="SerializedPropertyType.Gradient"/>.</summary>        public static Gradient GetGradientValue(this SerializedProperty property)            => (Gradient)GradientValue.GetValue(property, null);        /// <summary>Sets the <see cref="Gradient"/> value on a <see cref="SerializedPropertyType.Gradient"/>.</summary>        public static void SetGradientValue(this SerializedProperty property, Gradient value)            => GradientValue.SetValue(property, value, null);        /************************************************************************************************************************/        #endregion        /************************************************************************************************************************/        /// <summary>Indicates whether both properties refer to the same underlying field.</summary>        public static bool AreSameProperty(SerializedProperty a, SerializedProperty b)        {            if (a == b)                return true;            if (a == null)                return b == null;            if (b == null)                return false;            if (a.propertyPath != b.propertyPath)                return false;            var aTargets = a.serializedObject.targetObjects;            var bTargets = b.serializedObject.targetObjects;            if (aTargets.Length != bTargets.Length)                return false;            for (int i = 0; i < aTargets.Length; i++)            {                if (aTargets[i] != bTargets[i])                    return false;            }            return true;        }        /************************************************************************************************************************/        /// <summary>        /// Executes the `action` once with a new <see cref="SerializedProperty"/> for each of the        /// <see cref="SerializedObject.targetObjects"/>. Or if there is only one target, it uses the `property`.        /// </summary>        public static void ForEachTarget(this SerializedProperty property, Action<SerializedProperty> function,            string undoName = "Inspector")        {            var targets = property.serializedObject.targetObjects;            if (undoName != null)                Undo.RecordObjects(targets, undoName);            if (targets.Length == 1)            {                function(property);                property.serializedObject.ApplyModifiedProperties();            }            else            {                var path = property.propertyPath;                for (int i = 0; i < targets.Length; i++)                {                    using var serializedObject = new SerializedObject(targets[i]);                    property = serializedObject.FindProperty(path);                    function(property);                    property.serializedObject.ApplyModifiedProperties();                }            }        }        /************************************************************************************************************************/        /// <summary>        /// Adds a menu item to execute the specified `function` for each of the `property`s target objects.        /// </summary>        public static void AddFunction(this GenericMenu menu, string label, MenuFunctionState state, GenericMenu.MenuFunction function)        {            if (state != MenuFunctionState.Disabled)            {                menu.AddItem(new(label), state == MenuFunctionState.Selected, function);            }            else            {                menu.AddDisabledItem(new(label));            }        }        /// <summary>        /// Adds a menu item to execute the specified `function` for each of the `property`s target objects.        /// </summary>        public static void AddFunction(this GenericMenu menu, string label, bool enabled, GenericMenu.MenuFunction function)            => AddFunction(menu, label, enabled ? MenuFunctionState.Normal : MenuFunctionState.Disabled, function);        /************************************************************************************************************************/        /// <summary>Adds a menu item to execute the specified `function` for each of the `property`s target objects.</summary>        public static void AddPropertyModifierFunction(this GenericMenu menu, SerializedProperty property, string label,            MenuFunctionState state, Action<SerializedProperty> function)        {            if (state != MenuFunctionState.Disabled && GUI.enabled)            {                menu.AddItem(new(label), state == MenuFunctionState.Selected, () =>                {                    ForEachTarget(property, function);                    GUIUtility.keyboardControl = 0;                    GUIUtility.hotControl = 0;                    EditorGUIUtility.editingTextField = false;                });            }            else            {                menu.AddDisabledItem(new(label));            }        }        /// <summary>Adds a menu item to execute the specified `function` for each of the `property`s target objects.</summary>        public static void AddPropertyModifierFunction(this GenericMenu menu, SerializedProperty property, string label, bool enabled,            Action<SerializedProperty> function)            => AddPropertyModifierFunction(menu, property, label, enabled ? MenuFunctionState.Normal : MenuFunctionState.Disabled, function);        /// <summary>Adds a menu item to execute the specified `function` for each of the `property`s target objects.</summary>        public static void AddPropertyModifierFunction(this GenericMenu menu, SerializedProperty property, string label,            Action<SerializedProperty> function)            => AddPropertyModifierFunction(menu, property, label, MenuFunctionState.Normal, function);        /************************************************************************************************************************/        /// <summary>        /// Calls the specified `method` for each of the underlying values of the `property` (in case it represents        /// multiple selected objects) and records an undo step for any modifications made.        /// </summary>        public static void ModifyValues<T>(this SerializedProperty property, Action<T> method, string undoName = "Inspector")        {            RecordUndo(property, undoName);            var values = GetValues<T>(property);            for (int i = 0; i < values.Length; i++)                method(values[i]);            OnPropertyChanged(property);        }        /************************************************************************************************************************/        /// <summary>        /// Records the state of the specified `property` so it can be undone.        /// </summary>        public static void RecordUndo(this SerializedProperty property, string undoName = "Inspector")            => Undo.RecordObjects(property.serializedObject.targetObjects, undoName);        /************************************************************************************************************************/        /// <summary>        /// Updates the specified `property` and marks its target objects as dirty so any changes to a prefab will be saved.        /// </summary>        public static void OnPropertyChanged(this SerializedProperty property)        {            var targets = property.serializedObject.targetObjects;            // If this change is made to a prefab, this makes sure that any instances in the scene will be updated.            for (int i = 0; i < targets.Length; i++)            {                EditorUtility.SetDirty(targets[i]);            }            property.serializedObject.Update();        }        /************************************************************************************************************************/        /// <summary>        /// Returns the <see cref="SerializedPropertyType"/> that represents fields of the specified `type`.        /// </summary>        public static SerializedPropertyType GetPropertyType(Type type)        {            // Primitives.            if (type == typeof(bool))                return SerializedPropertyType.Boolean;            if (type == typeof(int))                return SerializedPropertyType.Integer;            if (type == typeof(float))                return SerializedPropertyType.Float;            if (type == typeof(string))                return SerializedPropertyType.String;            if (type == typeof(LayerMask))                return SerializedPropertyType.LayerMask;            // Vectors.            if (type == typeof(Vector2))                return SerializedPropertyType.Vector2;            if (type == typeof(Vector3))                return SerializedPropertyType.Vector3;            if (type == typeof(Vector4))                return SerializedPropertyType.Vector4;            if (type == typeof(Quaternion))                return SerializedPropertyType.Quaternion;            // Other.            if (type == typeof(Color) || type == typeof(Color32))                return SerializedPropertyType.Color;            if (type == typeof(Gradient))                return SerializedPropertyType.Gradient;            if (type == typeof(Rect))                return SerializedPropertyType.Rect;            if (type == typeof(Bounds))                return SerializedPropertyType.Bounds;            if (type == typeof(AnimationCurve))                return SerializedPropertyType.AnimationCurve;            // Int Variants.            if (type == typeof(Vector2Int))                return SerializedPropertyType.Vector2Int;            if (type == typeof(Vector3Int))                return SerializedPropertyType.Vector3Int;            if (type == typeof(RectInt))                return SerializedPropertyType.RectInt;            if (type == typeof(BoundsInt))                return SerializedPropertyType.BoundsInt;            // Special.            if (typeof(Object).IsAssignableFrom(type))                return SerializedPropertyType.ObjectReference;            if (type.IsEnum)                return SerializedPropertyType.Enum;            return SerializedPropertyType.Generic;        }        /************************************************************************************************************************/        /// <summary>Removes the specified array element from the `property`.</summary>        /// <remarks>        /// If the element is not at its default value, the first call to        /// <see cref="SerializedProperty.DeleteArrayElementAtIndex"/> will only reset it, so this method will        /// call it again if necessary to ensure that it actually gets removed.        /// </remarks>        public static void RemoveArrayElement(SerializedProperty property, int index)        {            var count = property.arraySize;            if ((uint)index >= count)                return;            property.DeleteArrayElementAtIndex(index);            if (property.arraySize == count)                property.DeleteArrayElementAtIndex(index);        }        /************************************************************************************************************************/        #endregion        /************************************************************************************************************************/        #region Accessor Pool        /************************************************************************************************************************/        private static readonly Dictionary<Type, Dictionary<string, PropertyAccessor>>            TypeToPathToAccessor = new();        /************************************************************************************************************************/        /// <summary>        /// Returns a <see cref="PropertyAccessor"/> that can be used to access the details of the specified `property`.        /// </summary>        public static PropertyAccessor GetAccessor(this SerializedProperty property)        {            var type = property.serializedObject.targetObject.GetType();            return GetAccessor(property, property.propertyPath, ref type);        }        /************************************************************************************************************************/        /// <summary>        /// Returns a <see cref="PropertyAccessor"/> for a <see cref="SerializedProperty"/> with the specified `propertyPath`        /// on the specified `type` of object.        /// </summary>        private static PropertyAccessor GetAccessor(SerializedProperty property, string propertyPath, ref Type type)        {            if (!TypeToPathToAccessor.TryGetValue(type, out var pathToAccessor))                TypeToPathToAccessor.Add(type, pathToAccessor = new());            if (!pathToAccessor.TryGetValue(propertyPath, out var accessor))            {                var nameStartIndex = propertyPath.LastIndexOf('.');                string elementName;                PropertyAccessor parent;                // Array.                if (nameStartIndex > 6 &&                    nameStartIndex < propertyPath.Length - 7 &&                    string.Compare(propertyPath, nameStartIndex - 6, ArrayDataPrefix, 0, 12) == 0)                {                    var index = int.Parse(propertyPath.Substring(nameStartIndex + 6, propertyPath.Length - nameStartIndex - 7));                    var nameEndIndex = nameStartIndex - 6;                    nameStartIndex = propertyPath.LastIndexOf('.', nameEndIndex - 1);                    elementName = propertyPath.Substring(nameStartIndex + 1, nameEndIndex - nameStartIndex - 1);                    FieldInfo field;                    if (nameStartIndex >= 0)                    {                        parent = GetAccessor(property, propertyPath[..nameStartIndex], ref type);                        field = GetField(parent, property, type, elementName);                    }                    else                    {                        parent = null;                        field = GetField(type, elementName);                    }                    accessor = new CollectionPropertyAccessor(parent, elementName, field, index);                }                else// Single.                {                    if (nameStartIndex >= 0)                    {                        elementName = propertyPath[(nameStartIndex + 1)..];                        parent = GetAccessor(property, propertyPath[..nameStartIndex], ref type);                    }                    else                    {                        elementName = propertyPath;                        parent = null;                    }                    var field = GetField(parent, property, type, elementName);                    accessor = new PropertyAccessor(parent, elementName, field);                }                pathToAccessor.Add(propertyPath, accessor);            }            if (accessor != null)            {                var field = accessor.GetField(property);                if (field != null)                {                    type = field.FieldType;                }                else                {                    var value = accessor.GetValue(property);                    type = value?.GetType();                }            }            return accessor;        }        /************************************************************************************************************************/        /// <summary>Returns a field with the specified `name` in the `declaringType` or any of its base types.</summary>        /// <remarks>Uses the <see cref="InstanceBindings"/>.</remarks>        public static FieldInfo GetField(PropertyAccessor accessor, SerializedProperty property, Type declaringType, string name)        {            declaringType = accessor?.GetFieldElementType(property) ?? declaringType;            return GetField(declaringType, name);        }        /// <summary>Returns a field with the specified `name` in the `declaringType` or any of its base types.</summary>        /// <remarks>Uses the <see cref="InstanceBindings"/>.</remarks>        public static FieldInfo GetField(Type declaringType, string name)        {            while (declaringType != null)            {                var field = declaringType.GetField(name, InstanceBindings);                if (field != null)                    return field;                declaringType = declaringType.BaseType;            }            return null;        }        /************************************************************************************************************************/        #endregion        /************************************************************************************************************************/        #region PropertyAccessor        /************************************************************************************************************************/        /// <summary>[Editor-Only]        /// A wrapper for accessing the underlying values and fields of a <see cref="SerializedProperty"/>.        /// </summary>        public class PropertyAccessor        {            /************************************************************************************************************************/            /// <summary>The accessor for the field which this accessor is nested inside.</summary>            public readonly PropertyAccessor Parent;            /// <summary>The name of the field wrapped by this accessor.</summary>            public readonly string Name;            /// <summary>The field wrapped by this accessor.</summary>            protected readonly FieldInfo Field;            /// <summary>            /// The type of the wrapped <see cref="Field"/>.            /// Or if it's a collection, this is the type of items in the collection.            /// </summary>            protected readonly Type FieldElementType;            /// <summary>            /// Does the <see cref="Field"/> in this accessor or any <see cref="Parent"/> have a            /// <see cref="SerializeReference"/> attribute?            /// </summary>            public readonly bool IsDynamic;            /************************************************************************************************************************/            /// <summary>[Internal] Creates a new <see cref="PropertyAccessor"/>.</summary>            internal PropertyAccessor(PropertyAccessor parent, string name, FieldInfo field)                : this(parent, name, field, field?.FieldType)            { }            /// <summary>Creates a new <see cref="PropertyAccessor"/>.</summary>            protected PropertyAccessor(PropertyAccessor parent, string name, FieldInfo field, Type fieldElementType)            {                Parent = parent;                Name = name;                if ((parent != null && parent.IsDynamic) ||                    field == null ||                    field.IsDefined(typeof(SerializeReference), false))                {                    IsDynamic = true;                    return;                }                Field = field;                FieldElementType = fieldElementType;            }            /************************************************************************************************************************/            /// <summary>Returns the <see cref="Field"/> if there is one or tries to get it from the object's type.</summary>            ///             /// <remarks>            /// If this accessor has a <see cref="Parent"/>, the `obj` must be associated with the root            /// <see cref="SerializedProperty"/> and this method will change it to reference the parent field's value.            /// </remarks>            ///             /// <example><code>            /// [Serializable]            /// public class InnerClass            /// {            ///     public float value;            /// }            ///             /// [Serializable]            /// public class RootClass            /// {            ///     public InnerClass inner;            /// }            ///             /// public class MyBehaviour : MonoBehaviour            /// {            ///     public RootClass root;            /// }            ///             /// [UnityEditor.CustomEditor(typeof(MyBehaviour))]            /// public class MyEditor : UnityEditor.Editor            /// {            ///     private void OnEnable()            ///     {            ///         var serializedObject = new SerializedObject(target);            ///         var rootProperty = serializedObject.FindProperty("root");            ///         var innerProperty = rootProperty.FindPropertyRelative("inner");            ///         var valueProperty = innerProperty.FindPropertyRelative("value");            ///             ///         var accessor = valueProperty.GetAccessor();            ///             ///         object obj = target;            ///         var valueField = accessor.GetField(ref obj);            ///         // valueField is a FieldInfo referring to InnerClass.value.            ///         // obj now holds the ((MyBehaviour)target).root.inner.            ///     }            /// }            /// </code></example>            ///             public FieldInfo GetField(ref object obj)            {                if (Parent != null)                    obj = Parent.GetValue(obj);                if (Field != null)                    return Field;                if (obj is null)                    return null;                return Serialization.GetField(obj.GetType(), Name);            }            /// <summary>            /// Returns the <see cref="Field"/> if there is one, otherwise calls <see cref="GetField(ref object)"/>.            /// </summary>            public FieldInfo GetField(object obj)                => Field ?? GetField(ref obj);            /// <summary>            /// Calls <see cref="GetField(object)"/> with the <see cref="SerializedObject.targetObject"/>.            /// </summary>            public FieldInfo GetField(SerializedObject serializedObject)                => serializedObject != null ? GetField(serializedObject.targetObject) : null;            /// <summary>            /// Calls <see cref="GetField(SerializedObject)"/> with the            /// <see cref="SerializedProperty.serializedObject"/>.            /// </summary>            public FieldInfo GetField(SerializedProperty serializedProperty)                => serializedProperty != null ? GetField(serializedProperty.serializedObject) : null;            /************************************************************************************************************************/            /// <summary>            /// Returns the <see cref="FieldElementType"/> if there is one, otherwise calls <see cref="GetField(ref object)"/>            /// and returns its <see cref="FieldInfo.FieldType"/>.            /// </summary>            public virtual Type GetFieldElementType(object obj)                => FieldElementType ?? GetField(ref obj)?.FieldType;            /// <summary>            /// Calls <see cref="GetFieldElementType(object)"/> with the            /// <see cref="SerializedObject.targetObject"/>.            /// </summary>            public Type GetFieldElementType(SerializedObject serializedObject)                => serializedObject != null ? GetFieldElementType(serializedObject.targetObject) : null;            /// <summary>            /// Calls <see cref="GetFieldElementType(SerializedObject)"/> with the            /// <see cref="SerializedProperty.serializedObject"/>.            /// </summary>            public Type GetFieldElementType(SerializedProperty serializedProperty)                => serializedProperty != null ? GetFieldElementType(serializedProperty.serializedObject) : null;            /************************************************************************************************************************/            /// <summary>            /// Gets the value of the from the <see cref="Parent"/> (if there is one), then uses it to get and return            /// the value of the <see cref="Field"/>.            /// </summary>            public virtual object GetValue(object obj)            {                var field = GetField(ref obj);                if (field is null ||                    (obj is null && !field.IsStatic))                    return null;                return field.GetValue(obj);            }            /// <summary>            /// Gets the value of the from the <see cref="Parent"/> (if there is one), then uses it to get and return            /// the value of the <see cref="Field"/>.            /// </summary>            public object GetValue(SerializedObject serializedObject)                => serializedObject != null ? GetValue(serializedObject.targetObject) : null;            /// <summary>            /// Gets the value of the from the <see cref="Parent"/> (if there is one), then uses it to get and return            /// the value of the <see cref="Field"/>.            /// </summary>            public object GetValue(SerializedProperty serializedProperty)                => serializedProperty != null ? GetValue(serializedProperty.serializedObject.targetObject) : null;            /************************************************************************************************************************/            /// <summary>            /// Gets the value of the from the <see cref="Parent"/> (if there is one), then uses it to set the value            /// of the <see cref="Field"/>.            /// </summary>            public virtual void SetValue(object obj, object value)            {                var field = GetField(ref obj);                if (field is null ||                    obj is null)                    return;                field.SetValue(obj, value);            }            /// <summary>            /// Gets the value of the from the <see cref="Parent"/> (if there is one), then uses it to set the value            /// of the <see cref="Field"/>.            /// </summary>            public void SetValue(SerializedObject serializedObject, object value)            {                if (serializedObject != null)                    SetValue(serializedObject.targetObject, value);            }            /// <summary>            /// Gets the value of the from the <see cref="Parent"/> (if there is one), then uses it to set the value            /// of the <see cref="Field"/>.            /// </summary>            public void SetValue(SerializedProperty serializedProperty, object value)            {                if (serializedProperty != null)                    SetValue(serializedProperty.serializedObject, value);            }            /************************************************************************************************************************/            /// <summary>            /// Resets the value of the <see cref="SerializedProperty"/> to the default value of its type by executing            /// its constructor and field initializers.            /// </summary>            /// <remarks>            /// If you don't want to run constructors and field initializers, you can call            /// <see cref="Serialization.ResetValue"/> instead.            /// </remarks>            /// <example><code>            /// SerializedProperty property;            /// property.GetAccessor().ResetValue(property);            /// </code></example>            public void ResetValue(SerializedProperty property, string undoName = "Inspector")            {                property.RecordUndo(undoName);                property.serializedObject.ApplyModifiedProperties();                var type = GetValue(property)?.GetType();                var value = type != null ? Activator.CreateInstance(type) : null;                SetValue(property, value);                property.serializedObject.Update();            }            /************************************************************************************************************************/            /// <summary>Returns a description of this accessor's path.</summary>            public override string ToString()            {                if (Parent != null)                    return $"{Parent}.{Name}";                else                    return Name;            }            /************************************************************************************************************************/            /// <summary>Returns this accessor's <see cref="SerializedProperty.propertyPath"/>.</summary>            public virtual string GetPath()            {                if (Parent != null)                    return $"{Parent.GetPath()}.{Name}";                else                    return Name;            }            /************************************************************************************************************************/        }        /************************************************************************************************************************/        #endregion        /************************************************************************************************************************/        #region CollectionPropertyAccessor        /************************************************************************************************************************/        /// <summary>[Editor-Only] A <see cref="PropertyAccessor"/> for a specific element index in a collection.</summary>        public class CollectionPropertyAccessor : PropertyAccessor        {            /************************************************************************************************************************/            /// <summary>The index of the array element this accessor targets.</summary>            public readonly int ElementIndex;            /************************************************************************************************************************/            /// <summary>[Internal] Creates a new <see cref="CollectionPropertyAccessor"/>.</summary>            internal CollectionPropertyAccessor(PropertyAccessor parent, string name, FieldInfo field, int elementIndex)                : base(parent, name, field, GetElementType(field?.FieldType))            {                ElementIndex = elementIndex;            }            /************************************************************************************************************************/            /// <inheritdoc/>            public override Type GetFieldElementType(object obj)                => FieldElementType ?? GetElementType(GetField(ref obj)?.FieldType);            /************************************************************************************************************************/            /// <summary>Returns the type of elements in the array.</summary>            public static Type GetElementType(Type fieldType)            {                if (fieldType == null)                    return null;                if (fieldType.IsArray)                    return fieldType.GetElementType();                if (fieldType.IsGenericType)                    return fieldType.GetGenericArguments()[0];                Debug.LogWarning($"{nameof(Serialization)}.{nameof(CollectionPropertyAccessor)}:" +                    $" unable to determine element type for {fieldType}");                return fieldType;            }            /************************************************************************************************************************/            /// <summary>Returns the collection object targeted by this accessor.</summary>            public object GetCollection(object obj)                => base.GetValue(obj);            /// <inheritdoc/>            public override object GetValue(object obj)            {                var collection = base.GetValue(obj);                if (collection == null)                    return null;                if (collection is IList list)                    return ElementIndex < list.Count ? list[ElementIndex] : null;                var enumerator = ((IEnumerable)collection).GetEnumerator();                for (int i = 0; i < ElementIndex; i++)                    if (!enumerator.MoveNext())                        return null;                return enumerator.Current;            }            /************************************************************************************************************************/            /// <summary>Sets the collection object targeted by this accessor.</summary>            public void SetCollection(object obj, object value)                => base.SetValue(obj, value);            /// <inheritdoc/>            public override void SetValue(object obj, object value)            {                var collection = base.GetValue(obj);                if (collection == null)                    return;                if (collection is IList list)                {                    if (ElementIndex < list.Count)                        list[ElementIndex] = value;                    return;                }                throw new InvalidOperationException(                    $"{nameof(SetValue)} failed: field doesn't implement {nameof(IList)}.");            }            /************************************************************************************************************************/            /// <summary>Returns a description of this accessor's path.</summary>            public override string ToString()                => $"{base.ToString()}[{ElementIndex}]";            /************************************************************************************************************************/            /// <summary>Returns the <see cref="SerializedProperty.propertyPath"/> of the array containing the target.</summary>            public string GetCollectionPath()                => base.GetPath();            /// <summary>Returns this accessor's <see cref="SerializedProperty.propertyPath"/>.</summary>            public override string GetPath()                => $"{base.GetPath()}{ArrayDataPrefix}{ElementIndex}{ArrayDataSuffix}";            /************************************************************************************************************************/        }        /************************************************************************************************************************/        #endregion        /************************************************************************************************************************/    }}#endif
 |