/* 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()); if (result is MessageBoxAttribute.Message message) { return message; } else { return $@"Method ""{methodName}"" did not return a valid message"; } } catch (TargetInvocationException e) { Debug.LogException(e.InnerException); return $@"Method ""{methodName}"" threw a {e.InnerException.GetType().Name}: {e.InnerException.Message ?? "(no message)"}"; } catch (Exception e) { Debug.LogException(e, target); return $@"{e.GetType().Name} thrown when calling ""{methodName}"": {e.Message ?? "(no message)"}"; } } /// /// 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 string GetLabel(this LabelFromAttribute 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, show only a placeholder return "-"; } 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(string)) { return $@"Method ""{methodName}"" must return a string"; } if (method.GetParameters().Length != 0) { return $@"Method ""{methodName}"" must not accept any parameters"; } try { var result = method.Invoke(target, Array.Empty()); if (result is string message) { return message; } else { return $@"Method ""{methodName}"" did not return a valid message"; } } catch (TargetInvocationException e) { Debug.LogException(e.InnerException); return $@"Method ""{methodName}"" threw a {e.InnerException.GetType().Name}: {e.InnerException.Message ?? "(no message)"}"; } catch (Exception e) { Debug.LogException(e, target); return $@"{e.GetType().Name} thrown when calling ""{methodName}"": {e.Message ?? "(no message)"}"; } } } /// /// Contains information about a property in a serialized object relevant to /// the Yarn Spinner attribute system. /// readonly struct PropertyInfo { /// /// A property on a . /// public readonly SerializedProperty serializedProperty; /// /// The collection of Yarn Editor attributes on the field that represents. /// public readonly YarnEditorAttribute[] attributes; /// /// The field that represents. /// private readonly FieldInfo? field; /// /// Gets the collection of all attributes associated with the field that /// a represents. /// /// /// public static IEnumerable GetAttributes(SerializedProperty property) { // The script property doesn't correspond to a field on the target // object, so we won't find one when we ask for it. In this // situation, always return an empty collection if we're getting a // property with that name if (property.name == YarnEditor.ScriptPropertyName) { return Array.Empty(); } // Attempt to find the field that backs the property FieldInfo? field = GetField(property); if (field != null) { // We found the field; get all custom attributes from it return field.GetCustomAttributes(); } else { // We didn't find it. This is generally an error; we'll complain // about it and return an empty collection of attributes so that // we don't break the Inspector. Debug.LogWarning($"Failed to find field {property.name} on object {property.serializedObject.targetObject.name}"); return Array.Empty(); } } private static FieldInfo? GetField(SerializedProperty property) { var target = property.serializedObject.targetObject; var field = target.GetType().GetField(property.name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); return field; } /// /// Initialises a new given a serialized /// property. /// /// The serialized property to create the from. public PropertyInfo(SerializedProperty property) { this.serializedProperty = property; this.attributes = GetAttributes(property).OfType().ToArray(); this.field = GetField(property); } /// /// Get a value indicating whether was /// declared on a parent type of its . /// public bool IsInherited { get { if (this.field == null) { return false; } var owner = this.serializedProperty.serializedObject.targetObject; var ownerType = owner.GetType(); var declaringType = this.field.DeclaringType; return ownerType == declaringType; } } } /// /// A custom editor that makes use of /// attributes to control the appearance of variables in the Inspector. /// /// /// /// To use this editor for your classes, create a subclass of it and use the /// attribute to mark it as the editor for your /// type. The Yarn Editor attributes will then start working in the /// Inspector for your objects. /// /// public abstract class YarnEditor : UnityEditor.Editor { internal const string ScriptPropertyName = "m_Script"; private string? currentGroup; private Dictionary propertyInfos = new Dictionary(); private Dictionary customPropertyRenderers = new Dictionary(); private List<(string Message, MessageType Type)> messageBoxes = new List<(string, MessageType)>(); /// /// Draws a single property in the Inspector. /// /// private void DrawPropertyField(PropertyInfo property) { int indentation = 0; GroupAttribute? group = null; string? label = null; this.messageBoxes.Clear(); // Get all relevant attributes for this property and get information // from it. foreach (var attr in property.attributes) { AttributeExtensions.AttributeEvaluationResult result; switch (attr) { case VisibilityAttribute visibilityAttribute: result = visibilityAttribute.Evaluate(property.serializedProperty.serializedObject); if (result.Result == AttributeExtensions.AttributeEvaluationResult.ResultType.Failed) { // A visibility attribute has indicated that we // shouldn't show the field, so exit from this // method early and don't draw the property. return; } break; case IndentAttribute indentAttribute: indentation = indentAttribute.indentLevel; result = true; break; case GroupAttribute groupAttribute: group = groupAttribute; result = true; break; case LabelAttribute labelAttribute: label = labelAttribute.Label; result = true; break; case MustNotBeNullAttribute mustNotBeNullAttribute: result = mustNotBeNullAttribute.Evaluate(property.serializedProperty); if (result.Result == AttributeExtensions.AttributeEvaluationResult.ResultType.Failed) { messageBoxes.Add((mustNotBeNullAttribute.Label ?? $"{ObjectNames.NicifyVariableName(property.serializedProperty.name)} must not be null", MessageType.Error)); } break; case MustNotBeNullWhenAttribute mustNotBeNullWhenAttribute: result = mustNotBeNullWhenAttribute.Evaluate(property.serializedProperty); if (result.Result == AttributeExtensions.AttributeEvaluationResult.ResultType.Failed) { messageBoxes.Add((mustNotBeNullWhenAttribute.Label ?? $"{ObjectNames.NicifyVariableName(property.serializedProperty.name)} must not be " + $"null when {ObjectNames.NicifyVariableName(mustNotBeNullWhenAttribute.Condition)} is set", MessageType.Error)); } break; case MessageBoxAttribute messageBoxAttribute: MessageBoxAttribute.Message message = messageBoxAttribute.GetMessage(property.serializedProperty.serializedObject); if (message.text != null) { messageBoxes.Add((message.text, message.type switch { MessageBoxAttribute.Type.Info => MessageType.Info, MessageBoxAttribute.Type.Warning => MessageType.Warning, MessageBoxAttribute.Type.Error => MessageType.Error, _ => MessageType.None })); } result = true; break; case LabelFromAttribute labelFromAttribute: label = labelFromAttribute.GetLabel(property.serializedProperty.serializedObject); result = true; break; default: result = $"Unknown attribute {attr.GetType()}"; break; } if (result.Result == AttributeExtensions.AttributeEvaluationResult.ResultType.Error) { messageBoxes.Add((result.Message ?? "Unknown error", MessageType.Error)); } } // Gets a unique string ID for a given group on a specific object. string GetGroupID(GroupAttribute group) { var target = property.serializedProperty.serializedObject.targetObject; string uniqueID; #if UNITY_6000_4_OR_NEWER uniqueID = target.GetEntityId().ToString(); #else uniqueID = target.GetInstanceID().ToString(); #endif var uniqueGroupID = $"{target.GetType()}_{uniqueID}_group_{group.GroupName}"; return uniqueGroupID; } // Renders the header of a group. If the group is a foldout, renders // the header and manages its state. void StartGroup(GroupAttribute group) { if (group.FoldOut) { var uniqueGroupID = GetGroupID(group); var isToggled = SessionState.GetBool(uniqueGroupID, false); GUIContent content = new GUIContent(group.GroupName); isToggled = EditorGUILayout.Foldout(isToggled, content, EditorStyles.foldoutHeader); SessionState.SetBool(uniqueGroupID, isToggled); } else { EditorGUILayout.LabelField(group.GroupName, EditorStyles.boldLabel); } EditorGUI.indentLevel += 1; } void EndGroup() { EditorGUI.indentLevel -= 1; EditorGUILayout.Space(); } // Figure out if we're starting a new group, leaving the group, or // switching to a new group if (currentGroup == null && group != null) { // We've started a group. StartGroup(group); } else if (currentGroup != null && group == null) { // We've left the current group. EndGroup(); } else if (currentGroup != null && group != null && currentGroup.Equals(group.GroupName, StringComparison.Ordinal) == false) { // We've changed group. EndGroup(); StartGroup(group); } currentGroup = group?.GroupName; if (group?.FoldOut ?? false) { var id = GetGroupID(group); var isOpen = SessionState.GetBool(id, false); if (!isOpen) { // We're in a group that's not open. Don't render this // property. return; } } EditorGUI.indentLevel += indentation; if (customPropertyRenderers.TryGetValue(property.serializedProperty.name, out var customRenderer)) { // We have a custom renderer for this property - use it! customRenderer.Invoke(property.serializedProperty); } else { // Use the default renderer for this property if (label == null) { EditorGUILayout.PropertyField(property.serializedProperty); } else { EditorGUILayout.PropertyField(property.serializedProperty, new GUIContent(label)); } } // Render the message boxes we've accumulated foreach (var box in messageBoxes) { EditorGUILayout.HelpBox(box.Message, box.Type); } EditorGUI.indentLevel -= indentation; } private void DrawPropertyField(SerializedProperty property) { if (propertyInfos.TryGetValue(property.name, out var propertyInfo)) { DrawPropertyField(propertyInfo); } else { // We don't have a PropertyInfo for this property - just draw // the default field EditorGUILayout.PropertyField(property); } } private void OnEnable() { propertyInfos.Clear(); customPropertyRenderers.Clear(); // Build a dictionary of property names to SerializedProperties Dictionary namesToProperties = new Dictionary(); // Find all properties on the target, and build a PropertyInfo // struct for it that will contain all of its relevant attributes. var propertyIterator = serializedObject.GetIterator(); propertyIterator.NextVisible(true); do { var propertyInfo = new PropertyInfo(propertyIterator.Copy()); propertyInfos.Add(propertyIterator.name, propertyInfo); namesToProperties[propertyIterator.name] = propertyIterator.Copy(); } while (propertyIterator.NextVisible(false)); // Find all methods on this object that have a CustomUI attribute, // and remember that that method should be called for handling the // relevant property. var methods = this.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (var method in methods) { var attr = method.GetCustomAttribute(); if (attr == null) { // No CustomUI attribute on this method. Move on. continue; } // Does the attribute reference a property that exists? if (propertyInfos.TryGetValue(attr.propertyName, out var prop) == false) { // We don't have a property named attr.propertyName. Log a // warning about it. Debug.LogWarning($"{serializedObject.targetObject.GetType()} has no property '{attr.propertyName}' (or it is not visible)"); continue; } // Does the attribute reference a property that we already have // a renderer for? if (this.customPropertyRenderers.ContainsKey(attr.propertyName)) { Debug.LogWarning($"{nameof(DialogueRunnerEditor)} already has a custom renderer for {attr.propertyName}"); continue; } PropertyRenderer propertyRenderer = (PropertyRenderer)method.CreateDelegate(typeof(PropertyRenderer), this); customPropertyRenderers.Add(attr.propertyName, propertyRenderer); } } /// /// Draws the Inspector for the edited object. /// public override void OnInspectorGUI() { this.currentGroup = null; // Start iterating the list of properties var currentProperty = serializedObject.GetIterator(); currentProperty.NextVisible(true); do { if (currentProperty.name == ScriptPropertyName) { // Don't draw the 'script' property continue; } DrawPropertyField(currentProperty); } while (currentProperty.NextVisible(false)); serializedObject.ApplyModifiedProperties(); } } /// /// The editor for objects. /// [CanEditMultipleObjects] [CustomEditor(typeof(VoiceOverPresenter))] public class VoiceOverPresenterEditor : YarnEditor { } /// /// The editor for objects. /// [CanEditMultipleObjects] [CustomEditor(typeof(LinePresenter))] public class LinePresenterEditor : YarnEditor { } /// /// The editor for objects. /// [CanEditMultipleObjects] [CustomEditor(typeof(OptionsPresenter))] public class OptionsPresenterEditor : YarnEditor { } /// /// The editor for objects. /// [CanEditMultipleObjects] [CustomEditor(typeof(LineAdvancer))] public class LineAdvancerEditor : YarnEditor { } /// /// The editor for objects. /// [CanEditMultipleObjects] [CustomEditor(typeof(BuiltinLocalisedLineProvider))] public class BuiltinLocalisedLineProviderEditor : YarnEditor { } /// /// The editor for objects. /// [CanEditMultipleObjects] [CustomEditor(typeof(DialogueRunner))] public class DialogueRunnerEditor : YarnEditor { private const string docsLabel = "Docs"; private const string samplesLabel = "Samples"; private const string discordLabel = "Discord"; private const string tellUsLabel = "Tell us about your game!"; private const string docsURL = "https://docs.yarnspinner.dev/"; private const string discordURL = "https://discord.com/invite/yarnspinner"; private const string tellUsURL = "https://yarnspinner.dev/tell-us"; private const int logoMaxWidth = 240; // px, because links line is about 350px wide private static GUIStyle? _urlStyle = null; private static GUIStyle UrlStyle { get { if (_urlStyle == null) { _urlStyle = new GUIStyle(GUI.skin.label); _urlStyle.richText = true; } return _urlStyle; } } private static Texture2D? _yarnSpinnerLogo = null; private static Texture2D YarnSpinnerLogo { get { if (_yarnSpinnerLogo == null) { string? logoPath = AssetDatabase.GUIDToAssetPath("16f8cd23bf0d0480bb8ecc39be853cda"); _yarnSpinnerLogo = AssetDatabase.LoadAssetAtPath(logoPath); } return _yarnSpinnerLogo; } } internal static void DrawYarnSpinnerHeader() { bool MakeLinkButton(string labelText) { #if UNITY_6000_0_OR_NEWER string styledText = "" + labelText + ""; #else // Underlines aren't available in earlier versions of Unity string styledText = "" + labelText + ""; #endif return GUILayout.Button(styledText, UrlStyle, GUILayout.ExpandWidth(false)); } void InstallSamples() { try { // if we have the samples already installed we can just use them // we don't really care HOW they got them at this point // for now just open the package manager, later Mars wanted to add in a wizard here if (YarnPackageImporter.IsSamplesPackageInstalled) { YarnPackageImporter.OpenSamplesUI(); } else { // we don't have the samples installed YarnPackageImporter.InstallSamples(); } } catch (YarnPackageImporterException ex) { // TODO show error dialogue // for now just log it Debug.LogException(ex); } } EditorGUILayout.Space(); EditorGUILayout.BeginHorizontal(GUILayout.ExpandWidth(false)); GUILayout.FlexibleSpace(); // centre by padding from left // https://discussions.unity.com/t/how-to-display-an-image-logo-in-a-custom-editor/528405/9 float imageWidth = Math.Min(EditorGUIUtility.currentViewWidth - 40, logoMaxWidth); float imageHeight = imageWidth * YarnSpinnerLogo.height / YarnSpinnerLogo.width; Rect rect = GUILayoutUtility.GetRect(imageWidth, imageHeight); GUI.DrawTexture(rect, YarnSpinnerLogo, ScaleMode.ScaleToFit); GUILayout.FlexibleSpace(); // centre by padding from right EditorGUILayout.EndHorizontal(); Rect linksLine = EditorGUILayout.BeginHorizontal(GUILayout.ExpandWidth(false)); GUILayout.FlexibleSpace(); // centre by padding from left if (MakeLinkButton(docsLabel)) { Application.OpenURL(docsURL); } // we default to assuming most of the time we don't need to change the visuals of the button // but if there is an installation request and it isn't yet complete // we set the button to be disabled var isActive = true; if (YarnPackageImporter.Status == YarnPackageImporter.SamplesPackageStatus.Installing) { isActive = false; } GUI.enabled = isActive; if (MakeLinkButton(samplesLabel)) { InstallSamples(); } GUI.enabled = true; if (MakeLinkButton(discordLabel)) { Application.OpenURL(discordURL); } if (MakeLinkButton(tellUsLabel)) { Application.OpenURL(tellUsURL); } GUILayout.FlexibleSpace(); // centre by padding from right EditorGUILayout.EndHorizontal(); EditorGUIUtility.AddCursorRect(linksLine, MouseCursor.Link); EditorGUILayout.Space(); } public override void OnInspectorGUI() { DrawYarnSpinnerHeader(); base.OnInspectorGUI(); } /// /// Draws the variable storage property in the inspector. If it's null, /// shows an info box. /// [CustomUIFor(nameof(DialogueRunner.variableStorage))] public void DrawVariableStorage(SerializedProperty variableStorageProperty) { // Draw the property. If it's null, show an info box. EditorGUILayout.PropertyField(variableStorageProperty); if (variableStorageProperty.objectReferenceValue == null) { EditorGUI.indentLevel += 1; EditorGUILayout.HelpBox($"An {ObjectNames.NicifyVariableName(nameof(InMemoryVariableStorage))} component will be added at run time.", MessageType.Info); EditorGUI.indentLevel -= 1; } } /// /// Draws the line provider property in the inspector. /// [CustomUIFor(nameof(DialogueRunner.lineProvider))] public void DrawLineProvider(SerializedProperty lineProviderProperty) { // Draw the line provider. EditorGUILayout.PropertyField(lineProviderProperty); // Check to see if the line provider is using Unity Localization. If // it is, offer some tools to help set it up, if needed. var yarnProjectProperty = lineProviderProperty.serializedObject.FindProperty(nameof(DialogueRunner.yarnProject)); var yarnProject = yarnProjectProperty.objectReferenceValue as YarnProject; var yarnProjectIsUnityLoc = yarnProject != null && yarnProject.localizationType == LocalizationType.Unity; if (lineProviderProperty.objectReferenceValue == null) { // We don't have a line provider. EditorGUI.indentLevel += 1; if (yarnProjectIsUnityLoc) { #if USE_UNITY_LOCALIZATION // If this is a project using Unity localisation, we can't // add a line provider at runtime because we won't know what // string table it should use. In this situation, we'll show // a warning and offer a quick button they can click to add // one. string unityLocalizedLineProvider = ObjectNames.NicifyVariableName(nameof(UnityLocalization.UnityLocalisedLineProvider)); EditorGUILayout.HelpBox($"This project uses Unity Localization. You will need to add a {unityLocalizedLineProvider} for it to work. Click the button below to add one, and then set it up.", MessageType.Warning); GameObject gameObject = (serializedObject.targetObject as DialogueRunner)!.gameObject; UnityLocalization.UnityLocalisedLineProvider existingLineProvider = gameObject.GetComponent(); if (existingLineProvider != null) { // If there is an existing UnityLocalizedLineProvider, // offer a button to use it. if (GUILayout.Button($"Use {unityLocalizedLineProvider}")) { lineProviderProperty.objectReferenceValue = existingLineProvider; } } else { // If there isn't an existing // UnityLocalizedLineProvider, offer a button to add it. if (GUILayout.Button($"Add {unityLocalizedLineProvider}")) { var lineProvider = gameObject.AddComponent(); lineProviderProperty.objectReferenceValue = lineProvider; } } #else EditorGUILayout.HelpBox($"This project uses Unity Localization, but Unity Localization is not installed. Please install it, or change this Yarn Project to use Yarn Spinner's internal localisation system.", MessageType.Error); #endif } else { // Otherwise, we'll assume they're using the built-in // localisation system, and we can safely create one at // runtime because we know everything we need to to set that // up. EditorGUILayout.HelpBox($"A {ObjectNames.NicifyVariableName(nameof(BuiltinLocalisedLineProvider))} component will be added at run time.", MessageType.Info); } EditorGUI.indentLevel -= 1; } else { // We do have a line provider. // If it's a BuiltInLocalisationLineProvider and the project is // using Unity localization, that's probably a mistake, and we // should warn the user. Type lineProviderType = lineProviderProperty.objectReferenceValue.GetType(); bool lineProviderIsUnityLineProvider = typeof(UnityLocalisedLineProvider).IsAssignableFrom(lineProviderType); if (yarnProjectIsUnityLoc && lineProviderIsUnityLineProvider == false) { #if USE_UNITY_LOCALIZATION EditorGUILayout.HelpBox($"This project uses Unity Localization, but you are using a {ObjectNames.NicifyVariableName(lineProviderType.Name)}. You should use a {ObjectNames.NicifyVariableName(nameof(UnityLocalisedLineProvider))} instead.", MessageType.Warning); #else EditorGUILayout.HelpBox($"This project uses Unity Localization, but Unity Localization is not installed. Please install it, or change this Yarn Project to use Yarn Spinner's internal localisation system.", MessageType.Error); #endif } } } } }