Files
Cielonos/Packages/com.lunawolfstudios.scriptablesheets/Editor/Modules/Tables/TableProperties/SerializedTableProperty.cs
SoulliesOfficial 7ee2894a63 整合SLSUtilities
2026-01-17 11:35:49 -05:00

623 lines
20 KiB
C#

using LunaWolfStudiosEditor.ScriptableSheets.Serializables;
using LunaWolfStudiosEditor.ScriptableSheets.Shared;
using Newtonsoft.Json;
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
namespace LunaWolfStudiosEditor.ScriptableSheets.Tables
{
public struct SerializedTableProperty : ITableProperty
{
public const string NullObjectValue = "null";
private const char AssetDelimiter = '&';
// Sub Asset names can contain special chars, so use a unique string.
private const string SubAssetDelimiter = "&SUBASSET#";
private readonly Object m_RootObject;
public Object RootObject => m_RootObject;
private readonly string m_PropertyPath;
public string PropertyPath => m_PropertyPath;
private readonly string m_ControlName;
public string ControlName => m_ControlName;
public SerializedTableProperty(Object rootObject, string propertyPath, string controlName)
{
m_RootObject = rootObject;
m_PropertyPath = propertyPath;
m_ControlName = controlName;
}
public SerializedObject GetSerializedObject()
{
return new SerializedObject(m_RootObject);
}
public SerializedProperty GetSerializedProperty()
{
return GetSerializedProperty(GetSerializedObject());
}
public SerializedProperty GetSerializedProperty(SerializedObject serializedObject)
{
return serializedObject.FindProperty(PropertyPath);
}
public string GetProperty(FlatFileFormatSettings formatSettings)
{
var property = GetSerializedProperty();
if (property.propertyPath == UnityConstants.Field.Name)
{
return m_RootObject.name;
}
switch (property.propertyType)
{
case SerializedPropertyType.Integer:
return property.GetIntStringValue();
case SerializedPropertyType.Boolean:
return property.boolValue.ToString();
case SerializedPropertyType.Float:
return property.GetFloatStringValue();
case SerializedPropertyType.String:
return property.stringValue;
case SerializedPropertyType.Color:
return ColorUtility.ToHtmlStringRGBA(property.colorValue);
case SerializedPropertyType.LayerMask:
return property.intValue.ToString();
case SerializedPropertyType.Enum:
if (!IsDefaultUnityEngineType(property.serializedObject) && formatSettings.UseStringEnums && property.TryGetEnumType(m_RootObject, out Type enumType))
{
if (!enumType.HasFlagsAttribute())
{
var enumName = Enum.GetName(enumType, property.intValue);
if (!string.IsNullOrEmpty(enumName))
{
return enumName;
}
}
}
return property.intValue.ToString();
case SerializedPropertyType.ObjectReference:
var objValue = property.objectReferenceValue;
if (objValue != null && AssetDatabase.TryGetGUIDAndLocalFileIdentifier(property.objectReferenceValue, out string guid, out long localId))
{
return $"{guid}{AssetDelimiter}{objValue.name}";
}
else
{
return NullObjectValue;
}
case SerializedPropertyType.ArraySize:
return property.intValue.ToString();
case SerializedPropertyType.Character:
return ((char) property.intValue).ToString();
case SerializedPropertyType.AnimationCurve:
var animationCurveJson = JsonUtility.ToJson(new SerializableAnimationCurve(property));
if (formatSettings.WrapOption.IsJsonUnsafe())
{
return animationCurveJson.EncodeBase64();
}
else
{
return animationCurveJson;
}
case SerializedPropertyType.Gradient:
var gradientJson = JsonUtility.ToJson(new SerializableGradient(property));
if (formatSettings.WrapOption.IsJsonUnsafe())
{
return gradientJson.EncodeBase64();
}
else
{
return gradientJson;
}
case SerializedPropertyType.Generic:
if (property.IsAssetReference())
{
var assetGuid = property.FindPropertyRelative(UnityConstants.Field.AssetRefGuid)?.stringValue;
var subObjectName = property.FindPropertyRelative(UnityConstants.Field.AssetRefSubObjectName)?.stringValue;
if (string.IsNullOrEmpty(assetGuid))
{
return NullObjectValue;
}
return $"{assetGuid}{SubAssetDelimiter}{subObjectName}";
}
else
{
Debug.LogWarning($"Unsupported generic property type for property at path {PropertyPath}.");
return string.Empty;
}
default:
Debug.LogWarning($"Unsupported property type {property.propertyType} for property at path {PropertyPath}.");
return string.Empty;
}
}
public void SetProperty(string value, FlatFileFormatSettings formatSettings)
{
var serializedObject = GetSerializedObject();
var property = GetSerializedProperty(serializedObject);
if (property != null)
{
if ((!property.editable && property.name != UnityConstants.Field.Name) || value == null)
{
// Cannot change read-only properties. To set properties null use an empty string or the null object string.
return;
}
var propertyType = property.propertyType;
switch (property.propertyType)
{
case SerializedPropertyType.Integer:
case SerializedPropertyType.ArraySize:
if (!property.TrySetIntValue(value))
{
LogParseWarning(value, propertyType, $"Not a valid int for numeric type '{property.type}'.");
}
break;
case SerializedPropertyType.LayerMask:
value = value.UnwrapLayerMask();
if (int.TryParse(value, out int intValue))
{
property.intValue = intValue;
}
else
{
LogParseWarning(value, propertyType, "Not a valid int.");
}
break;
case SerializedPropertyType.Enum:
if (!IsDefaultUnityEngineType(serializedObject) && formatSettings.UseStringEnums)
{
if (property.TryGetEnumType(m_RootObject, out Type enumType))
{
if (!enumType.HasFlagsAttribute() || value.Contains(UnityConstants.EnumPrefix))
{
try
{
// Unity prefixes single enum values with 'Enum:' when copying from the Inspector.
value = value.Replace(UnityConstants.EnumPrefix, string.Empty);
// Use Enum.Parse instead of TryParse to support older versions of C#.
var enumObject = Enum.Parse(enumType, value, formatSettings.IgnoreCase);
property.intValue = (int) enumObject;
}
catch (Exception ex)
{
LogParseWarning(value, propertyType, ex.Message);
}
break;
}
else
{
value = value.UnwrapLayerMask();
}
}
else
{
LogParseWarning(value, propertyType, "Did not find a valid enum type.");
}
}
if (int.TryParse(value, out int enumValue))
{
property.intValue = enumValue;
}
else
{
LogParseWarning(value, propertyType, "Not a valid int.");
}
break;
case SerializedPropertyType.Boolean:
if (bool.TryParse(value, out bool boolValue))
{
property.boolValue = boolValue;
}
else if (int.TryParse(value, out int boolIntValue))
{
property.boolValue = boolIntValue > 0;
}
else
{
LogParseWarning(value, propertyType, "Not a valid bool.");
}
break;
case SerializedPropertyType.Float:
if (!property.TrySetFloatValue(value))
{
LogParseWarning(value, propertyType, "Not a valid float.");
}
break;
case SerializedPropertyType.String:
if (property.name == UnityConstants.Field.Name)
{
if (!value.Any(c => Path.GetInvalidFileNameChars().Contains(c)))
{
if (value != m_RootObject.name)
{
var assetPath = AssetDatabase.GetAssetPath(m_RootObject);
if (assetPath.Contains($"/{m_RootObject.name}."))
{
var assetRenameResponse = AssetDatabase.RenameAsset(assetPath, value);
if (!string.IsNullOrEmpty(assetRenameResponse))
{
LogParseWarning(value, propertyType, assetRenameResponse);
}
}
else
{
m_RootObject.name = value;
}
}
}
else
{
Debug.LogWarning($"Invalid filename '{value}'.");
}
}
else
{
property.stringValue = value;
}
break;
case SerializedPropertyType.Color:
if (value.Length > 0 && value[0] != '#')
{
value = "#" + value;
}
if (ColorUtility.TryParseHtmlString(value, out Color parsedColor))
{
property.colorValue = parsedColor;
}
else
{
LogParseWarning(value, propertyType, "Not a valid color hex code.");
}
break;
case SerializedPropertyType.ObjectReference:
if (value.Length > 0 && value != NullObjectValue)
{
// Handle case where user might copy from the inspector.
UnityObjectWrapper objectWrapper = null;
if (value.Contains(UnityConstants.ObjectWrapperJSON))
{
value = value.Replace(UnityConstants.ObjectWrapperJSON, string.Empty);
try
{
objectWrapper = JsonConvert.DeserializeObject<UnityObjectWrapper>(value);
}
catch (Exception ex)
{
LogParseWarning(value, propertyType, ex.Message);
break;
}
value = objectWrapper.Guid;
}
var parts = value.Split(AssetDelimiter);
var assetPath = AssetDatabase.GUIDToAssetPath(parts[0]);
if (!string.IsNullOrEmpty(assetPath))
{
// GUIDs for Unity built in extra assets are not unique and must be found with a manual search by filename or localId.
if (assetPath == UnityConstants.Path.BuiltInExtra)
{
Object foundAsset = null;
var friendlyType = property.FriendlyType();
if (objectWrapper != null)
{
foreach (var asset in SerializedPropertyUtility.UnityBuiltInAssets)
{
if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(asset, out string guid, out long localId) && objectWrapper.LocalId == localId)
{
foundAsset = asset;
break;
}
}
if (foundAsset != null)
{
property.objectReferenceValue = foundAsset;
}
else
{
LogParseWarning(value, propertyType, $"Unable to find asset at path '{assetPath}' with {objectWrapper}.");
}
}
else if (parts.Length > 1 && !string.IsNullOrWhiteSpace(parts[1]))
{
foreach (var asset in SerializedPropertyUtility.UnityBuiltInAssets)
{
if (asset.name == parts[1] && friendlyType == asset.GetType().Name)
{
foundAsset = asset;
break;
}
}
if (foundAsset != null)
{
property.objectReferenceValue = foundAsset;
}
else
{
LogParseWarning(value, propertyType, $"Unable to find asset '{parts[1].GetEscapedText()}' at path '{assetPath}'.");
}
}
else
{
LogParseWarning(value, propertyType, $"Unable to find asset with GUID '{parts[0]}' at path '{assetPath}'. Please include the asset name.");
}
}
else
{
if (!IsDefaultUnityEngineType(serializedObject))
{
// Certain Object types like Sprite as well as Sub Assets require the Type to be explicit when loading from an asset path.
var objectType = ReflectionUtility.GetNestedFieldType(m_RootObject.GetType(), property.propertyPath);
if (objectType != null)
{
property.objectReferenceValue = AssetDatabase.LoadAssetAtPath(assetPath, objectType);
}
else
{
property.objectReferenceValue = AssetDatabase.LoadAssetAtPath(assetPath, typeof(Object));
}
}
else
{
property.objectReferenceValue = AssetDatabase.LoadAssetAtPath(assetPath, typeof(Object));
}
// If we still haven't found the exact Object then see if it's a Sub Asset. This can occur with TMPro shared material assets and child GameObjects.
if (parts.Length > 1 && (property.objectReferenceValue == null || property.objectReferenceValue.name != parts[1]))
{
foreach (var subAsset in AssetDatabase.LoadAllAssetsAtPath(assetPath))
{
property.objectReferenceValue = subAsset;
// Validate the asset names match up if possible.
if (property.objectReferenceValue != null && property.objectReferenceValue.name == parts[1])
{
break;
}
}
}
}
}
else
{
LogParseWarning(value, propertyType, $"Unable to find asset path for GUID '{parts[0].GetEscapedText()}'.");
}
}
else
{
property.objectReferenceValue = null;
}
break;
case SerializedPropertyType.Character:
if (value.Length > 0)
{
property.intValue = value[0];
}
else
{
LogParseWarning(value, propertyType, "Char cannot be empty.");
}
break;
case SerializedPropertyType.AnimationCurve:
if (formatSettings.WrapOption.IsJsonUnsafe())
{
value = value.DecodeBase64();
}
// Handle case where user might copy from the inspector.
value = value.Replace("UnityEditor.AnimationCurveWrapperJSON:{\"c", "{\"m_AnimationC");
try
{
var animationCurve = JsonUtility.FromJson<SerializableAnimationCurve>(value).AnimationCurve;
property.animationCurveValue = animationCurve;
}
catch (Exception ex)
{
LogParseWarning(value, propertyType, ex.Message);
}
break;
case SerializedPropertyType.Gradient:
if (formatSettings.WrapOption.IsJsonUnsafe())
{
value = value.DecodeBase64();
}
// Handle case where user might copy from the inspector.
value = value.Replace("UnityEditor.GradientWrapperJSON:{\"g", "{\"m_G");
try
{
var gradient = JsonUtility.FromJson<SerializableGradient>(value).Gradient;
property.SetGradientValue(gradient);
}
catch (Exception ex)
{
LogParseWarning(value, propertyType, ex.Message);
}
break;
case SerializedPropertyType.Generic:
if (property.IsAssetReference())
{
var guidProperty = property.FindPropertyRelative(UnityConstants.Field.AssetRefGuid);
var subObjectNameProperty = property.FindPropertyRelative(UnityConstants.Field.AssetRefSubObjectName);
var subObjectTypeProperty = property.FindPropertyRelative(UnityConstants.Field.AssetRefSubObjectType);
if (guidProperty != null && subObjectNameProperty != null && subObjectTypeProperty != null)
{
// Remove line endings to avoid creating sub asset references when pasting content.
value = value.Replace("\r", string.Empty).Replace("\n", string.Empty);
if (value.Length > 0 && value != NullObjectValue)
{
var parts = value.Split(new string[] { SubAssetDelimiter }, StringSplitOptions.None);
var assetGuid = parts[0];
var assetPath = AssetDatabase.GUIDToAssetPath(assetGuid);
if (!string.IsNullOrEmpty(assetPath))
{
if (parts.Length > 1 && !string.IsNullOrEmpty(parts[1]))
{
var assetName = parts[1];
var subAssets = AssetDatabase.LoadAllAssetRepresentationsAtPath(assetPath);
var subObject = subAssets.FirstOrDefault(s => s.name == assetName);
if (subObject != null)
{
guidProperty.stringValue = assetGuid;
subObjectNameProperty.stringValue = assetName;
subObjectTypeProperty.stringValue = subObject.GetType().AssemblyQualifiedName;
}
else
{
LogParseWarning(value, propertyType, $"Failed to find sub asset '{assetName}' at path '{assetPath}'.");
}
}
else
{
guidProperty.stringValue = assetGuid;
subObjectNameProperty.stringValue = string.Empty;
subObjectTypeProperty.stringValue = string.Empty;
}
}
else
{
LogParseWarning(value, propertyType, $"Failed to find asset with GUID '{assetGuid}'.");
}
}
else
{
guidProperty.stringValue = string.Empty;
subObjectNameProperty.stringValue = string.Empty;
subObjectTypeProperty.stringValue = string.Empty;
}
}
else
{
LogParseWarning(value, propertyType, $"Unsupported type of {UnityConstants.Type.AssetReference}. Unable to find {UnityConstants.Type.AssetReference} properties.");
}
}
else
{
LogParseWarning(value, propertyType, "Unsupported Property Type.");
}
break;
default:
LogParseWarning(value, propertyType, "Unsupported Property Type.");
break;
}
serializedObject.ApplyModifiedProperties();
}
else
{
if (SerializedPropertyUtility.IsArrayElement(m_PropertyPath))
{
Debug.LogWarning($"Unable to find array element at path {m_PropertyPath} on {nameof(Object)} {m_RootObject.name}. Did you update the size of the array?");
}
else
{
Debug.LogError($"Unable to find property at path {m_PropertyPath} on {nameof(Object)} {m_RootObject.name}");
}
}
}
private void LogParseWarning(string value, SerializedPropertyType propertyType, string message)
{
Debug.LogWarning($"Cannot parse '{value.GetEscapedText()}' to type {propertyType} for property at path {m_PropertyPath} on {nameof(Object)} {m_RootObject.name}.\n{message}");
}
public void AddNewLine()
{
var serializedObject = GetSerializedObject();
var property = GetSerializedProperty(serializedObject);
if (property.propertyPath == UnityConstants.Field.Name)
{
return;
}
if (property.propertyType != SerializedPropertyType.String)
{
return;
}
var editor = typeof(EditorGUI).GetField("activeEditor", BindingFlags.Static | BindingFlags.NonPublic)?.GetValue(null) as TextEditor;
if (editor == null)
{
return;
}
editor.Insert('\n');
property.stringValue = editor.text;
serializedObject.ApplyModifiedProperties();
var currentIndex = editor.cursorIndex;
// Unity's default TextFields do not support multilines, brute force it.
editor.cursorIndex = currentIndex;
editor.selectIndex = currentIndex;
EditorApplication.delayCall += () =>
{
if (editor != null)
{
editor.cursorIndex = currentIndex;
editor.selectIndex = currentIndex;
EditorApplication.delayCall += () =>
{
// If the user changed the index inbetween frames then we ignore this second delayed call, which would reset it back.
if (editor != null && editor.cursorIndex != editor.selectIndex)
{
editor.cursorIndex = currentIndex;
editor.selectIndex = currentIndex;
}
};
}
};
}
public bool IsInputFieldProperty(bool isScriptableObject)
{
var property = GetSerializedProperty();
return property.IsInputFieldProperty(isScriptableObject);
}
public bool IsUnityLocalizationProperty()
{
return PropertyPath.EndsWith(UnityConstants.Field.TableReferenceCollectionName) || PropertyPath.EndsWith(UnityConstants.Field.TableEntryReferenceKeyId);
}
// Draw our own selected cell border for properties Unity doesn't automatically.
public bool NeedsSelectionBorder(bool lockNames = false)
{
var property = GetSerializedProperty();
// Name property is readonly on certain fields like Enum TextAssets.
var isNameProperty = property.propertyPath == UnityConstants.Field.Name;
return (!isNameProperty && property.IsReadOnly() || (isNameProperty && lockNames)) || property.propertyType == SerializedPropertyType.AnimationCurve || property.propertyType == SerializedPropertyType.Gradient;
}
// Various default Unity Engine Components have different limitations on how backing fields are serialized and accessed.
private static bool IsDefaultUnityEngineType(SerializedObject serializedObject)
{
return serializedObject.targetObject.GetType().FullName.StartsWith(UnityConstants.Type.UnityEngine);
}
}
}