Files
Mementos/Unity/Alternate Genre Jam/Assets/Ink/InkLibs/InkRuntime/VariablesState.cs
2021-06-30 21:39:19 +10:00

418 lines
16 KiB
C#

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;
}
}