/*
Yarn Spinner is licensed to you under the terms found in the file LICENSE.md.
*/
using UnityEngine;
using UnityEditor;
#if USE_UNITY_LOCALIZATION
using UnityEngine.Localization;
#endif
using System.Linq;
using System.Collections.Generic;
using System;
using System.Reflection;
using Yarn.Unity.UnityLocalization;
using Yarn.Unity.Attributes;
namespace Yarn.Unity.Editor
{
#nullable enable
///
/// A delegate that renders a serialized property in the Inspector.
///
///
delegate void PropertyRenderer(SerializedProperty property);
///
/// An attribute that allows an editor to override the appearance of a named
/// property in the Inspector.
///
///
/// When applied to a method in a subclass that
/// takes a single argument and returns
/// , the method will be invoked when the editor needs
/// to draw UI for the property (instead of drawing the default property
/// field.)
///
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class CustomUIForAttribute : YarnEditorAttribute
{
///
/// The name of the property that this attribute is for.
///
///
/// This must match a property on a serialized object, and will be used
/// to determine which property to render.
///
public string propertyName;
///
/// Initializes a new with the
/// specified property name.
///
public CustomUIForAttribute(string methodName)
{
this.propertyName = methodName;
}
}
internal static class AttributeExtensions
{
///
/// A struct that represents the result of evaluating an attribute.
///
public readonly struct AttributeEvaluationResult
{
///
/// The type of result this is.
///
public enum ResultType
{
///
/// The attribute was successfully evaluated to a true value.
///
Passed,
///
/// The attribute was successfully evaluated to a false value.
///
Failed,
///
/// The attribute evaluation failed with an error message.
///
Error,
}
///
/// Gets or sets the type of result this is.
///
public readonly ResultType Result;
///
/// Gets or sets a message indicating why the evaluation failed, if
/// it did.
///
/// This value is non- if is equal to .
///
public readonly string? Message;
///
/// Initializes a new AttributeEvaluationResult with the specified
/// result and message.
///
private AttributeEvaluationResult(ResultType result, string? message)
{
this.Result = result;
this.Message = message;
}
///
/// Implicitly converts a boolean value to an .
///
///
/// The resulting will have
/// a of either
/// or , depending on the value of
/// .
///
public static implicit operator AttributeEvaluationResult(bool value)
{
return new AttributeEvaluationResult
(
result: value ? ResultType.Passed : ResultType.Failed,
message: null
);
}
///
/// Implicitly converts a string value to an .
///
///
/// The resulting will have
/// a of .
///
public static implicit operator AttributeEvaluationResult(string errorMessage)
{
return new AttributeEvaluationResult
(
result: ResultType.Error,
message: errorMessage
);
}
}
///
/// Evaluates a on a serialized
/// object.
///
/// An indicating
/// whether the attribute was successfully evaluated and
/// passed.
public static AttributeEvaluationResult Evaluate(this VisibilityAttribute visibilityAttribute, SerializedObject target)
{
if (target.targetObject == null)
{
return "Target object is null";
}
var property = target.FindProperty(visibilityAttribute.Condition);
SerializedPropertyType propertyType;
int enumValue = -1;
bool booleanValue = false;
UnityEngine.Object? objectValue = null;
if (property != null)
{
// Found a serialized property on this object. Is it a type we
// can use?
propertyType = property.propertyType;
switch (property.propertyType)
{
case SerializedPropertyType.Boolean:
booleanValue = property.boolValue;
break;
case SerializedPropertyType.ObjectReference:
objectValue = property.objectReferenceValue;
break;
case SerializedPropertyType.Enum:
enumValue = property.intValue;
break;
default:
return $"{visibilityAttribute.Condition} must be an enum value or boolean, not " + property.type;
}
}
else
{
var prop = target.targetObject.GetType().GetProperty(visibilityAttribute.Condition, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
if (prop != null)
{
// There is! Fetch its value, and check to see if it's a
// type we can use.
var propertyValue = prop.GetValue(target.targetObject);
if (propertyValue is bool booleanPropertyValue)
{
propertyType = SerializedPropertyType.Boolean;
booleanValue = booleanPropertyValue;
}
else if (propertyValue is UnityEngine.Object objectPropertyValue)
{
propertyType = SerializedPropertyType.ObjectReference;
objectValue = (UnityEngine.Object)objectPropertyValue;
}
else if (propertyValue is Enum enumPropertyValue)
{
enumValue = Convert.ToInt32(enumPropertyValue);
propertyType = SerializedPropertyType.Enum;
}
else
{
return $"{visibilityAttribute.Condition} must be an object reference, enum value or boolean, not " + prop.PropertyType.Name;
}
}
else
{
// Failed to find a serialized property, or a C# property.
return $"{visibilityAttribute.Condition} not found";
}
}
bool result;
switch (visibilityAttribute.Mode)
{
case VisibilityAttribute.AttributeMode.BooleanCondition:
switch (propertyType)
{
case SerializedPropertyType.ObjectReference:
result = objectValue != null;
break;
case SerializedPropertyType.Boolean:
result = booleanValue;
break;
default:
// Property is an unhandled type
return $"{visibilityAttribute.Condition} must be a boolean or object reference, not {propertyType}";
}
break;
case VisibilityAttribute.AttributeMode.EnumEquality:
if (propertyType == SerializedPropertyType.Enum)
{
result = enumValue == visibilityAttribute.EnumValue;
}
else
{
return $"{visibilityAttribute.Condition} must be an enum, not a {propertyType}";
}
break;
default:
return $"Unhandled visibility attribute mode {visibilityAttribute.Mode}";
}
if (visibilityAttribute.Invert)
{
result = !result;
}
return result;
}
///
/// Evaluates a on a serialized
/// property.
///
/// An indicating
/// whether the attribute was successfully evaluated and
/// passed.
public static AttributeEvaluationResult Evaluate(this MustNotBeNullAttribute mustNotBeNullAttribute, SerializedProperty property)
{
if (property.propertyType != SerializedPropertyType.ObjectReference)
{
return $"{property.name} must be an object reference";
}
return property.objectReferenceValue != null;
}
///
/// Evaluates a on a serialized
/// property.
///
/// An AttributeEvaluationResult indicating whether the
/// attribute was successfully evaluated and passed.
public static AttributeEvaluationResult Evaluate(this MustNotBeNullWhenAttribute mustNotBeNullWhenAttribute, SerializedProperty property)
{
if (property.propertyType != SerializedPropertyType.ObjectReference)
{
return $"{property.name} must be an object reference";
}
bool ruleApplies;
var targetProperty = property.serializedObject.FindProperty(mustNotBeNullWhenAttribute.Condition);
if (targetProperty == null)
{
return $"Unknown property {mustNotBeNullWhenAttribute.Condition}";
}
switch (targetProperty.propertyType)
{
case SerializedPropertyType.ObjectReference:
ruleApplies = targetProperty.objectReferenceValue != null;
break;
case SerializedPropertyType.Boolean:
ruleApplies = targetProperty.boolValue;
break;
default:
// Property is an unhandled type
return $"{mustNotBeNullWhenAttribute.Condition} must be a boolean or object reference, not {targetProperty.propertyType}";
}
if (!ruleApplies)
{
// The rule doesn't apply, so indicate that we're a-ok
return true;
}
return property.objectReferenceValue != null;
}
///
/// Gets information for showing a message box from a
/// MessageBoxAttribute on a serialized object.
///
/// A with the text
/// and type of the message box.
///
public static MessageBoxAttribute.Message GetMessage(this MessageBoxAttribute messageBoxAttribute, SerializedObject serializedObject)
{
if (serializedObject == null || serializedObject.targetObject == null)
{
return "Serialized object is null";
}
var methodName = messageBoxAttribute.SourceMethod;
if (serializedObject.isEditingMultipleObjects)
{
// If we're editing multiple objects, don't show a message box
return null;
}
var target = serializedObject.targetObject;
var method = serializedObject.targetObject.GetType().GetMethod(methodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
if (method == null)
{
return $@"Failed to find an instance method ""{methodName}"" on this object";
}
if (method.ReturnType != typeof(MessageBoxAttribute.Message))
{
return $@"Method ""{methodName}"" must return a {nameof(MessageBoxAttribute.Message)}";
}
if (method.GetParameters().Length != 0)
{
return $@"Method ""{methodName}"" must not accept any parameters";
}
try
{
var result = method.Invoke(target, Array.Empty