Files
trail-into-darkness/Packages/com.jovian.utilties/Editor/EditorSerializationUtility.cs
2026-03-29 18:59:24 +02:00

429 lines
20 KiB
C#

#if UNITY_EDITOR
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Text.RegularExpressions;
using UnityEngine;
using UnityEngine.Assertions;
using Debug = UnityEngine.Debug;
using Object = UnityEngine.Object;
using Type = System.Type;
using UnityEditor;
namespace Jovian.Utilities.Editor {
/// <summary>
/// Helper class for serializing objects in the editor
/// </summary>
public static class EditorSerializationUtility {
/// <summary>
/// Returns the property type for cases when it is needed, like in the case of trying to get the type of SerializedPropertyType.ObjectReference
/// Solution found on https://answers.unity.com/questions/929293/get-field-type-of-serializedproperty.html
/// </summary>
/// <param name="property"></param>
/// <returns></returns>
public static Type GetTypeFromProperty(SerializedProperty property) {
//gets parent type info
string[] slices = property.propertyPath.Split('.');
Type type = property.serializedObject.targetObject.GetType();
for (int i = 0; i < slices.Length; i++) {
string slice = slices[i];
if (slice == "Array") {
i++; //skips "data[x]"
if (type.IsArray) // e.g Type[]
{
type = type.GetElementType();
}
else if (type.IsGenericType) // e.g. List<Type>
{
type = type.GetGenericArguments()[0];
}
else {
throw new NotSupportedException("Unsupported array type. Type[] or List<Type> are only supported array types");
}
}
else {
//gets info on field and its type
type = type.GetField(slice, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.Instance)?.FieldType;
}
if (type == null) {
throw new NullReferenceException(
$"Type is null, something is not working correctly. Path={property.propertyPath}, Slice={slice}");
}
}
//type is now the type of the property
return type;
}
private static readonly Regex isArrayElementRegex = new Regex("Array.data\\[\\d+\\]$");
/// <summary>
/// Checks if the property is an array element
/// </summary>
/// <param name="property"></param>
/// <returns></returns>
public static bool IsPropertyAnArrayElement(SerializedProperty property) {
return isArrayElementRegex.IsMatch(property.propertyPath);
}
/// <summary>
/// Returns the parent array property for the given property, which is a member of the array
/// </summary>
/// <param name="property"></param>
/// <returns></returns>
public static SerializedProperty GetArrayPropertyWithElementProperty(SerializedProperty property) {
SerializedObject serializedObject = property.serializedObject;
string propertyPath = property.propertyPath;
int arrayDataIndex = propertyPath.LastIndexOf(".Array.data", StringComparison.Ordinal);
string propertyPathWithoutArray = propertyPath.Substring(0, arrayDataIndex);
int pathDividerIndex = propertyPathWithoutArray.LastIndexOf(".", StringComparison.Ordinal);
string parentPropertyName = propertyPathWithoutArray;
if (pathDividerIndex != -1) {
parentPropertyName = propertyPathWithoutArray.Substring(pathDividerIndex);
}
return serializedObject.FindProperty(parentPropertyName);
}
/// <summary>
/// Will save specifically the property args passed in the form new object[] { "propertyName1", "propertyName2", etc. } to the specified targetObject
/// </summary>
/// <param name="targetObject"></param>
/// <param name="args"></param>
/// <exception cref="NotSupportedException"></exception>
/// <exception cref="Exception"></exception>
public static void SaveObjectProperties(Object targetObject, params object[] args) {
SerializedObject serializedObject = new SerializedObject(targetObject);
for (int i = 0; i < args.Length; i += 2) {
object keyArg = args[i];
Type keyType = keyArg.GetType();
if ((keyType == typeof(string)) == false) {
throw new NotSupportedException($"Key must be string. {args[i]} is {keyType}");
}
SerializedProperty property = serializedObject.FindProperty((string)keyArg);
object argValue = args[i + 1];
if (property == null) {
throw new Exception($"No property found for key {keyArg}");
}
if (argValue == null) {
property.objectReferenceValue = null;
}
else {
SetSerializedPropertyValue(property, argValue);
}
}
serializedObject.ApplyModifiedProperties();
}
/// <summary>
/// Will auto-detect the value type and save it to the property. It only supports the serializable types that the Unity serialization system supports
/// </summary>
/// <param name="fromProperty"></param>
/// <param name="value"></param>
/// <exception cref="NotSupportedException"></exception>
public static void SetSerializedPropertyValue(SerializedProperty fromProperty, object value) {
if (fromProperty == null) {
Debug.LogError("SetSerializedPropertyValue failed, property is null");
return;
}
// Strings are counted as arrays but should be handled separately
if (fromProperty.isArray && fromProperty.propertyType != SerializedPropertyType.String) {
fromProperty.arraySize = 0;
var argArray = (IEnumerable)value;
var enumerator = argArray.GetEnumerator();
int index = 0;
while (enumerator.MoveNext()) {
fromProperty.InsertArrayElementAtIndex(index);
var arrayProperty = fromProperty.GetArrayElementAtIndex(index);
SetSerializedPropertyValue(arrayProperty, enumerator.Current);
index++;
}
}
else {
switch (fromProperty.propertyType) {
case SerializedPropertyType.AnimationCurve:
fromProperty.animationCurveValue = (AnimationCurve)value;
break;
case SerializedPropertyType.Boolean:
fromProperty.boolValue = (bool)value;
break;
case SerializedPropertyType.BoundsInt:
fromProperty.boundsIntValue = (BoundsInt)value;
break;
case SerializedPropertyType.Character:
fromProperty.intValue = (int)(char)value;
break;
case SerializedPropertyType.Color: {
if (value is Color32 color32) {
fromProperty.colorValue = (Color)color32;
}
else {
fromProperty.colorValue = (Color)value;
}
break;
}
case SerializedPropertyType.ExposedReference:
case SerializedPropertyType.ObjectReference:
fromProperty.objectReferenceValue = (Object)value;
break;
case SerializedPropertyType.Float:
fromProperty.floatValue = (float)value;
break;
case SerializedPropertyType.Integer:
fromProperty.intValue = (int)value;
break;
case SerializedPropertyType.LayerMask:
fromProperty.intValue = ((LayerMask)value).value;
break;
case SerializedPropertyType.Quaternion:
fromProperty.quaternionValue = (Quaternion)value;
break;
case SerializedPropertyType.Rect:
fromProperty.rectValue = (Rect)value;
break;
case SerializedPropertyType.RectInt:
fromProperty.rectIntValue = (RectInt)value;
break;
case SerializedPropertyType.String:
fromProperty.stringValue = (string)value;
break;
case SerializedPropertyType.Vector2:
fromProperty.vector2Value = (Vector2)value;
break;
case SerializedPropertyType.Vector2Int:
fromProperty.vector2IntValue = (Vector2Int)value;
break;
case SerializedPropertyType.Vector3:
fromProperty.vector3Value = (Vector3)value;
break;
case SerializedPropertyType.Vector3Int:
fromProperty.vector3IntValue = (Vector3Int)value;
break;
case SerializedPropertyType.Vector4:
fromProperty.vector4Value = (Vector4)value;
break;
case SerializedPropertyType.Enum:
fromProperty.enumValueIndex = Array.IndexOf(Enum.GetValues(value.GetType()), value);
// flags???
break;
case SerializedPropertyType.Generic:
SaveGenericProperty(fromProperty, value);
break;
default:
throw new NotSupportedException($"PropertyType {fromProperty.propertyType} is not supported - yet. Array? {fromProperty.isArray} Path: {fromProperty.propertyPath}");
}
}
}
/// <summary>
/// Will copy fromProperty to toProperty. It only supports the serializable types that the Unity serialization system supports
/// </summary>
/// <param name="fromProperty"></param>
/// <param name="toProperty"></param>
/// <exception cref="NotSupportedException"></exception>
public static void CopyPropertyValue(SerializedProperty fromProperty, SerializedProperty toProperty) {
Assert.IsNotNull(fromProperty, "fromProperty == null");
Assert.IsNotNull(toProperty, "toProperty == null");
Assert.AreEqual(fromProperty.propertyType, toProperty.propertyType, $"Properties do not match types. fromType={fromProperty.propertyType}, toType={toProperty.propertyType}");
switch (fromProperty.propertyType) {
case SerializedPropertyType.AnimationCurve:
toProperty.animationCurveValue = fromProperty.animationCurveValue;
break;
case SerializedPropertyType.Boolean:
toProperty.boolValue = fromProperty.boolValue;
break;
case SerializedPropertyType.BoundsInt:
toProperty.boundsIntValue = fromProperty.boundsIntValue;
break;
case SerializedPropertyType.Character:
toProperty.intValue = fromProperty.intValue;
break;
case SerializedPropertyType.Color: {
toProperty.colorValue = fromProperty.colorValue;
break;
}
case SerializedPropertyType.ExposedReference:
case SerializedPropertyType.ObjectReference:
toProperty.objectReferenceValue = fromProperty.objectReferenceValue;
break;
case SerializedPropertyType.Float:
toProperty.floatValue = fromProperty.floatValue;
break;
case SerializedPropertyType.Integer:
toProperty.intValue = fromProperty.intValue;
break;
case SerializedPropertyType.LayerMask:
toProperty.intValue = fromProperty.intValue;
break;
case SerializedPropertyType.Quaternion:
toProperty.quaternionValue = fromProperty.quaternionValue;
break;
case SerializedPropertyType.Rect:
toProperty.rectValue = fromProperty.rectValue;
break;
case SerializedPropertyType.RectInt:
toProperty.rectIntValue = fromProperty.rectIntValue;
break;
case SerializedPropertyType.String:
toProperty.stringValue = fromProperty.stringValue;
break;
case SerializedPropertyType.Vector2:
toProperty.vector2Value = fromProperty.vector2Value;
break;
case SerializedPropertyType.Vector2Int:
toProperty.vector2IntValue = fromProperty.vector2IntValue;
break;
case SerializedPropertyType.Vector3:
toProperty.vector3Value = fromProperty.vector3Value;
break;
case SerializedPropertyType.Vector3Int:
toProperty.vector3IntValue = fromProperty.vector3IntValue;
break;
case SerializedPropertyType.Vector4:
toProperty.vector4Value = fromProperty.vector4Value;
break;
case SerializedPropertyType.Enum:
toProperty.intValue = fromProperty.intValue;
break;
case SerializedPropertyType.Generic:
CopyGenericProperty(fromProperty, toProperty);
break;
case SerializedPropertyType.ManagedReference:
toProperty.managedReferenceValue = Activator.CreateInstance(fromProperty.managedReferenceValue.GetType());
CopyGenericProperty(fromProperty, toProperty);
break;
default:
throw new NotSupportedException($"PropertyType {fromProperty.propertyType} is not supported - yet. Array? {fromProperty.isArray} Path: {fromProperty.propertyPath}");
}
}
private static void CopyGenericProperty(SerializedProperty fromProperty, SerializedProperty toProperty) {
IEnumerator fromPropertyEnumerator = fromProperty.GetEnumerator();
IEnumerator toPropertyEnumerator = toProperty.GetEnumerator();
while (toPropertyEnumerator.MoveNext() && fromPropertyEnumerator.MoveNext()) {
if (toPropertyEnumerator.Current is SerializedProperty toChildProperty &&
fromPropertyEnumerator.Current is SerializedProperty fromChildProperty) {
CopyPropertyValue(fromChildProperty, toChildProperty);
}
}
}
private static void SaveGenericProperty(SerializedProperty property, object instance) {
Type type = instance.GetType();
IEnumerable<FieldInfo> fields = type.GetRuntimeFields();
foreach (FieldInfo field in fields) {
if (field.IsNotSerialized || field.IsStatic) {
continue;
}
SerializedProperty fieldProperty = property.FindPropertyRelative(field.Name);
if (fieldProperty != null) {
SetSerializedPropertyValue(fieldProperty, field.GetValue(instance));
}
else {
Debug.Log($"SaveGenericProperty cannot field serializedProperty named '{field.Name}'");
}
}
}
public static bool TryGetAttribute<TAttribute>(
this SerializedProperty serializedProperty,
out TAttribute attribute,
bool includeAttributesFromParentProperties = false,
bool includeInheritedAttributes = true
)
where TAttribute : Attribute {
if (serializedProperty == null) {
throw new ArgumentNullException(nameof(serializedProperty));
}
Type targetObjectType = serializedProperty.serializedObject.targetObject.GetType();
if (targetObjectType == null) {
throw new ArgumentException($"Could not find the {nameof(targetObjectType)} of {nameof(serializedProperty)}");
}
string[] pathSegments = includeAttributesFromParentProperties ? serializedProperty.propertyPath.Split('.') : new[] { serializedProperty.propertyPath };
foreach (string pathSegment in pathSegments) {
FieldInfo fieldInfo = targetObjectType.GetField(pathSegment, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy);
if (fieldInfo != null) {
attribute = fieldInfo.GetCustomAttribute<TAttribute>(includeInheritedAttributes);
if (attribute != null) {
return true;
}
}
PropertyInfo propertyInfo = targetObjectType.GetProperty(pathSegment, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy);
if (propertyInfo != null) {
attribute = propertyInfo.GetCustomAttribute<TAttribute>(includeInheritedAttributes);
if (attribute != null) {
return true;
}
}
}
attribute = null;
return false;
}
public static bool TryGetAttributes<TAttribute>(
SerializedProperty serializedProperty,
out List<TAttribute> attributes,
bool includeAttributesFromParentProperties = false,
bool includeInheritedAttributes = true
)
where TAttribute : Attribute {
if (serializedProperty == null) {
throw new ArgumentNullException(nameof(serializedProperty));
}
Type targetObjectType = serializedProperty.serializedObject.targetObject.GetType();
if (targetObjectType == null) {
throw new ArgumentException($"Could not find the {nameof(targetObjectType)} of {nameof(serializedProperty)}");
}
attributes = new List<TAttribute>();
string[] pathSegments = includeAttributesFromParentProperties ? serializedProperty.propertyPath.Split('.') : new[] { serializedProperty.propertyPath };
foreach (string pathSegment in pathSegments) {
FieldInfo fieldInfo = targetObjectType.GetField(pathSegment, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy);
if (fieldInfo != null) {
IEnumerable<TAttribute> foundAttributes = fieldInfo.GetCustomAttributes<TAttribute>(includeInheritedAttributes);
foreach (TAttribute foundAttribute in foundAttributes) {
attributes.Add(foundAttribute);
}
}
PropertyInfo propertyInfo = targetObjectType.GetProperty(pathSegment, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy);
if (propertyInfo != null) {
IEnumerable<TAttribute> foundAttributes = propertyInfo.GetCustomAttributes<TAttribute>(includeInheritedAttributes);
foreach (TAttribute attribute in foundAttributes) {
attributes.Add(attribute);
}
}
}
return attributes.Count > 0;
}
}
}
#endif