/* Yarn Spinner is licensed to you under the terms found in the file LICENSE.md. */ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using UnityEngine; using BoolDictionary = System.Collections.Generic.Dictionary; using FloatDictionary = System.Collections.Generic.Dictionary; using StringDictionary = System.Collections.Generic.Dictionary; #nullable enable namespace Yarn.Unity { /// /// A that a uses /// to store and retrieve variables. /// /// /// This abstract class inherits from , which /// means that subclasses of this class can be attached to s. /// public abstract class VariableStorageBehaviour : MonoBehaviour, Yarn.IVariableStorage { public Program? Program { get; set; } public ISmartVariableEvaluator? SmartVariableEvaluator { get; set; } /// public abstract bool TryGetValue(string variableName, [NotNullWhen(true)] out T? result); /// public abstract void SetValue(string variableName, string stringValue); /// public abstract void SetValue(string variableName, float floatValue); /// public abstract void SetValue(string variableName, bool boolValue); /// /// Notifies all currently registered change listeners that the variable /// named has changed value. /// /// The type of the variable. /// The name of the variable that changed /// value. /// The new value of the variable. protected void NotifyVariableChanged(string variableName, T newValue) { if (changeListeners.TryGetValue(variableName, out var delegates)) { foreach (var listener in delegates) { listener.DynamicInvoke(newValue); } } foreach (var listener in globalChangeListeners) { listener.DynamicInvoke(variableName, newValue); } } /// public abstract void Clear(); /// /// Returns a boolean value representing if a particular variable is /// inside the variable storage. /// /// The name of the variable to check /// for. /// if this variable storage contains a /// value for the variable named ; otherwise. public abstract bool Contains(string variableName); /// /// Provides a unified interface for loading many variables all at once. /// Will override anything already in the variable storage. /// /// Should the load also wipe the storage. Defaults /// to true so all existing variables will be cleared. /// public abstract void SetAllVariables(FloatDictionary floats, StringDictionary strings, BoolDictionary bools, bool clear = true); /// /// Provides a unified interface for exporting all variables. Intended /// to be a point for custom saving, editors, etc. /// public abstract (FloatDictionary FloatVariables, StringDictionary StringVariables, BoolDictionary BoolVariables) GetAllVariables(); public VariableKind GetVariableKind(string name) { if (this.Contains(name)) { return VariableKind.Stored; } else if (this.Program != null) { return Program.GetVariableKind(name); } else { Debug.Log($"Unable to determine kind of variable {name}: it is not stored in this variable storage, and {nameof(Program)} is not set"); return VariableKind.Unknown; } } private struct ChangeListenerDisposable : IDisposable { private Delegate listener; private readonly List listeners; public ChangeListenerDisposable(List listeners, Delegate listener) { this.listeners = listeners; this.listener = listener; } public readonly void Dispose() { listeners.Remove(listener); } } private Dictionary> changeListeners = new(); private List globalChangeListeners = new(); /// /// Registers a delegate that will be called when the variable is modified. /// /// The type of the variable. /// The name of the variable to watch for /// changes to. This variable must be of type , /// and it must not be a smart variable. /// The delegate to run when the variable changes /// value. /// An that removes the registration /// when its method is /// called. /// Called when is not set. /// Called when is not the name of a valid variable, or if /// does not match the type of the /// variable. public IDisposable AddChangeListener(string variableName, Action onChange) { if (Program == null) { throw new InvalidOperationException($"Can't add a change listener for {variableName}: {nameof(Program)} is not set"); } var kind = this.GetVariableKind(variableName); if (kind == VariableKind.Smart) { throw new ArgumentException($"Can't add a change listener for {variableName}: change listeners cannot be added for {VariableKind.Smart} variables."); } if (changeListeners.TryGetValue(variableName, out var list) == false) { list = new List(); changeListeners[variableName] = list; } if (kind == VariableKind.Stored) { var type = this.Program.InitialValues[variableName].ValueCase; if (type == Operand.ValueOneofCase.BoolValue && typeof(T) != typeof(bool)) { throw new ArgumentException($"Can't add a {typeof(T)} change listener for {variableName}: must be a {typeof(bool)}"); } if (type == Operand.ValueOneofCase.StringValue && typeof(T) != typeof(string)) { throw new ArgumentException($"Can't add a {typeof(T)} change listener for {variableName}: must be a {typeof(string)}"); } if (type == Operand.ValueOneofCase.FloatValue && typeof(float).IsAssignableFrom(typeof(T)) == false) { throw new ArgumentException($"Can't add a {typeof(T)} change listener for {variableName}: must be a number"); } } list.Add(onChange); return new ChangeListenerDisposable(list, onChange); } /// /// Registers a delegate that will be called when any variable is modified. /// /// The delegate to run when the variable changes /// value. /// An that removes the registration /// when its method is /// called. public IDisposable AddChangeListener(System.Action onChange) { globalChangeListeners ??= new(); globalChangeListeners.Add(onChange); return new ChangeListenerDisposable(globalChangeListeners, onChange); } } }