mirror of
https://github.com/Ratstail91/Mementos.git
synced 2025-11-29 02:24:28 +11:00
418 lines
16 KiB
C#
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;
|
|
}
|
|
}
|
|
|