-
Notifications
You must be signed in to change notification settings - Fork 8
/
PersistentVariablesSource.cs
314 lines (273 loc) · 12.6 KB
/
PersistentVariablesSource.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
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
{
/// <summary>
/// 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.
/// </summary>
[Serializable]
public class PersistentVariablesSource : ISource, IDictionary<string, VariablesGroupAsset>, ISerializationCallbackReceiver
{
[Serializable]
class NameValuePair
{
public string name;
[SerializeReference]
public VariablesGroupAsset group;
}
/// <summary>
/// Encapsulates a <see cref="BeginUpdating"/> and <see cref="EndUpdating"/> call.
/// </summary>
public struct ScopedUpdate : IDisposable
{
/// <summary>
/// Calls <see cref="EndUpdating"/>.
/// </summary>
public void Dispose() => EndUpdating();
}
[SerializeField]
List<NameValuePair> m_Groups = new List<NameValuePair>();
Dictionary<string, NameValuePair> m_GroupLookup = new Dictionary<string, NameValuePair>();
internal static int s_IsUpdating;
/// <summary>
/// Has <see cref="BeginUpdating"/> been called?
/// This can be used when updating the value of multiple <see cref="IVariable"/> in order to do
/// a single update after the updates instead of 1 per change.
/// </summary>
public static bool IsUpdating => s_IsUpdating != 0;
/// <summary>
/// The number of <see cref="VariablesGroupAsset"/> that are used for global variables.
/// </summary>
public int Count => m_Groups.Count;
/// <summary>
/// Implmented as part of IDictionary but not used. Will always return <see langword="false"/>.
/// </summary>
public bool IsReadOnly => false;
/// <summary>
/// Returns the global variable group names.
/// </summary>
public ICollection<string> Keys => m_GroupLookup.Keys;
/// <summary>
/// Returns the global variable groups for this source.
/// </summary>
public ICollection<VariablesGroupAsset> Values => m_GroupLookup.Values.Select(k => k.group).ToList();
/// <summary>
/// Returns the global variable group that matches <paramref name="name"/>.
/// </summary>
/// <param name="name">The name of the group to return.</param>
/// <returns></returns>
public VariablesGroupAsset this[string name]
{
get => m_GroupLookup[name].group;
set => Add(name, value);
}
/// <summary>
/// Called after the final <see cref="EndUpdating"/> 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.
/// </summary>
public static event Action EndUpdate;
/// <summary>
/// Creates a new instance and adds the "." operator to the parser.
/// </summary>
/// <param name="formatter"></param>
public PersistentVariablesSource(SmartFormatter formatter)
{
formatter.Parser.AddOperators(".");
}
/// <summary>
/// Indicates that multiple <see cref="IVariable"/> will be changed and <see cref="LocalizedString"/> should wait for <see cref="EndUpdate"/> before updating.
/// See <seealso cref="EndUpdating"/> and <seealso cref="EndUpdate"/>.
/// Note: <see cref="BeginUpdating"/> and <see cref="EndUpdating"/> can be nested, <see cref="EndUpdate"/> will only be called after the last <see cref="EndUpdate"/>.
/// </summary>
public static void BeginUpdating() => s_IsUpdating++;
/// <summary>
/// Indicates that updates to <see cref="IVariable"/> have finished and sends the <see cref="EndUpdate"/> event.
/// Note: <see cref="BeginUpdating"/> and <see cref="EndUpdating"/> can be nested, <see cref="EndUpdate"/> will only be called after the last <see cref="EndUpdate"/>.
/// </summary>
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;
}
}
/// <summary>
/// Can be used to create a <see cref="BeginUpdating"/> and <see cref="EndUpdating"/> scope.
/// </summary>
/// <returns></returns>
public static IDisposable UpdateScope()
{
BeginUpdating();
return new ScopedUpdate();
}
/// <summary>
/// Returns <see langword="true"/> if a global variable group could be found with a matching name, or <see langword="false"/> if one could not.
/// </summary>
/// <param name="name">The name of the global variable group to find.</param>
/// <param name="value">The found global variable group or <see langword="null"/> if one could not be found with a matching name.</param>
/// <returns><see langword="true"/> if a group could be found or <see langword="false"/> if one could not.</returns>
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;
}
/// <summary>
/// Add a global variable group to the source.
/// </summary>
/// <param name="name">The name of the group to add.</param>
/// <param name="group">The group to add.</param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="group"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">Thrown if <paramref name="name"/> is <see langword="null"/> or empty.</exception>
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);
}
/// <inheritdoc cref="Add(string, VariablesGroupAsset)"/>
public void Add(KeyValuePair<string, VariablesGroupAsset> item) => Add(item.Key, item.Value);
/// <summary>
/// Removes the group with the matching name.
/// </summary>
/// <param name="name">The name of the group to remove.</param>
/// <returns><see langword="true"/> if a group with a matching name was found and removed, or <see langword="false"/> if one was not.</returns>
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;
}
/// <inheritdoc cref="Remove(string)"/>
public bool Remove(KeyValuePair<string, VariablesGroupAsset> item) => Remove(item.Key);
/// <summary>
/// Removes all global variables.
/// </summary>
public void Clear()
{
m_GroupLookup.Clear();
m_Groups.Clear();
}
/// <summary>
/// Returns <see langword="true"/> if a global variable group is found with the same name.
/// </summary>
/// <param name="name">The name of the global variable group to check for.</param>
/// <returns><see langword="true"/> if a group with the name is found or <see langword="false"/> if one is not.</returns>
public bool ContainsKey(string name) => m_GroupLookup.ContainsKey(name);
/// <inheritdoc cref="ContainsKey(string)"/>
public bool Contains(KeyValuePair<string, VariablesGroupAsset> item) => TryGetValue(item.Key, out var v) && v == item.Value;
/// <summary>
/// Copy all global variable groups into the provided array starting at <paramref name="arrayIndex"/>.
/// </summary>
/// <param name="array">The array to copy the global variables into.</param>
/// <param name="arrayIndex">The index to start copying into.</param>
public void CopyTo(KeyValuePair<string, VariablesGroupAsset>[] array, int arrayIndex)
{
foreach (var entry in m_GroupLookup)
{
array[arrayIndex++] = new KeyValuePair<string, VariablesGroupAsset>(entry.Key, entry.Value.group);
}
}
/// <summary>
/// Returns an enumerator for all the global variables in the source.
/// </summary>
/// <returns></returns>
IEnumerator<KeyValuePair<string, VariablesGroupAsset>> IEnumerable<KeyValuePair<string, VariablesGroupAsset>>.GetEnumerator()
{
foreach (var v in m_GroupLookup)
{
yield return new KeyValuePair<string, VariablesGroupAsset>(v.Key, v.Value.group);
}
}
/// <summary>
/// Returns an enumerator for all the global variables in the source.
/// </summary>
/// <returns></returns>
public IEnumerator GetEnumerator()
{
foreach (var v in m_GroupLookup)
{
yield return new KeyValuePair<string, VariablesGroupAsset>(v.Key, v.Value.group);
}
}
/// <inheritdoc/>
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<string, NameValuePair>();
m_GroupLookup.Clear();
foreach (var v in m_Groups)
{
if (!string.IsNullOrEmpty(v.name))
{
m_GroupLookup[v.name] = v;
}
}
}
}
}