/
VariablesState.cs
417 lines (347 loc) · 15.5 KB
/
VariablesState.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
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
using System;
using System.Collections.Generic;
namespace Ink.Runtime
{
/// <summary>
/// Encompasses all the global variables in an ink Story, and
/// allows binding of a VariableChanged event so that that game
/// code can be notified whenever the global variables change.
/// </summary>
public class VariablesState : IEnumerable<string>
{
public delegate void VariableChanged(string variableName, Runtime.Object newValue);
public event VariableChanged variableChangedEvent;
public StatePatch patch;
public bool batchObservingVariableChanges
{
get {
return _batchObservingVariableChanges;
}
set {
_batchObservingVariableChanges = value;
if (value) {
_changedVariablesForBatchObs = new HashSet<string> ();
}
// Finished observing variables in a batch - now send
// notifications for changed variables all in one go.
else {
if (_changedVariablesForBatchObs != null) {
foreach (var variableName in _changedVariablesForBatchObs) {
var currentValue = _globalVariables [variableName];
variableChangedEvent (variableName, currentValue);
}
}
_changedVariablesForBatchObs = null;
}
}
}
bool _batchObservingVariableChanges;
// Allow StoryState to change the current callstack, e.g. for
// temporary function evaluation.
public CallStack callStack {
get {
return _callStack;
}
set {
_callStack = value;
}
}
/// <summary>
/// Get or set the value of a named global ink variable.
/// The types available are the standard ink types. Certain
/// types will be implicitly casted when setting.
/// For example, doubles to floats, longs to ints, and bools
/// to ints.
/// </summary>
public object this[string variableName]
{
get {
Runtime.Object varContents;
if (patch != null && patch.TryGetGlobal(variableName, out varContents))
return (varContents as Runtime.Value).valueObject;
// Search main dictionary first.
// If it's not found, it might be because the story content has changed,
// and the original default value hasn't be instantiated.
// Should really warn somehow, but it's difficult to see how...!
if ( _globalVariables.TryGetValue (variableName, out varContents) ||
_defaultGlobalVariables.TryGetValue(variableName, out varContents) )
return (varContents as Runtime.Value).valueObject;
else {
return null;
}
}
set {
if (!_defaultGlobalVariables.ContainsKey (variableName))
throw new StoryException ("Cannot assign to a variable ("+variableName+") that hasn't been declared in the story");
var val = Runtime.Value.Create(value);
if (val == null) {
if (value == null) {
throw new Exception ("Cannot pass null to VariableState");
} else {
throw new Exception ("Invalid value passed to VariableState: "+value.ToString());
}
}
SetGlobal (variableName, val);
}
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
/// <summary>
/// Enumerator to allow iteration over all global variables by name.
/// </summary>
public IEnumerator<string> GetEnumerator()
{
return _globalVariables.Keys.GetEnumerator();
}
public VariablesState (CallStack callStack, ListDefinitionsOrigin listDefsOrigin)
{
_globalVariables = new Dictionary<string, Object> ();
_callStack = callStack;
_listDefsOrigin = listDefsOrigin;
}
public void ApplyPatch()
{
foreach(var namedVar in patch.globals) {
_globalVariables[namedVar.Key] = namedVar.Value;
}
if(_changedVariablesForBatchObs != null ) {
foreach (var name in patch.changedVariables)
_changedVariablesForBatchObs.Add(name);
}
patch = null;
}
public void SetJsonToken(Dictionary<string, object> jToken)
{
_globalVariables.Clear();
foreach (var varVal in _defaultGlobalVariables) {
object loadedToken;
if( jToken.TryGetValue(varVal.Key, out loadedToken) ) {
_globalVariables[varVal.Key] = Json.JTokenToRuntimeObject(loadedToken);
} else {
_globalVariables[varVal.Key] = varVal.Value;
}
}
}
/// <summary>
/// When saving out JSON state, we can skip saving global values that
/// remain equal to the initial values that were declared in ink.
/// This makes the save file (potentially) much smaller assuming that
/// at least a portion of the globals haven't changed. However, it
/// can also take marginally longer to save in the case that the
/// majority HAVE changed, since it has to compare all globals.
/// It may also be useful to turn this off for testing worst case
/// save timing.
/// </summary>
public static bool dontSaveDefaultValues = true;
public void WriteJson(SimpleJson.Writer writer)
{
writer.WriteObjectStart();
foreach (var keyVal in _globalVariables)
{
var name = keyVal.Key;
var val = keyVal.Value;
if(dontSaveDefaultValues) {
// Don't write out values that are the same as the default global values
Runtime.Object defaultVal;
if (_defaultGlobalVariables.TryGetValue(name, out defaultVal))
{
if (RuntimeObjectsEqual(val, defaultVal))
continue;
}
}
writer.WritePropertyStart(name);
Json.WriteRuntimeObject(writer, val);
writer.WritePropertyEnd();
}
writer.WriteObjectEnd();
}
public bool RuntimeObjectsEqual(Runtime.Object obj1, Runtime.Object obj2)
{
if (obj1.GetType() != obj2.GetType()) return false;
// Perform equality on int/float/bool manually to avoid boxing
var boolVal = obj1 as BoolValue;
if( boolVal != null ) {
return boolVal.value == ((BoolValue)obj2).value;
}
var intVal = obj1 as IntValue;
if( intVal != null ) {
return intVal.value == ((IntValue)obj2).value;
}
var floatVal = obj1 as FloatValue;
if (floatVal != null)
{
return floatVal.value == ((FloatValue)obj2).value;
}
// Other Value type (using proper Equals: list, string, divert path)
var val1 = obj1 as Value;
var val2 = obj2 as Value;
if( val1 != null ) {
return val1.valueObject.Equals(val2.valueObject);
}
throw new System.Exception("FastRoughDefinitelyEquals: Unsupported runtime object type: "+obj1.GetType());
}
public Runtime.Object GetVariableWithName(string name)
{
return GetVariableWithName (name, -1);
}
public Runtime.Object TryGetDefaultVariableValue (string name)
{
Runtime.Object val = null;
_defaultGlobalVariables.TryGetValue (name, out val);
return val;
}
public bool GlobalVariableExistsWithName(string name)
{
return _globalVariables.ContainsKey(name) || _defaultGlobalVariables != null && _defaultGlobalVariables.ContainsKey(name);
}
Runtime.Object GetVariableWithName(string name, int contextIndex)
{
Runtime.Object varValue = GetRawVariableWithName (name, contextIndex);
// Get value from pointer?
var varPointer = varValue as VariablePointerValue;
if (varPointer) {
varValue = ValueAtVariablePointer (varPointer);
}
return varValue;
}
Runtime.Object GetRawVariableWithName(string name, int contextIndex)
{
Runtime.Object varValue = null;
// 0 context = global
if (contextIndex == 0 || contextIndex == -1) {
if (patch != null && patch.TryGetGlobal(name, out varValue))
return varValue;
if ( _globalVariables.TryGetValue (name, out varValue) )
return varValue;
// Getting variables can actually happen during globals set up since you can do
// VAR x = A_LIST_ITEM
// So _defaultGlobalVariables may be null.
// We need to do this check though in case a new global is added, so we need to
// revert to the default globals dictionary since an initial value hasn't yet been set.
if( _defaultGlobalVariables != null && _defaultGlobalVariables.TryGetValue(name, out varValue) ) {
return varValue;
}
var listItemValue = _listDefsOrigin.FindSingleItemListWithName (name);
if (listItemValue)
return listItemValue;
}
// Temporary
varValue = _callStack.GetTemporaryVariableWithName (name, contextIndex);
return varValue;
}
public Runtime.Object ValueAtVariablePointer(VariablePointerValue pointer)
{
return GetVariableWithName (pointer.variableName, pointer.contextIndex);
}
public void Assign(VariableAssignment varAss, Runtime.Object value)
{
var name = varAss.variableName;
int contextIndex = -1;
// Are we assigning to a global variable?
bool setGlobal = false;
if (varAss.isNewDeclaration) {
setGlobal = varAss.isGlobal;
} else {
setGlobal = GlobalVariableExistsWithName (name);
}
// Constructing new variable pointer reference
if (varAss.isNewDeclaration) {
var varPointer = value as VariablePointerValue;
if (varPointer) {
var fullyResolvedVariablePointer = ResolveVariablePointer (varPointer);
value = fullyResolvedVariablePointer;
}
}
// Assign to existing variable pointer?
// Then assign to the variable that the pointer is pointing to by name.
else {
// De-reference variable reference to point to
VariablePointerValue existingPointer = null;
do {
existingPointer = GetRawVariableWithName (name, contextIndex) as VariablePointerValue;
if (existingPointer) {
name = existingPointer.variableName;
contextIndex = existingPointer.contextIndex;
setGlobal = (contextIndex == 0);
}
} while(existingPointer);
}
if (setGlobal) {
SetGlobal (name, value);
} else {
_callStack.SetTemporaryVariable (name, value, varAss.isNewDeclaration, contextIndex);
}
}
public void SnapshotDefaultGlobals ()
{
_defaultGlobalVariables = new Dictionary<string, Object> (_globalVariables);
}
void RetainListOriginsForAssignment (Runtime.Object oldValue, Runtime.Object newValue)
{
var oldList = oldValue as ListValue;
var newList = newValue as ListValue;
if (oldList && newList && newList.value.Count == 0)
newList.value.SetInitialOriginNames (oldList.value.originNames);
}
public void SetGlobal(string variableName, Runtime.Object value)
{
Runtime.Object oldValue = null;
if( patch == null || !patch.TryGetGlobal(variableName, out oldValue) )
_globalVariables.TryGetValue (variableName, out oldValue);
ListValue.RetainListOriginsForAssignment (oldValue, value);
if (patch != null)
patch.SetGlobal(variableName, value);
else
_globalVariables [variableName] = value;
if (variableChangedEvent != null && !value.Equals (oldValue)) {
if (batchObservingVariableChanges) {
if (patch != null)
patch.AddChangedVariable(variableName);
else if(_changedVariablesForBatchObs != null)
_changedVariablesForBatchObs.Add (variableName);
} else {
variableChangedEvent (variableName, value);
}
}
}
// Given a variable pointer with just the name of the target known, resolve to a variable
// pointer that more specifically points to the exact instance: whether it's global,
// or the exact position of a temporary on the callstack.
VariablePointerValue ResolveVariablePointer(VariablePointerValue varPointer)
{
int contextIndex = varPointer.contextIndex;
if( contextIndex == -1 )
contextIndex = GetContextIndexOfVariableNamed (varPointer.variableName);
var valueOfVariablePointedTo = GetRawVariableWithName (varPointer.variableName, contextIndex);
// Extra layer of indirection:
// When accessing a pointer to a pointer (e.g. when calling nested or
// recursive functions that take a variable references, ensure we don't create
// a chain of indirection by just returning the final target.
var doubleRedirectionPointer = valueOfVariablePointedTo as VariablePointerValue;
if (doubleRedirectionPointer) {
return doubleRedirectionPointer;
}
// Make copy of the variable pointer so we're not using the value direct from
// the runtime. Temporary must be local to the current scope.
else {
return new VariablePointerValue (varPointer.variableName, contextIndex);
}
}
// 0 if named variable is global
// 1+ if named variable is a temporary in a particular call stack element
int GetContextIndexOfVariableNamed(string varName)
{
if (GlobalVariableExistsWithName(varName))
return 0;
return _callStack.currentElementIndex;
}
Dictionary<string, Runtime.Object> _globalVariables;
Dictionary<string, Runtime.Object> _defaultGlobalVariables;
// Used for accessing temporary variables
CallStack _callStack;
HashSet<string> _changedVariablesForBatchObs;
ListDefinitionsOrigin _listDefsOrigin;
}
}