using System; using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine.Localization.SmartFormat.Core.Extensions; using UnityEngine.Localization.SmartFormat.PersistentVariables; namespace UnityEngine.Localization.SmartFormat.Extensions { /// /// Can be used to provide global or local values that do not need to be passed in as arguments when formatting a string. /// The smart string should take the format {groupName.variableName}. e.g {global.player-score}. /// Note: The group name and variable names must not contain any spaces. /// [Serializable] public class PersistentVariablesSource : ISource, IDictionary, ISerializationCallbackReceiver { [Serializable] class NameValuePair { public string name; [SerializeReference] public VariablesGroupAsset group; } /// /// Encapsulates a and call. /// public struct ScopedUpdate : IDisposable { /// /// Calls . /// public void Dispose() => EndUpdating(); } [SerializeField] List m_Groups = new List(); Dictionary m_GroupLookup = new Dictionary(); internal static int s_IsUpdating; /// /// Has been called? /// This can be used when updating the value of multiple in order to do /// a single update after the updates instead of 1 per change. /// public static bool IsUpdating => s_IsUpdating != 0; /// /// The number of that are used for global variables. /// public int Count => m_Groups.Count; /// /// Implmented as part of IDictionary but not used. Will always return . /// public bool IsReadOnly => false; /// /// Returns the global variable group names. /// public ICollection Keys => m_GroupLookup.Keys; /// /// Returns the global variable groups for this source. /// public ICollection Values => m_GroupLookup.Values.Select(k => k.group).ToList(); /// /// Returns the global variable group that matches . /// /// The name of the group to return. /// public VariablesGroupAsset this[string name] { get => m_GroupLookup[name].group; set => Add(name, value); } /// /// Called after the final has been called. /// This can be used when you wish to respond to value change events but wish to do a /// single update at the end instead of 1 per change. /// For example, if you wanted to change the value of multiple global variables /// that a smart string was using then changing each value would result in a new string /// being generated, by using begin and end the string generation can be deferred until the /// final change so that only 1 update is performed. /// public static event Action EndUpdate; /// /// Creates a new instance and adds the "." operator to the parser. /// /// public PersistentVariablesSource(SmartFormatter formatter) { formatter.Parser.AddOperators("."); } /// /// Indicates that multiple will be changed and should wait for before updating. /// See and . /// Note: and can be nested, will only be called after the last . /// public static void BeginUpdating() => s_IsUpdating++; /// /// Indicates that updates to have finished and sends the event. /// Note: and can be nested, will only be called after the last . /// public static void EndUpdating() { s_IsUpdating--; if (s_IsUpdating == 0) { EndUpdate?.Invoke(); } else if (s_IsUpdating < 0) { Debug.LogWarning($"Incorrect number of Begin and End calls to {nameof(PersistentVariablesSource)}. {nameof(BeginUpdating)} must be called before {nameof(EndUpdating)}."); s_IsUpdating = 0; } } /// /// Can be used to create a and scope. /// /// public static IDisposable UpdateScope() { BeginUpdating(); return new ScopedUpdate(); } /// /// Returns if a global variable group could be found with a matching name, or if one could not. /// /// The name of the global variable group to find. /// The found global variable group or if one could not be found with a matching name. /// if a group could be found or if one could not. public bool TryGetValue(string name, out VariablesGroupAsset value) { if (m_GroupLookup.TryGetValue(name, out var v)) { value = v.group; return true; } value = null; return false; } /// /// Add a global variable group to the source. /// /// The name of the group to add. /// The group to add. /// Thrown if is . /// Thrown if is or empty. public void Add(string name, VariablesGroupAsset group) { if (string.IsNullOrEmpty(name)) throw new ArgumentException(nameof(name), "Name must not be null or empty."); if (group == null) throw new ArgumentNullException(nameof(group)); var pair = new NameValuePair { name = name, group = group }; name = name.ReplaceWhiteSpaces("-"); m_GroupLookup[name] = pair; m_Groups.Add(pair); } /// public void Add(KeyValuePair item) => Add(item.Key, item.Value); /// /// Removes the group with the matching name. /// /// The name of the group to remove. /// if a group with a matching name was found and removed, or if one was not. public bool Remove(string name) { if (m_GroupLookup.TryGetValue(name, out var v)) { m_Groups.Remove(v); m_GroupLookup.Remove(name); return true; } return false; } /// public bool Remove(KeyValuePair item) => Remove(item.Key); /// /// Removes all global variables. /// public void Clear() { m_GroupLookup.Clear(); m_Groups.Clear(); } /// /// Returns if a global variable group is found with the same name. /// /// The name of the global variable group to check for. /// if a group with the name is found or if one is not. public bool ContainsKey(string name) => m_GroupLookup.ContainsKey(name); /// public bool Contains(KeyValuePair item) => TryGetValue(item.Key, out var v) && v == item.Value; /// /// Copy all global variable groups into the provided array starting at . /// /// The array to copy the global variables into. /// The index to start copying into. public void CopyTo(KeyValuePair[] array, int arrayIndex) { foreach (var entry in m_GroupLookup) { array[arrayIndex++] = new KeyValuePair(entry.Key, entry.Value.group); } } /// /// Returns an enumerator for all the global variables in the source. /// /// IEnumerator> IEnumerable>.GetEnumerator() { foreach (var v in m_GroupLookup) { yield return new KeyValuePair(v.Key, v.Value.group); } } /// /// Returns an enumerator for all the global variables in the source. /// /// public IEnumerator GetEnumerator() { foreach (var v in m_GroupLookup) { yield return new KeyValuePair(v.Key, v.Value.group); } } /// public bool TryEvaluateSelector(ISelectorInfo selectorInfo) { var selector = selectorInfo.SelectorText; // First we test the current value if (selectorInfo.CurrentValue is IVariableGroup grp && EvaluateLocalGroup(selectorInfo, grp)) return true; // If we are at the root we also test the local variables if (selectorInfo.SelectorOperator == "" && EvaluateLocalGroup(selectorInfo, selectorInfo.FormatDetails.FormatCache?.LocalVariables)) return true; if (TryGetValue(selector, out var group)) { selectorInfo.Result = group; return true; } return false; } static bool EvaluateLocalGroup(ISelectorInfo selectorInfo, IVariableGroup variablleGroup) { if (variablleGroup == null) return false; if (variablleGroup != null && variablleGroup.TryGetValue(selectorInfo.SelectorText, out var variable)) { // Add the variable to the cache var cache = selectorInfo.FormatDetails.FormatCache; if (cache != null && variable is IVariableValueChanged valueChanged) { if (!cache.VariableTriggers.Contains(valueChanged)) cache.VariableTriggers.Add(valueChanged); } selectorInfo.Result = variable.GetSourceValue(selectorInfo); return true; } return false; } public void OnBeforeSerialize() {} public void OnAfterDeserialize() { if (m_GroupLookup == null) m_GroupLookup = new Dictionary(); m_GroupLookup.Clear(); foreach (var v in m_Groups) { if (!string.IsNullOrEmpty(v.name)) { m_GroupLookup[v.name] = v; } } } } }