mirror of
https://github.com/Ratstail91/Mementos.git
synced 2025-11-29 02:24:28 +11:00
Committed everything
This commit is contained in:
@@ -0,0 +1,443 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Ink.Runtime
|
||||
{
|
||||
public class CallStack
|
||||
{
|
||||
public class Element
|
||||
{
|
||||
public Pointer currentPointer;
|
||||
|
||||
public bool inExpressionEvaluation;
|
||||
public Dictionary<string, Runtime.Object> temporaryVariables;
|
||||
public PushPopType type;
|
||||
|
||||
// When this callstack element is actually a function evaluation called from the game,
|
||||
// we need to keep track of the size of the evaluation stack when it was called
|
||||
// so that we know whether there was any return value.
|
||||
public int evaluationStackHeightWhenPushed;
|
||||
|
||||
// When functions are called, we trim whitespace from the start and end of what
|
||||
// they generate, so we make sure know where the function's start and end are.
|
||||
public int functionStartInOuputStream;
|
||||
|
||||
public Element(PushPopType type, Pointer pointer, bool inExpressionEvaluation = false) {
|
||||
this.currentPointer = pointer;
|
||||
this.inExpressionEvaluation = inExpressionEvaluation;
|
||||
this.temporaryVariables = new Dictionary<string, Object>();
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public Element Copy()
|
||||
{
|
||||
var copy = new Element (this.type, currentPointer, this.inExpressionEvaluation);
|
||||
copy.temporaryVariables = new Dictionary<string,Object>(this.temporaryVariables);
|
||||
copy.evaluationStackHeightWhenPushed = evaluationStackHeightWhenPushed;
|
||||
copy.functionStartInOuputStream = functionStartInOuputStream;
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
|
||||
public class Thread
|
||||
{
|
||||
public List<Element> callstack;
|
||||
public int threadIndex;
|
||||
public Pointer previousPointer;
|
||||
|
||||
public Thread() {
|
||||
callstack = new List<Element>();
|
||||
}
|
||||
|
||||
public Thread(Dictionary<string, object> jThreadObj, Story storyContext) : this() {
|
||||
threadIndex = (int) jThreadObj ["threadIndex"];
|
||||
|
||||
List<object> jThreadCallstack = (List<object>) jThreadObj ["callstack"];
|
||||
foreach (object jElTok in jThreadCallstack) {
|
||||
|
||||
var jElementObj = (Dictionary<string, object>)jElTok;
|
||||
|
||||
PushPopType pushPopType = (PushPopType)(int)jElementObj ["type"];
|
||||
|
||||
Pointer pointer = Pointer.Null;
|
||||
|
||||
string currentContainerPathStr = null;
|
||||
object currentContainerPathStrToken;
|
||||
if (jElementObj.TryGetValue ("cPath", out currentContainerPathStrToken)) {
|
||||
currentContainerPathStr = currentContainerPathStrToken.ToString ();
|
||||
|
||||
var threadPointerResult = storyContext.ContentAtPath (new Path (currentContainerPathStr));
|
||||
pointer.container = threadPointerResult.container;
|
||||
pointer.index = (int)jElementObj ["idx"];
|
||||
|
||||
if (threadPointerResult.obj == null)
|
||||
throw new System.Exception ("When loading state, internal story location couldn't be found: " + currentContainerPathStr + ". Has the story changed since this save data was created?");
|
||||
else if (threadPointerResult.approximate)
|
||||
storyContext.Warning ("When loading state, exact internal story location couldn't be found: '" + currentContainerPathStr + "', so it was approximated to '"+pointer.container.path.ToString()+"' to recover. Has the story changed since this save data was created?");
|
||||
}
|
||||
|
||||
bool inExpressionEvaluation = (bool)jElementObj ["exp"];
|
||||
|
||||
var el = new Element (pushPopType, pointer, inExpressionEvaluation);
|
||||
|
||||
object temps;
|
||||
if ( jElementObj.TryGetValue("temp", out temps) ) {
|
||||
el.temporaryVariables = Json.JObjectToDictionaryRuntimeObjs((Dictionary<string, object>)temps);
|
||||
} else {
|
||||
el.temporaryVariables.Clear();
|
||||
}
|
||||
|
||||
callstack.Add (el);
|
||||
}
|
||||
|
||||
object prevContentObjPath;
|
||||
if( jThreadObj.TryGetValue("previousContentObject", out prevContentObjPath) ) {
|
||||
var prevPath = new Path((string)prevContentObjPath);
|
||||
previousPointer = storyContext.PointerAtPath(prevPath);
|
||||
}
|
||||
}
|
||||
|
||||
public Thread Copy() {
|
||||
var copy = new Thread ();
|
||||
copy.threadIndex = threadIndex;
|
||||
foreach(var e in callstack) {
|
||||
copy.callstack.Add(e.Copy());
|
||||
}
|
||||
copy.previousPointer = previousPointer;
|
||||
return copy;
|
||||
}
|
||||
|
||||
public void WriteJson(SimpleJson.Writer writer)
|
||||
{
|
||||
writer.WriteObjectStart();
|
||||
|
||||
// callstack
|
||||
writer.WritePropertyStart("callstack");
|
||||
writer.WriteArrayStart();
|
||||
foreach (CallStack.Element el in callstack)
|
||||
{
|
||||
writer.WriteObjectStart();
|
||||
if(!el.currentPointer.isNull) {
|
||||
writer.WriteProperty("cPath", el.currentPointer.container.path.componentsString);
|
||||
writer.WriteProperty("idx", el.currentPointer.index);
|
||||
}
|
||||
|
||||
writer.WriteProperty("exp", el.inExpressionEvaluation);
|
||||
writer.WriteProperty("type", (int)el.type);
|
||||
|
||||
if(el.temporaryVariables.Count > 0) {
|
||||
writer.WritePropertyStart("temp");
|
||||
Json.WriteDictionaryRuntimeObjs(writer, el.temporaryVariables);
|
||||
writer.WritePropertyEnd();
|
||||
}
|
||||
|
||||
writer.WriteObjectEnd();
|
||||
}
|
||||
writer.WriteArrayEnd();
|
||||
writer.WritePropertyEnd();
|
||||
|
||||
// threadIndex
|
||||
writer.WriteProperty("threadIndex", threadIndex);
|
||||
|
||||
if (!previousPointer.isNull)
|
||||
{
|
||||
writer.WriteProperty("previousContentObject", previousPointer.Resolve().path.ToString());
|
||||
}
|
||||
|
||||
writer.WriteObjectEnd();
|
||||
}
|
||||
}
|
||||
|
||||
public List<Element> elements {
|
||||
get {
|
||||
return callStack;
|
||||
}
|
||||
}
|
||||
|
||||
public int depth {
|
||||
get {
|
||||
return elements.Count;
|
||||
}
|
||||
}
|
||||
|
||||
public Element currentElement {
|
||||
get {
|
||||
var thread = _threads [_threads.Count - 1];
|
||||
var cs = thread.callstack;
|
||||
return cs [cs.Count - 1];
|
||||
}
|
||||
}
|
||||
|
||||
public int currentElementIndex {
|
||||
get {
|
||||
return callStack.Count - 1;
|
||||
}
|
||||
}
|
||||
|
||||
public Thread currentThread
|
||||
{
|
||||
get {
|
||||
return _threads [_threads.Count - 1];
|
||||
}
|
||||
set {
|
||||
Debug.Assert (_threads.Count == 1, "Shouldn't be directly setting the current thread when we have a stack of them");
|
||||
_threads.Clear ();
|
||||
_threads.Add (value);
|
||||
}
|
||||
}
|
||||
|
||||
public bool canPop {
|
||||
get {
|
||||
return callStack.Count > 1;
|
||||
}
|
||||
}
|
||||
|
||||
public CallStack (Story storyContext)
|
||||
{
|
||||
_startOfRoot = Pointer.StartOf(storyContext.rootContentContainer);
|
||||
Reset();
|
||||
}
|
||||
|
||||
|
||||
public CallStack(CallStack toCopy)
|
||||
{
|
||||
_threads = new List<Thread> ();
|
||||
foreach (var otherThread in toCopy._threads) {
|
||||
_threads.Add (otherThread.Copy ());
|
||||
}
|
||||
_threadCounter = toCopy._threadCounter;
|
||||
_startOfRoot = toCopy._startOfRoot;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_threads = new List<Thread>();
|
||||
_threads.Add(new Thread());
|
||||
|
||||
_threads[0].callstack.Add(new Element(PushPopType.Tunnel, _startOfRoot));
|
||||
}
|
||||
|
||||
|
||||
// Unfortunately it's not possible to implement jsonToken since
|
||||
// the setter needs to take a Story as a context in order to
|
||||
// look up objects from paths for currentContainer within elements.
|
||||
public void SetJsonToken(Dictionary<string, object> jObject, Story storyContext)
|
||||
{
|
||||
_threads.Clear ();
|
||||
|
||||
var jThreads = (List<object>) jObject ["threads"];
|
||||
|
||||
foreach (object jThreadTok in jThreads) {
|
||||
var jThreadObj = (Dictionary<string, object>)jThreadTok;
|
||||
var thread = new Thread (jThreadObj, storyContext);
|
||||
_threads.Add (thread);
|
||||
}
|
||||
|
||||
_threadCounter = (int)jObject ["threadCounter"];
|
||||
_startOfRoot = Pointer.StartOf(storyContext.rootContentContainer);
|
||||
}
|
||||
|
||||
public void WriteJson(SimpleJson.Writer w)
|
||||
{
|
||||
w.WriteObject(writer =>
|
||||
{
|
||||
writer.WritePropertyStart("threads");
|
||||
{
|
||||
writer.WriteArrayStart();
|
||||
|
||||
foreach (CallStack.Thread thread in _threads)
|
||||
{
|
||||
thread.WriteJson(writer);
|
||||
}
|
||||
|
||||
writer.WriteArrayEnd();
|
||||
}
|
||||
writer.WritePropertyEnd();
|
||||
|
||||
writer.WritePropertyStart("threadCounter");
|
||||
{
|
||||
writer.Write(_threadCounter);
|
||||
}
|
||||
writer.WritePropertyEnd();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public void PushThread()
|
||||
{
|
||||
var newThread = currentThread.Copy ();
|
||||
_threadCounter++;
|
||||
newThread.threadIndex = _threadCounter;
|
||||
_threads.Add (newThread);
|
||||
}
|
||||
|
||||
public Thread ForkThread()
|
||||
{
|
||||
var forkedThread = currentThread.Copy();
|
||||
_threadCounter++;
|
||||
forkedThread.threadIndex = _threadCounter;
|
||||
return forkedThread;
|
||||
}
|
||||
|
||||
public void PopThread()
|
||||
{
|
||||
if (canPopThread) {
|
||||
_threads.Remove (currentThread);
|
||||
} else {
|
||||
throw new System.Exception("Can't pop thread");
|
||||
}
|
||||
}
|
||||
|
||||
public bool canPopThread
|
||||
{
|
||||
get {
|
||||
return _threads.Count > 1 && !elementIsEvaluateFromGame;
|
||||
}
|
||||
}
|
||||
|
||||
public bool elementIsEvaluateFromGame
|
||||
{
|
||||
get {
|
||||
return currentElement.type == PushPopType.FunctionEvaluationFromGame;
|
||||
}
|
||||
}
|
||||
|
||||
public void Push(PushPopType type, int externalEvaluationStackHeight = 0, int outputStreamLengthWithPushed = 0)
|
||||
{
|
||||
// When pushing to callstack, maintain the current content path, but jump out of expressions by default
|
||||
var element = new Element (
|
||||
type,
|
||||
currentElement.currentPointer,
|
||||
inExpressionEvaluation: false
|
||||
);
|
||||
|
||||
element.evaluationStackHeightWhenPushed = externalEvaluationStackHeight;
|
||||
element.functionStartInOuputStream = outputStreamLengthWithPushed;
|
||||
|
||||
callStack.Add (element);
|
||||
}
|
||||
|
||||
public bool CanPop(PushPopType? type = null) {
|
||||
|
||||
if (!canPop)
|
||||
return false;
|
||||
|
||||
if (type == null)
|
||||
return true;
|
||||
|
||||
return currentElement.type == type;
|
||||
}
|
||||
|
||||
public void Pop(PushPopType? type = null)
|
||||
{
|
||||
if (CanPop (type)) {
|
||||
callStack.RemoveAt (callStack.Count - 1);
|
||||
return;
|
||||
} else {
|
||||
throw new System.Exception("Mismatched push/pop in Callstack");
|
||||
}
|
||||
}
|
||||
|
||||
// Get variable value, dereferencing a variable pointer if necessary
|
||||
public Runtime.Object GetTemporaryVariableWithName(string name, int contextIndex = -1)
|
||||
{
|
||||
if (contextIndex == -1)
|
||||
contextIndex = currentElementIndex+1;
|
||||
|
||||
Runtime.Object varValue = null;
|
||||
|
||||
var contextElement = callStack [contextIndex-1];
|
||||
|
||||
if (contextElement.temporaryVariables.TryGetValue (name, out varValue)) {
|
||||
return varValue;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetTemporaryVariable(string name, Runtime.Object value, bool declareNew, int contextIndex = -1)
|
||||
{
|
||||
if (contextIndex == -1)
|
||||
contextIndex = currentElementIndex+1;
|
||||
|
||||
var contextElement = callStack [contextIndex-1];
|
||||
|
||||
if (!declareNew && !contextElement.temporaryVariables.ContainsKey(name)) {
|
||||
throw new System.Exception ("Could not find temporary variable to set: " + name);
|
||||
}
|
||||
|
||||
Runtime.Object oldValue;
|
||||
if( contextElement.temporaryVariables.TryGetValue(name, out oldValue) )
|
||||
ListValue.RetainListOriginsForAssignment (oldValue, value);
|
||||
|
||||
contextElement.temporaryVariables [name] = value;
|
||||
}
|
||||
|
||||
// Find the most appropriate context for this variable.
|
||||
// Are we referencing a temporary or global variable?
|
||||
// Note that the compiler will have warned us about possible conflicts,
|
||||
// so anything that happens here should be safe!
|
||||
public int ContextForVariableNamed(string name)
|
||||
{
|
||||
// Current temporary context?
|
||||
// (Shouldn't attempt to access contexts higher in the callstack.)
|
||||
if (currentElement.temporaryVariables.ContainsKey (name)) {
|
||||
return currentElementIndex+1;
|
||||
}
|
||||
|
||||
// Global
|
||||
else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public Thread ThreadWithIndex(int index)
|
||||
{
|
||||
return _threads.Find (t => t.threadIndex == index);
|
||||
}
|
||||
|
||||
private List<Element> callStack
|
||||
{
|
||||
get {
|
||||
return currentThread.callstack;
|
||||
}
|
||||
}
|
||||
|
||||
public string callStackTrace {
|
||||
get {
|
||||
var sb = new System.Text.StringBuilder();
|
||||
|
||||
for(int t=0; t<_threads.Count; t++) {
|
||||
|
||||
var thread = _threads[t];
|
||||
var isCurrent = (t == _threads.Count-1);
|
||||
sb.AppendFormat("=== THREAD {0}/{1} {2}===\n", (t+1), _threads.Count, (isCurrent ? "(current) ":""));
|
||||
|
||||
for(int i=0; i<thread.callstack.Count; i++) {
|
||||
|
||||
if( thread.callstack[i].type == PushPopType.Function )
|
||||
sb.Append(" [FUNCTION] ");
|
||||
else
|
||||
sb.Append(" [TUNNEL] ");
|
||||
|
||||
var pointer = thread.callstack[i].currentPointer;
|
||||
if( !pointer.isNull ) {
|
||||
sb.Append("<SOMEWHERE IN ");
|
||||
sb.Append(pointer.container.path.ToString());
|
||||
sb.AppendLine(">");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
List<Thread> _threads;
|
||||
int _threadCounter;
|
||||
Pointer _startOfRoot;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 152213b0affe5410c9b7ef8eac085a64
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,54 @@
|
||||
|
||||
namespace Ink.Runtime
|
||||
{
|
||||
/// <summary>
|
||||
/// A generated Choice from the story.
|
||||
/// A single ChoicePoint in the Story could potentially generate
|
||||
/// different Choices dynamically dependent on state, so they're
|
||||
/// separated.
|
||||
/// </summary>
|
||||
public class Choice : Runtime.Object
|
||||
{
|
||||
/// <summary>
|
||||
/// The main text to presented to the player for this Choice.
|
||||
/// </summary>
|
||||
public string text { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The target path that the Story should be diverted to if
|
||||
/// this Choice is chosen.
|
||||
/// </summary>
|
||||
public string pathStringOnChoice {
|
||||
get {
|
||||
return targetPath.ToString ();
|
||||
}
|
||||
set {
|
||||
targetPath = new Path (value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the path to the original choice point - where was this choice defined in the story?
|
||||
/// </summary>
|
||||
/// <value>A dot separated path into the story data.</value>
|
||||
public string sourcePath;
|
||||
|
||||
/// <summary>
|
||||
/// The original index into currentChoices list on the Story when
|
||||
/// this Choice was generated, for convenience.
|
||||
/// </summary>
|
||||
public int index { get; set; }
|
||||
|
||||
public Path targetPath;
|
||||
|
||||
public CallStack.Thread threadAtGeneration { get; set; }
|
||||
public int originalThreadIndex;
|
||||
|
||||
public bool isInvisibleDefault;
|
||||
|
||||
public Choice()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8db545ff917fa46d7aa5f472a9fdbad8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,89 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Ink.Runtime
|
||||
{
|
||||
/// <summary>
|
||||
/// The ChoicePoint represents the point within the Story where
|
||||
/// a Choice instance gets generated. The distinction is made
|
||||
/// because the text of the Choice can be dynamically generated.
|
||||
/// </summary>
|
||||
public class ChoicePoint : Runtime.Object
|
||||
{
|
||||
public Path pathOnChoice {
|
||||
get {
|
||||
// Resolve any relative paths to global ones as we come across them
|
||||
if (_pathOnChoice != null && _pathOnChoice.isRelative) {
|
||||
var choiceTargetObj = choiceTarget;
|
||||
if (choiceTargetObj) {
|
||||
_pathOnChoice = choiceTargetObj.path;
|
||||
}
|
||||
}
|
||||
return _pathOnChoice;
|
||||
}
|
||||
set {
|
||||
_pathOnChoice = value;
|
||||
}
|
||||
}
|
||||
Path _pathOnChoice;
|
||||
|
||||
public Container choiceTarget {
|
||||
get {
|
||||
return this.ResolvePath (_pathOnChoice).container;
|
||||
}
|
||||
}
|
||||
|
||||
public string pathStringOnChoice {
|
||||
get {
|
||||
return CompactPathString (pathOnChoice);
|
||||
}
|
||||
set {
|
||||
pathOnChoice = new Path (value);
|
||||
}
|
||||
}
|
||||
|
||||
public bool hasCondition { get; set; }
|
||||
public bool hasStartContent { get; set; }
|
||||
public bool hasChoiceOnlyContent { get; set; }
|
||||
public bool onceOnly { get; set; }
|
||||
public bool isInvisibleDefault { get; set; }
|
||||
|
||||
public int flags {
|
||||
get {
|
||||
int flags = 0;
|
||||
if (hasCondition) flags |= 1;
|
||||
if (hasStartContent) flags |= 2;
|
||||
if (hasChoiceOnlyContent) flags |= 4;
|
||||
if (isInvisibleDefault) flags |= 8;
|
||||
if (onceOnly) flags |= 16;
|
||||
return flags;
|
||||
}
|
||||
set {
|
||||
hasCondition = (value & 1) > 0;
|
||||
hasStartContent = (value & 2) > 0;
|
||||
hasChoiceOnlyContent = (value & 4) > 0;
|
||||
isInvisibleDefault = (value & 8) > 0;
|
||||
onceOnly = (value & 16) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
public ChoicePoint (bool onceOnly)
|
||||
{
|
||||
this.onceOnly = onceOnly;
|
||||
}
|
||||
|
||||
public ChoicePoint() : this(true) {}
|
||||
|
||||
public override string ToString ()
|
||||
{
|
||||
int? targetLineNum = DebugLineNumberOfPath (pathOnChoice);
|
||||
string targetString = pathOnChoice.ToString ();
|
||||
|
||||
if (targetLineNum != null) {
|
||||
targetString = " line " + targetLineNum + "("+targetString+")";
|
||||
}
|
||||
|
||||
return "Choice: -> " + targetString;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b790cb942d6b84ca28e93a2b46d8c48f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,366 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Ink.Runtime
|
||||
{
|
||||
public class Container : Runtime.Object, INamedContent
|
||||
{
|
||||
public string name { get; set; }
|
||||
|
||||
public List<Runtime.Object> content {
|
||||
get {
|
||||
return _content;
|
||||
}
|
||||
set {
|
||||
AddContent (value);
|
||||
}
|
||||
}
|
||||
List<Runtime.Object> _content;
|
||||
|
||||
public Dictionary<string, INamedContent> namedContent { get; set; }
|
||||
|
||||
public Dictionary<string, Runtime.Object> namedOnlyContent {
|
||||
get {
|
||||
var namedOnlyContentDict = new Dictionary<string, Runtime.Object>();
|
||||
foreach (var kvPair in namedContent) {
|
||||
namedOnlyContentDict [kvPair.Key] = (Runtime.Object)kvPair.Value;
|
||||
}
|
||||
|
||||
foreach (var c in content) {
|
||||
var named = c as INamedContent;
|
||||
if (named != null && named.hasValidName) {
|
||||
namedOnlyContentDict.Remove (named.name);
|
||||
}
|
||||
}
|
||||
|
||||
if (namedOnlyContentDict.Count == 0)
|
||||
namedOnlyContentDict = null;
|
||||
|
||||
return namedOnlyContentDict;
|
||||
}
|
||||
set {
|
||||
var existingNamedOnly = namedOnlyContent;
|
||||
if (existingNamedOnly != null) {
|
||||
foreach (var kvPair in existingNamedOnly) {
|
||||
namedContent.Remove (kvPair.Key);
|
||||
}
|
||||
}
|
||||
|
||||
if (value == null)
|
||||
return;
|
||||
|
||||
foreach (var kvPair in value) {
|
||||
var named = kvPair.Value as INamedContent;
|
||||
if( named != null )
|
||||
AddToNamedContentOnly (named);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool visitsShouldBeCounted { get; set; }
|
||||
public bool turnIndexShouldBeCounted { get; set; }
|
||||
public bool countingAtStartOnly { get; set; }
|
||||
|
||||
[Flags]
|
||||
public enum CountFlags
|
||||
{
|
||||
Visits = 1,
|
||||
Turns = 2,
|
||||
CountStartOnly = 4
|
||||
}
|
||||
|
||||
public int countFlags
|
||||
{
|
||||
get {
|
||||
CountFlags flags = 0;
|
||||
if (visitsShouldBeCounted) flags |= CountFlags.Visits;
|
||||
if (turnIndexShouldBeCounted) flags |= CountFlags.Turns;
|
||||
if (countingAtStartOnly) flags |= CountFlags.CountStartOnly;
|
||||
|
||||
// If we're only storing CountStartOnly, it serves no purpose,
|
||||
// since it's dependent on the other two to be used at all.
|
||||
// (e.g. for setting the fact that *if* a gather or choice's
|
||||
// content is counted, then is should only be counter at the start)
|
||||
// So this is just an optimisation for storage.
|
||||
if (flags == CountFlags.CountStartOnly) {
|
||||
flags = 0;
|
||||
}
|
||||
|
||||
return (int)flags;
|
||||
}
|
||||
set {
|
||||
var flag = (CountFlags)value;
|
||||
if ((flag & CountFlags.Visits) > 0) visitsShouldBeCounted = true;
|
||||
if ((flag & CountFlags.Turns) > 0) turnIndexShouldBeCounted = true;
|
||||
if ((flag & CountFlags.CountStartOnly) > 0) countingAtStartOnly = true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool hasValidName
|
||||
{
|
||||
get { return name != null && name.Length > 0; }
|
||||
}
|
||||
|
||||
public Path pathToFirstLeafContent
|
||||
{
|
||||
get {
|
||||
if( _pathToFirstLeafContent == null )
|
||||
_pathToFirstLeafContent = path.PathByAppendingPath (internalPathToFirstLeafContent);
|
||||
|
||||
return _pathToFirstLeafContent;
|
||||
}
|
||||
}
|
||||
Path _pathToFirstLeafContent;
|
||||
|
||||
Path internalPathToFirstLeafContent
|
||||
{
|
||||
get {
|
||||
var components = new List<Path.Component>();
|
||||
var container = this;
|
||||
while (container != null) {
|
||||
if (container.content.Count > 0) {
|
||||
components.Add (new Path.Component (0));
|
||||
container = container.content [0] as Container;
|
||||
}
|
||||
}
|
||||
return new Path(components);
|
||||
}
|
||||
}
|
||||
|
||||
public Container ()
|
||||
{
|
||||
_content = new List<Runtime.Object> ();
|
||||
namedContent = new Dictionary<string, INamedContent> ();
|
||||
}
|
||||
|
||||
public void AddContent(Runtime.Object contentObj)
|
||||
{
|
||||
content.Add (contentObj);
|
||||
|
||||
if (contentObj.parent) {
|
||||
throw new System.Exception ("content is already in " + contentObj.parent);
|
||||
}
|
||||
|
||||
contentObj.parent = this;
|
||||
|
||||
TryAddNamedContent (contentObj);
|
||||
}
|
||||
|
||||
public void AddContent(IList<Runtime.Object> contentList)
|
||||
{
|
||||
foreach (var c in contentList) {
|
||||
AddContent (c);
|
||||
}
|
||||
}
|
||||
|
||||
public void InsertContent(Runtime.Object contentObj, int index)
|
||||
{
|
||||
content.Insert (index, contentObj);
|
||||
|
||||
if (contentObj.parent) {
|
||||
throw new System.Exception ("content is already in " + contentObj.parent);
|
||||
}
|
||||
|
||||
contentObj.parent = this;
|
||||
|
||||
TryAddNamedContent (contentObj);
|
||||
}
|
||||
|
||||
public void TryAddNamedContent(Runtime.Object contentObj)
|
||||
{
|
||||
var namedContentObj = contentObj as INamedContent;
|
||||
if (namedContentObj != null && namedContentObj.hasValidName) {
|
||||
AddToNamedContentOnly (namedContentObj);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddToNamedContentOnly(INamedContent namedContentObj)
|
||||
{
|
||||
Debug.Assert (namedContentObj is Runtime.Object, "Can only add Runtime.Objects to a Runtime.Container");
|
||||
var runtimeObj = (Runtime.Object)namedContentObj;
|
||||
runtimeObj.parent = this;
|
||||
|
||||
namedContent [namedContentObj.name] = namedContentObj;
|
||||
}
|
||||
|
||||
public void AddContentsOfContainer(Container otherContainer)
|
||||
{
|
||||
content.AddRange (otherContainer.content);
|
||||
foreach (var obj in otherContainer.content) {
|
||||
obj.parent = this;
|
||||
TryAddNamedContent (obj);
|
||||
}
|
||||
}
|
||||
|
||||
protected Runtime.Object ContentWithPathComponent(Path.Component component)
|
||||
{
|
||||
if (component.isIndex) {
|
||||
|
||||
if (component.index >= 0 && component.index < content.Count) {
|
||||
return content [component.index];
|
||||
}
|
||||
|
||||
// When path is out of range, quietly return nil
|
||||
// (useful as we step/increment forwards through content)
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
else if (component.isParent) {
|
||||
return this.parent;
|
||||
}
|
||||
|
||||
else {
|
||||
INamedContent foundContent = null;
|
||||
if (namedContent.TryGetValue (component.name, out foundContent)) {
|
||||
return (Runtime.Object)foundContent;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public SearchResult ContentAtPath(Path path, int partialPathStart = 0, int partialPathLength = -1)
|
||||
{
|
||||
if (partialPathLength == -1)
|
||||
partialPathLength = path.length;
|
||||
|
||||
var result = new SearchResult ();
|
||||
result.approximate = false;
|
||||
|
||||
Container currentContainer = this;
|
||||
Runtime.Object currentObj = this;
|
||||
|
||||
for (int i = partialPathStart; i < partialPathLength; ++i) {
|
||||
var comp = path.GetComponent(i);
|
||||
|
||||
// Path component was wrong type
|
||||
if (currentContainer == null) {
|
||||
result.approximate = true;
|
||||
break;
|
||||
}
|
||||
|
||||
var foundObj = currentContainer.ContentWithPathComponent(comp);
|
||||
|
||||
// Couldn't resolve entire path?
|
||||
if (foundObj == null) {
|
||||
result.approximate = true;
|
||||
break;
|
||||
}
|
||||
|
||||
currentObj = foundObj;
|
||||
currentContainer = foundObj as Container;
|
||||
}
|
||||
|
||||
result.obj = currentObj;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void BuildStringOfHierarchy(StringBuilder sb, int indentation, Runtime.Object pointedObj)
|
||||
{
|
||||
Action appendIndentation = () => {
|
||||
const int spacesPerIndent = 4;
|
||||
for(int i=0; i<spacesPerIndent*indentation;++i) {
|
||||
sb.Append(" ");
|
||||
}
|
||||
};
|
||||
|
||||
appendIndentation ();
|
||||
sb.Append("[");
|
||||
|
||||
if (this.hasValidName) {
|
||||
sb.AppendFormat (" ({0})", this.name);
|
||||
}
|
||||
|
||||
if (this == pointedObj) {
|
||||
sb.Append (" <---");
|
||||
}
|
||||
|
||||
sb.AppendLine ();
|
||||
|
||||
indentation++;
|
||||
|
||||
for (int i=0; i<content.Count; ++i) {
|
||||
|
||||
var obj = content [i];
|
||||
|
||||
if (obj is Container) {
|
||||
|
||||
var container = (Container)obj;
|
||||
|
||||
container.BuildStringOfHierarchy (sb, indentation, pointedObj);
|
||||
|
||||
} else {
|
||||
appendIndentation ();
|
||||
if (obj is StringValue) {
|
||||
sb.Append ("\"");
|
||||
sb.Append (obj.ToString ().Replace ("\n", "\\n"));
|
||||
sb.Append ("\"");
|
||||
} else {
|
||||
sb.Append (obj.ToString ());
|
||||
}
|
||||
}
|
||||
|
||||
if (i != content.Count - 1) {
|
||||
sb.Append (",");
|
||||
}
|
||||
|
||||
if ( !(obj is Container) && obj == pointedObj ) {
|
||||
sb.Append (" <---");
|
||||
}
|
||||
|
||||
sb.AppendLine ();
|
||||
}
|
||||
|
||||
|
||||
var onlyNamed = new Dictionary<string, INamedContent> ();
|
||||
|
||||
foreach (var objKV in namedContent) {
|
||||
if (content.Contains ((Runtime.Object)objKV.Value)) {
|
||||
continue;
|
||||
} else {
|
||||
onlyNamed.Add (objKV.Key, objKV.Value);
|
||||
}
|
||||
}
|
||||
|
||||
if (onlyNamed.Count > 0) {
|
||||
appendIndentation ();
|
||||
sb.AppendLine ("-- named: --");
|
||||
|
||||
foreach (var objKV in onlyNamed) {
|
||||
|
||||
Debug.Assert (objKV.Value is Container, "Can only print out named Containers");
|
||||
var container = (Container)objKV.Value;
|
||||
container.BuildStringOfHierarchy (sb, indentation, pointedObj);
|
||||
|
||||
sb.AppendLine ();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
indentation--;
|
||||
|
||||
appendIndentation ();
|
||||
sb.Append ("]");
|
||||
}
|
||||
|
||||
public virtual string BuildStringOfHierarchy()
|
||||
{
|
||||
var sb = new StringBuilder ();
|
||||
|
||||
BuildStringOfHierarchy (sb, 0, null);
|
||||
|
||||
return sb.ToString ();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2739750c31df04462a8dedfa904e8760
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,170 @@
|
||||
using System;
|
||||
|
||||
namespace Ink.Runtime
|
||||
{
|
||||
public class ControlCommand : Runtime.Object
|
||||
{
|
||||
public enum CommandType
|
||||
{
|
||||
NotSet = -1,
|
||||
EvalStart,
|
||||
EvalOutput,
|
||||
EvalEnd,
|
||||
Duplicate,
|
||||
PopEvaluatedValue,
|
||||
PopFunction,
|
||||
PopTunnel,
|
||||
BeginString,
|
||||
EndString,
|
||||
NoOp,
|
||||
ChoiceCount,
|
||||
Turns,
|
||||
TurnsSince,
|
||||
ReadCount,
|
||||
Random,
|
||||
SeedRandom,
|
||||
VisitIndex,
|
||||
SequenceShuffleIndex,
|
||||
StartThread,
|
||||
Done,
|
||||
End,
|
||||
ListFromInt,
|
||||
ListRange,
|
||||
ListRandom,
|
||||
//----
|
||||
TOTAL_VALUES
|
||||
}
|
||||
|
||||
public CommandType commandType { get; protected set; }
|
||||
|
||||
public ControlCommand (CommandType commandType)
|
||||
{
|
||||
this.commandType = commandType;
|
||||
}
|
||||
|
||||
// Require default constructor for serialisation
|
||||
public ControlCommand() : this(CommandType.NotSet) {}
|
||||
|
||||
public override Object Copy()
|
||||
{
|
||||
return new ControlCommand (commandType);
|
||||
}
|
||||
|
||||
// The following static factory methods are to make generating these objects
|
||||
// slightly more succinct. Without these, the code gets pretty massive! e.g.
|
||||
//
|
||||
// var c = new Runtime.ControlCommand(Runtime.ControlCommand.CommandType.EvalStart)
|
||||
//
|
||||
// as opposed to
|
||||
//
|
||||
// var c = Runtime.ControlCommand.EvalStart()
|
||||
|
||||
public static ControlCommand EvalStart() {
|
||||
return new ControlCommand(CommandType.EvalStart);
|
||||
}
|
||||
|
||||
public static ControlCommand EvalOutput() {
|
||||
return new ControlCommand(CommandType.EvalOutput);
|
||||
}
|
||||
|
||||
public static ControlCommand EvalEnd() {
|
||||
return new ControlCommand(CommandType.EvalEnd);
|
||||
}
|
||||
|
||||
public static ControlCommand Duplicate() {
|
||||
return new ControlCommand(CommandType.Duplicate);
|
||||
}
|
||||
|
||||
public static ControlCommand PopEvaluatedValue() {
|
||||
return new ControlCommand (CommandType.PopEvaluatedValue);
|
||||
}
|
||||
|
||||
public static ControlCommand PopFunction() {
|
||||
return new ControlCommand (CommandType.PopFunction);
|
||||
}
|
||||
|
||||
public static ControlCommand PopTunnel() {
|
||||
return new ControlCommand (CommandType.PopTunnel);
|
||||
}
|
||||
|
||||
public static ControlCommand BeginString() {
|
||||
return new ControlCommand (CommandType.BeginString);
|
||||
}
|
||||
|
||||
public static ControlCommand EndString() {
|
||||
return new ControlCommand (CommandType.EndString);
|
||||
}
|
||||
|
||||
public static ControlCommand NoOp() {
|
||||
return new ControlCommand(CommandType.NoOp);
|
||||
}
|
||||
|
||||
public static ControlCommand ChoiceCount() {
|
||||
return new ControlCommand(CommandType.ChoiceCount);
|
||||
}
|
||||
|
||||
public static ControlCommand Turns ()
|
||||
{
|
||||
return new ControlCommand (CommandType.Turns);
|
||||
}
|
||||
|
||||
public static ControlCommand TurnsSince() {
|
||||
return new ControlCommand(CommandType.TurnsSince);
|
||||
}
|
||||
|
||||
public static ControlCommand ReadCount ()
|
||||
{
|
||||
return new ControlCommand (CommandType.ReadCount);
|
||||
}
|
||||
|
||||
public static ControlCommand Random ()
|
||||
{
|
||||
return new ControlCommand (CommandType.Random);
|
||||
}
|
||||
|
||||
public static ControlCommand SeedRandom ()
|
||||
{
|
||||
return new ControlCommand (CommandType.SeedRandom);
|
||||
}
|
||||
|
||||
public static ControlCommand VisitIndex() {
|
||||
return new ControlCommand(CommandType.VisitIndex);
|
||||
}
|
||||
|
||||
public static ControlCommand SequenceShuffleIndex() {
|
||||
return new ControlCommand(CommandType.SequenceShuffleIndex);
|
||||
}
|
||||
|
||||
public static ControlCommand StartThread() {
|
||||
return new ControlCommand (CommandType.StartThread);
|
||||
}
|
||||
|
||||
public static ControlCommand Done() {
|
||||
return new ControlCommand (CommandType.Done);
|
||||
}
|
||||
|
||||
public static ControlCommand End() {
|
||||
return new ControlCommand (CommandType.End);
|
||||
}
|
||||
|
||||
public static ControlCommand ListFromInt () {
|
||||
return new ControlCommand (CommandType.ListFromInt);
|
||||
}
|
||||
|
||||
public static ControlCommand ListRange ()
|
||||
{
|
||||
return new ControlCommand (CommandType.ListRange);
|
||||
}
|
||||
|
||||
public static ControlCommand ListRandom ()
|
||||
{
|
||||
return new ControlCommand (CommandType.ListRandom);
|
||||
}
|
||||
|
||||
public override string ToString ()
|
||||
{
|
||||
return commandType.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 41100189b7af04624a6c92ee13e18232
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
|
||||
namespace Ink.Runtime
|
||||
{
|
||||
public class DebugMetadata
|
||||
{
|
||||
public int startLineNumber = 0;
|
||||
public int endLineNumber = 0;
|
||||
public int startCharacterNumber = 0;
|
||||
public int endCharacterNumber = 0;
|
||||
public string fileName = null;
|
||||
public string sourceName = null;
|
||||
|
||||
public DebugMetadata ()
|
||||
{
|
||||
}
|
||||
|
||||
// Currently only used in VariableReference in order to
|
||||
// merge the debug metadata of a Path.Of.Indentifiers into
|
||||
// one single range.
|
||||
public DebugMetadata Merge(DebugMetadata dm)
|
||||
{
|
||||
var newDebugMetadata = new DebugMetadata();
|
||||
|
||||
// These are not supposed to be differ between 'this' and 'dm'.
|
||||
newDebugMetadata.fileName = fileName;
|
||||
newDebugMetadata.sourceName = sourceName;
|
||||
|
||||
if (startLineNumber < dm.startLineNumber)
|
||||
{
|
||||
newDebugMetadata.startLineNumber = startLineNumber;
|
||||
newDebugMetadata.startCharacterNumber = startCharacterNumber;
|
||||
}
|
||||
else if (startLineNumber > dm.startLineNumber)
|
||||
{
|
||||
newDebugMetadata.startLineNumber = dm.startLineNumber;
|
||||
newDebugMetadata.startCharacterNumber = dm.startCharacterNumber;
|
||||
}
|
||||
else
|
||||
{
|
||||
newDebugMetadata.startLineNumber = startLineNumber;
|
||||
newDebugMetadata.startCharacterNumber = Math.Min(startCharacterNumber, dm.startCharacterNumber);
|
||||
}
|
||||
|
||||
if (endLineNumber > dm.endLineNumber)
|
||||
{
|
||||
newDebugMetadata.endLineNumber = endLineNumber;
|
||||
newDebugMetadata.endCharacterNumber = endCharacterNumber;
|
||||
}
|
||||
else if (endLineNumber < dm.endLineNumber)
|
||||
{
|
||||
newDebugMetadata.endLineNumber = dm.endLineNumber;
|
||||
newDebugMetadata.endCharacterNumber = dm.endCharacterNumber;
|
||||
}
|
||||
else
|
||||
{
|
||||
newDebugMetadata.endLineNumber = endLineNumber;
|
||||
newDebugMetadata.endCharacterNumber = Math.Max(endCharacterNumber, dm.endCharacterNumber);
|
||||
}
|
||||
|
||||
return newDebugMetadata;
|
||||
}
|
||||
|
||||
public override string ToString ()
|
||||
{
|
||||
if (fileName != null) {
|
||||
return string.Format ("line {0} of {1}", startLineNumber, fileName);
|
||||
} else {
|
||||
return "line " + startLineNumber;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c1ae8e933de9541df9d12af663906aaf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,149 @@
|
||||
using System.Text;
|
||||
|
||||
namespace Ink.Runtime
|
||||
{
|
||||
public class Divert : Runtime.Object
|
||||
{
|
||||
public Path targetPath {
|
||||
get {
|
||||
// Resolve any relative paths to global ones as we come across them
|
||||
if (_targetPath != null && _targetPath.isRelative) {
|
||||
var targetObj = targetPointer.Resolve();
|
||||
if (targetObj) {
|
||||
_targetPath = targetObj.path;
|
||||
}
|
||||
}
|
||||
return _targetPath;
|
||||
}
|
||||
set {
|
||||
_targetPath = value;
|
||||
_targetPointer = Pointer.Null;
|
||||
}
|
||||
}
|
||||
Path _targetPath;
|
||||
|
||||
public Pointer targetPointer {
|
||||
get {
|
||||
if (_targetPointer.isNull) {
|
||||
var targetObj = ResolvePath (_targetPath).obj;
|
||||
|
||||
if (_targetPath.lastComponent.isIndex) {
|
||||
_targetPointer.container = targetObj.parent as Container;
|
||||
_targetPointer.index = _targetPath.lastComponent.index;
|
||||
} else {
|
||||
_targetPointer = Pointer.StartOf (targetObj as Container);
|
||||
}
|
||||
}
|
||||
return _targetPointer;
|
||||
}
|
||||
}
|
||||
Pointer _targetPointer;
|
||||
|
||||
|
||||
public string targetPathString {
|
||||
get {
|
||||
if (targetPath == null)
|
||||
return null;
|
||||
|
||||
return CompactPathString (targetPath);
|
||||
}
|
||||
set {
|
||||
if (value == null) {
|
||||
targetPath = null;
|
||||
} else {
|
||||
targetPath = new Path (value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string variableDivertName { get; set; }
|
||||
public bool hasVariableTarget { get { return variableDivertName != null; } }
|
||||
|
||||
public bool pushesToStack { get; set; }
|
||||
public PushPopType stackPushType;
|
||||
|
||||
public bool isExternal { get; set; }
|
||||
public int externalArgs { get; set; }
|
||||
|
||||
public bool isConditional { get; set; }
|
||||
|
||||
public Divert ()
|
||||
{
|
||||
pushesToStack = false;
|
||||
}
|
||||
|
||||
public Divert(PushPopType stackPushType)
|
||||
{
|
||||
pushesToStack = true;
|
||||
this.stackPushType = stackPushType;
|
||||
}
|
||||
|
||||
public override bool Equals (object obj)
|
||||
{
|
||||
var otherDivert = obj as Divert;
|
||||
if (otherDivert) {
|
||||
if (this.hasVariableTarget == otherDivert.hasVariableTarget) {
|
||||
if (this.hasVariableTarget) {
|
||||
return this.variableDivertName == otherDivert.variableDivertName;
|
||||
} else {
|
||||
return this.targetPath.Equals(otherDivert.targetPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public override int GetHashCode ()
|
||||
{
|
||||
if (hasVariableTarget) {
|
||||
const int variableTargetSalt = 12345;
|
||||
return variableDivertName.GetHashCode() + variableTargetSalt;
|
||||
} else {
|
||||
const int pathTargetSalt = 54321;
|
||||
return targetPath.GetHashCode() + pathTargetSalt;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString ()
|
||||
{
|
||||
if (hasVariableTarget) {
|
||||
return "Divert(variable: " + variableDivertName + ")";
|
||||
}
|
||||
else if (targetPath == null) {
|
||||
return "Divert(null)";
|
||||
} else {
|
||||
|
||||
var sb = new StringBuilder ();
|
||||
|
||||
string targetStr = targetPath.ToString ();
|
||||
int? targetLineNum = DebugLineNumberOfPath (targetPath);
|
||||
if (targetLineNum != null) {
|
||||
targetStr = "line " + targetLineNum;
|
||||
}
|
||||
|
||||
sb.Append ("Divert");
|
||||
|
||||
if (isConditional)
|
||||
sb.Append ("?");
|
||||
|
||||
if (pushesToStack) {
|
||||
if (stackPushType == PushPopType.Function) {
|
||||
sb.Append (" function");
|
||||
} else {
|
||||
sb.Append (" tunnel");
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append (" -> ");
|
||||
sb.Append (targetPathString);
|
||||
|
||||
sb.Append (" (");
|
||||
sb.Append (targetStr);
|
||||
sb.Append (")");
|
||||
|
||||
return sb.ToString ();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e98dd0cea4d0a4ddcbc9a1f135b486d4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace Ink
|
||||
{
|
||||
/// <summary>
|
||||
/// Callback for errors throughout both the ink runtime and compiler.
|
||||
/// </summary>
|
||||
public delegate void ErrorHandler(string message, ErrorType type);
|
||||
|
||||
/// <summary>
|
||||
/// Author errors will only ever come from the compiler so don't need to be handled
|
||||
/// by your Story error handler. The "Error" ErrorType is by far the most common
|
||||
/// for a runtime story error (rather than compiler error), though the Warning type
|
||||
/// is also possible.
|
||||
/// </summary>
|
||||
public enum ErrorType
|
||||
{
|
||||
/// Generated by a "TODO" note in the ink source
|
||||
Author,
|
||||
/// You should probably fix this, but it's not critical
|
||||
Warning,
|
||||
/// Critical error that can't be recovered from
|
||||
Error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5119822ad16c24c40b1f80c639c17671
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,93 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ink.Runtime
|
||||
{
|
||||
public class Flow {
|
||||
public string name;
|
||||
public CallStack callStack;
|
||||
public List<Runtime.Object> outputStream;
|
||||
public List<Choice> currentChoices;
|
||||
|
||||
public Flow(string name, Story story) {
|
||||
this.name = name;
|
||||
this.callStack = new CallStack(story);
|
||||
this.outputStream = new List<Object>();
|
||||
this.currentChoices = new List<Choice>();
|
||||
}
|
||||
|
||||
public Flow(string name, Story story, Dictionary<string, object> jObject) {
|
||||
this.name = name;
|
||||
this.callStack = new CallStack(story);
|
||||
this.callStack.SetJsonToken ((Dictionary < string, object > )jObject ["callstack"], story);
|
||||
this.outputStream = Json.JArrayToRuntimeObjList ((List<object>)jObject ["outputStream"]);
|
||||
this.currentChoices = Json.JArrayToRuntimeObjList<Choice>((List<object>)jObject ["currentChoices"]);
|
||||
|
||||
// choiceThreads is optional
|
||||
object jChoiceThreadsObj;
|
||||
jObject.TryGetValue("choiceThreads", out jChoiceThreadsObj);
|
||||
LoadFlowChoiceThreads((Dictionary<string, object>)jChoiceThreadsObj, story);
|
||||
}
|
||||
|
||||
public void WriteJson(SimpleJson.Writer writer)
|
||||
{
|
||||
writer.WriteObjectStart();
|
||||
|
||||
writer.WriteProperty("callstack", callStack.WriteJson);
|
||||
writer.WriteProperty("outputStream", w => Json.WriteListRuntimeObjs(w, outputStream));
|
||||
|
||||
// choiceThreads: optional
|
||||
// Has to come BEFORE the choices themselves are written out
|
||||
// since the originalThreadIndex of each choice needs to be set
|
||||
bool hasChoiceThreads = false;
|
||||
foreach (Choice c in currentChoices)
|
||||
{
|
||||
c.originalThreadIndex = c.threadAtGeneration.threadIndex;
|
||||
|
||||
if (callStack.ThreadWithIndex(c.originalThreadIndex) == null)
|
||||
{
|
||||
if (!hasChoiceThreads)
|
||||
{
|
||||
hasChoiceThreads = true;
|
||||
writer.WritePropertyStart("choiceThreads");
|
||||
writer.WriteObjectStart();
|
||||
}
|
||||
|
||||
writer.WritePropertyStart(c.originalThreadIndex);
|
||||
c.threadAtGeneration.WriteJson(writer);
|
||||
writer.WritePropertyEnd();
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChoiceThreads)
|
||||
{
|
||||
writer.WriteObjectEnd();
|
||||
writer.WritePropertyEnd();
|
||||
}
|
||||
|
||||
|
||||
writer.WriteProperty("currentChoices", w => {
|
||||
w.WriteArrayStart();
|
||||
foreach (var c in currentChoices)
|
||||
Json.WriteChoice(w, c);
|
||||
w.WriteArrayEnd();
|
||||
});
|
||||
|
||||
|
||||
writer.WriteObjectEnd();
|
||||
}
|
||||
|
||||
// Used both to load old format and current
|
||||
public void LoadFlowChoiceThreads(Dictionary<string, object> jChoiceThreads, Story story)
|
||||
{
|
||||
foreach (var choice in currentChoices) {
|
||||
var foundActiveThread = callStack.ThreadWithIndex(choice.originalThreadIndex);
|
||||
if( foundActiveThread != null ) {
|
||||
choice.threadAtGeneration = foundActiveThread.Copy ();
|
||||
} else {
|
||||
var jSavedChoiceThread = (Dictionary <string, object>) jChoiceThreads[choice.originalThreadIndex.ToString()];
|
||||
choice.threadAtGeneration = new CallStack.Thread(jSavedChoiceThread, story);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 625dee430631288439017a5dc0349c86
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Ink.Runtime
|
||||
{
|
||||
public class Glue : Runtime.Object
|
||||
{
|
||||
public Glue() { }
|
||||
|
||||
public override string ToString ()
|
||||
{
|
||||
return "Glue";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 60dc9de8c1e834c6eade8d924a7afe45
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,10 @@
|
||||
|
||||
namespace Ink.Runtime
|
||||
{
|
||||
public interface INamedContent
|
||||
{
|
||||
string name { get; }
|
||||
bool hasValidName { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: db1f7c668efd4422d8437ac965745242
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,584 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Ink.Runtime
|
||||
{
|
||||
/// <summary>
|
||||
/// The underlying type for a list item in ink. It stores the original list definition
|
||||
/// name as well as the item name, but without the value of the item. When the value is
|
||||
/// stored, it's stored in a KeyValuePair of InkListItem and int.
|
||||
/// </summary>
|
||||
public struct InkListItem
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the list where the item was originally defined.
|
||||
/// </summary>
|
||||
public readonly string originName;
|
||||
|
||||
/// <summary>
|
||||
/// The main name of the item as defined in ink.
|
||||
/// </summary>
|
||||
public readonly string itemName;
|
||||
|
||||
/// <summary>
|
||||
/// Create an item with the given original list definition name, and the name of this
|
||||
/// item.
|
||||
/// </summary>
|
||||
public InkListItem (string originName, string itemName)
|
||||
{
|
||||
this.originName = originName;
|
||||
this.itemName = itemName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an item from a dot-separted string of the form "listDefinitionName.listItemName".
|
||||
/// </summary>
|
||||
public InkListItem (string fullName)
|
||||
{
|
||||
var nameParts = fullName.Split ('.');
|
||||
this.originName = nameParts [0];
|
||||
this.itemName = nameParts [1];
|
||||
}
|
||||
|
||||
public static InkListItem Null {
|
||||
get {
|
||||
return new InkListItem (null, null);
|
||||
}
|
||||
}
|
||||
|
||||
public bool isNull {
|
||||
get {
|
||||
return originName == null && itemName == null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the full dot-separated name of the item, in the form "listDefinitionName.itemName".
|
||||
/// </summary>
|
||||
public string fullName {
|
||||
get {
|
||||
return (originName ?? "?") + "." + itemName;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the full dot-separated name of the item, in the form "listDefinitionName.itemName".
|
||||
/// Calls fullName internally.
|
||||
/// </summary>
|
||||
public override string ToString ()
|
||||
{
|
||||
return fullName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is this item the same as another item?
|
||||
/// </summary>
|
||||
public override bool Equals (object obj)
|
||||
{
|
||||
if (obj is InkListItem) {
|
||||
var otherItem = (InkListItem)obj;
|
||||
return otherItem.itemName == itemName
|
||||
&& otherItem.originName == originName;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the hashcode for an item.
|
||||
/// </summary>
|
||||
public override int GetHashCode ()
|
||||
{
|
||||
int originCode = 0;
|
||||
int itemCode = itemName.GetHashCode ();
|
||||
if (originName != null)
|
||||
originCode = originName.GetHashCode ();
|
||||
|
||||
return originCode + itemCode;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The InkList is the underlying type that's used to store an instance of a
|
||||
/// list in ink. It's not used for the *definition* of the list, but for a list
|
||||
/// value that's stored in a variable.
|
||||
/// Somewhat confusingly, it's backed by a C# Dictionary, and has nothing to
|
||||
/// do with a C# List!
|
||||
/// </summary>
|
||||
public class InkList : Dictionary<InkListItem, int>
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a new empty ink list.
|
||||
/// </summary>
|
||||
public InkList () { }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new ink list that contains the same contents as another list.
|
||||
/// </summary>
|
||||
public InkList(InkList otherList) : base(otherList)
|
||||
{
|
||||
_originNames = otherList.originNames;
|
||||
if (otherList.origins != null)
|
||||
{
|
||||
origins = new List<ListDefinition>(otherList.origins);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new empty ink list that's intended to hold items from a particular origin
|
||||
/// list definition. The origin Story is needed in order to be able to look up that definition.
|
||||
/// </summary>
|
||||
public InkList (string singleOriginListName, Story originStory)
|
||||
{
|
||||
SetInitialOriginName (singleOriginListName);
|
||||
|
||||
ListDefinition def;
|
||||
if (originStory.listDefinitions.TryListGetDefinition (singleOriginListName, out def))
|
||||
origins = new List<ListDefinition> { def };
|
||||
else
|
||||
throw new System.Exception ("InkList origin could not be found in story when constructing new list: " + singleOriginListName);
|
||||
}
|
||||
|
||||
public InkList (KeyValuePair<InkListItem, int> singleElement)
|
||||
{
|
||||
Add (singleElement.Key, singleElement.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a string to an ink list and returns for use in the story.
|
||||
/// </summary>
|
||||
/// <returns>InkList created from string list item</returns>
|
||||
/// <param name="itemKey">Item key.</param>
|
||||
/// <param name="originStory">Origin story.</param>
|
||||
public static InkList FromString(string myListItem, Story originStory) {
|
||||
var listValue = originStory.listDefinitions.FindSingleItemListWithName (myListItem);
|
||||
if (listValue)
|
||||
return new InkList (listValue.value);
|
||||
else
|
||||
throw new System.Exception ("Could not find the InkListItem from the string '" + myListItem + "' to create an InkList because it doesn't exist in the original list definition in ink.");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Adds the given item to the ink list. Note that the item must come from a list definition that
|
||||
/// is already "known" to this list, so that the item's value can be looked up. By "known", we mean
|
||||
/// that it already has items in it from that source, or it did at one point - it can't be a
|
||||
/// completely fresh empty list, or a list that only contains items from a different list definition.
|
||||
/// </summary>
|
||||
public void AddItem (InkListItem item)
|
||||
{
|
||||
if (item.originName == null) {
|
||||
AddItem (item.itemName);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var origin in origins) {
|
||||
if (origin.name == item.originName) {
|
||||
int intVal;
|
||||
if (origin.TryGetValueForItem (item, out intVal)) {
|
||||
this [item] = intVal;
|
||||
return;
|
||||
} else {
|
||||
throw new System.Exception ("Could not add the item " + item + " to this list because it doesn't exist in the original list definition in ink.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new System.Exception ("Failed to add item to list because the item was from a new list definition that wasn't previously known to this list. Only items from previously known lists can be used, so that the int value can be found.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the given item to the ink list, attempting to find the origin list definition that it belongs to.
|
||||
/// The item must therefore come from a list definition that is already "known" to this list, so that the
|
||||
/// item's value can be looked up. By "known", we mean that it already has items in it from that source, or
|
||||
/// it did at one point - it can't be a completely fresh empty list, or a list that only contains items from
|
||||
/// a different list definition.
|
||||
/// </summary>
|
||||
public void AddItem (string itemName)
|
||||
{
|
||||
ListDefinition foundListDef = null;
|
||||
|
||||
foreach (var origin in origins) {
|
||||
if (origin.ContainsItemWithName (itemName)) {
|
||||
if (foundListDef != null) {
|
||||
throw new System.Exception ("Could not add the item " + itemName + " to this list because it could come from either " + origin.name + " or " + foundListDef.name);
|
||||
} else {
|
||||
foundListDef = origin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foundListDef == null)
|
||||
throw new System.Exception ("Could not add the item " + itemName + " to this list because it isn't known to any list definitions previously associated with this list.");
|
||||
|
||||
var item = new InkListItem (foundListDef.name, itemName);
|
||||
var itemVal = foundListDef.ValueForItem(item);
|
||||
this [item] = itemVal;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this ink list contains an item with the given short name
|
||||
/// (ignoring the original list where it was defined).
|
||||
/// </summary>
|
||||
public bool ContainsItemNamed (string itemName)
|
||||
{
|
||||
foreach (var itemWithValue in this) {
|
||||
if (itemWithValue.Key.itemName == itemName) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Story has to set this so that the value knows its origin,
|
||||
// necessary for certain operations (e.g. interacting with ints).
|
||||
// Only the story has access to the full set of lists, so that
|
||||
// the origin can be resolved from the originListName.
|
||||
public List<ListDefinition> origins;
|
||||
public ListDefinition originOfMaxItem {
|
||||
get {
|
||||
if (origins == null) return null;
|
||||
|
||||
var maxOriginName = maxItem.Key.originName;
|
||||
foreach (var origin in origins) {
|
||||
if (origin.name == maxOriginName)
|
||||
return origin;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Origin name needs to be serialised when content is empty,
|
||||
// assuming a name is availble, for list definitions with variable
|
||||
// that is currently empty.
|
||||
public List<string> originNames {
|
||||
get {
|
||||
if (this.Count > 0) {
|
||||
if (_originNames == null && this.Count > 0)
|
||||
_originNames = new List<string> ();
|
||||
else
|
||||
_originNames.Clear ();
|
||||
|
||||
foreach (var itemAndValue in this)
|
||||
_originNames.Add (itemAndValue.Key.originName);
|
||||
}
|
||||
|
||||
return _originNames;
|
||||
}
|
||||
}
|
||||
List<string> _originNames;
|
||||
|
||||
public void SetInitialOriginName (string initialOriginName)
|
||||
{
|
||||
_originNames = new List<string> { initialOriginName };
|
||||
}
|
||||
|
||||
public void SetInitialOriginNames (List<string> initialOriginNames)
|
||||
{
|
||||
if (initialOriginNames == null)
|
||||
_originNames = null;
|
||||
else
|
||||
_originNames = new List<string>(initialOriginNames);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the maximum item in the list, equivalent to calling LIST_MAX(list) in ink.
|
||||
/// </summary>
|
||||
public KeyValuePair<InkListItem, int> maxItem {
|
||||
get {
|
||||
KeyValuePair<InkListItem, int> max = new KeyValuePair<InkListItem, int>();
|
||||
foreach (var kv in this) {
|
||||
if (max.Key.isNull || kv.Value > max.Value)
|
||||
max = kv;
|
||||
}
|
||||
return max;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the minimum item in the list, equivalent to calling LIST_MIN(list) in ink.
|
||||
/// </summary>
|
||||
public KeyValuePair<InkListItem, int> minItem {
|
||||
get {
|
||||
var min = new KeyValuePair<InkListItem, int> ();
|
||||
foreach (var kv in this) {
|
||||
if (min.Key.isNull || kv.Value < min.Value)
|
||||
min = kv;
|
||||
}
|
||||
return min;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The inverse of the list, equivalent to calling LIST_INVERSE(list) in ink
|
||||
/// </summary>
|
||||
public InkList inverse {
|
||||
get {
|
||||
var list = new InkList ();
|
||||
if (origins != null) {
|
||||
foreach (var origin in origins) {
|
||||
foreach (var itemAndValue in origin.items) {
|
||||
if (!this.ContainsKey (itemAndValue.Key))
|
||||
list.Add (itemAndValue.Key, itemAndValue.Value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The list of all items from the original list definition, equivalent to calling
|
||||
/// LIST_ALL(list) in ink.
|
||||
/// </summary>
|
||||
public InkList all {
|
||||
get {
|
||||
var list = new InkList ();
|
||||
if (origins != null) {
|
||||
foreach (var origin in origins) {
|
||||
foreach (var itemAndValue in origin.items)
|
||||
list[itemAndValue.Key] = itemAndValue.Value;
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new list that is the combination of the current list and one that's
|
||||
/// passed in. Equivalent to calling (list1 + list2) in ink.
|
||||
/// </summary>
|
||||
public InkList Union (InkList otherList)
|
||||
{
|
||||
var union = new InkList (this);
|
||||
foreach (var kv in otherList) {
|
||||
union [kv.Key] = kv.Value;
|
||||
}
|
||||
return union;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new list that is the intersection of the current list with another
|
||||
/// list that's passed in - i.e. a list of the items that are shared between the
|
||||
/// two other lists. Equivalent to calling (list1 ^ list2) in ink.
|
||||
/// </summary>
|
||||
public InkList Intersect (InkList otherList)
|
||||
{
|
||||
var intersection = new InkList ();
|
||||
foreach (var kv in this) {
|
||||
if (otherList.ContainsKey (kv.Key))
|
||||
intersection.Add (kv.Key, kv.Value);
|
||||
}
|
||||
return intersection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new list that's the same as the current one, except with the given items
|
||||
/// removed that are in the passed in list. Equivalent to calling (list1 - list2) in ink.
|
||||
/// </summary>
|
||||
/// <param name="listToRemove">List to remove.</param>
|
||||
public InkList Without (InkList listToRemove)
|
||||
{
|
||||
var result = new InkList (this);
|
||||
foreach (var kv in listToRemove)
|
||||
result.Remove (kv.Key);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the current list contains all the items that are in the list that
|
||||
/// is passed in. Equivalent to calling (list1 ? list2) in ink.
|
||||
/// </summary>
|
||||
/// <param name="otherList">Other list.</param>
|
||||
public bool Contains (InkList otherList)
|
||||
{
|
||||
foreach (var kv in otherList) {
|
||||
if (!this.ContainsKey (kv.Key)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if all the item values in the current list are greater than all the
|
||||
/// item values in the passed in list. Equivalent to calling (list1 > list2) in ink.
|
||||
/// </summary>
|
||||
public bool GreaterThan (InkList otherList)
|
||||
{
|
||||
if (Count == 0) return false;
|
||||
if (otherList.Count == 0) return true;
|
||||
|
||||
// All greater
|
||||
return minItem.Value > otherList.maxItem.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the item values in the current list overlap or are all greater than
|
||||
/// the item values in the passed in list. None of the item values in the current list must
|
||||
/// fall below the item values in the passed in list. Equivalent to (list1 >= list2) in ink,
|
||||
/// or LIST_MIN(list1) >= LIST_MIN(list2) && LIST_MAX(list1) >= LIST_MAX(list2).
|
||||
/// </summary>
|
||||
public bool GreaterThanOrEquals (InkList otherList)
|
||||
{
|
||||
if (Count == 0) return false;
|
||||
if (otherList.Count == 0) return true;
|
||||
|
||||
return minItem.Value >= otherList.minItem.Value
|
||||
&& maxItem.Value >= otherList.maxItem.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if all the item values in the current list are less than all the
|
||||
/// item values in the passed in list. Equivalent to calling (list1 < list2) in ink.
|
||||
/// </summary>
|
||||
public bool LessThan (InkList otherList)
|
||||
{
|
||||
if (otherList.Count == 0) return false;
|
||||
if (Count == 0) return true;
|
||||
|
||||
return maxItem.Value < otherList.minItem.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the item values in the current list overlap or are all less than
|
||||
/// the item values in the passed in list. None of the item values in the current list must
|
||||
/// go above the item values in the passed in list. Equivalent to (list1 <= list2) in ink,
|
||||
/// or LIST_MAX(list1) <= LIST_MAX(list2) && LIST_MIN(list1) <= LIST_MIN(list2).
|
||||
/// </summary>
|
||||
public bool LessThanOrEquals (InkList otherList)
|
||||
{
|
||||
if (otherList.Count == 0) return false;
|
||||
if (Count == 0) return true;
|
||||
|
||||
return maxItem.Value <= otherList.maxItem.Value
|
||||
&& minItem.Value <= otherList.minItem.Value;
|
||||
}
|
||||
|
||||
public InkList MaxAsList ()
|
||||
{
|
||||
if (Count > 0)
|
||||
return new InkList (maxItem);
|
||||
else
|
||||
return new InkList ();
|
||||
}
|
||||
|
||||
public InkList MinAsList ()
|
||||
{
|
||||
if (Count > 0)
|
||||
return new InkList (minItem);
|
||||
else
|
||||
return new InkList ();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a sublist with the elements given the minimum and maxmimum bounds.
|
||||
/// The bounds can either be ints which are indices into the entire (sorted) list,
|
||||
/// or they can be InkLists themselves. These are intended to be single-item lists so
|
||||
/// you can specify the upper and lower bounds. If you pass in multi-item lists, it'll
|
||||
/// use the minimum and maximum items in those lists respectively.
|
||||
/// WARNING: Calling this method requires a full sort of all the elements in the list.
|
||||
/// </summary>
|
||||
public InkList ListWithSubRange(object minBound, object maxBound)
|
||||
{
|
||||
if (this.Count == 0) return new InkList();
|
||||
|
||||
var ordered = orderedItems;
|
||||
|
||||
int minValue = 0;
|
||||
int maxValue = int.MaxValue;
|
||||
|
||||
if (minBound is int)
|
||||
{
|
||||
minValue = (int)minBound;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
if( minBound is InkList && ((InkList)minBound).Count > 0 )
|
||||
minValue = ((InkList)minBound).minItem.Value;
|
||||
}
|
||||
|
||||
if (maxBound is int)
|
||||
maxValue = (int)maxBound;
|
||||
else
|
||||
{
|
||||
if (minBound is InkList && ((InkList)minBound).Count > 0)
|
||||
maxValue = ((InkList)maxBound).maxItem.Value;
|
||||
}
|
||||
|
||||
var subList = new InkList();
|
||||
subList.SetInitialOriginNames(originNames);
|
||||
foreach(var item in ordered) {
|
||||
if( item.Value >= minValue && item.Value <= maxValue ) {
|
||||
subList.Add(item.Key, item.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return subList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the passed object is also an ink list that contains
|
||||
/// the same items as the current list, false otherwise.
|
||||
/// </summary>
|
||||
public override bool Equals (object other)
|
||||
{
|
||||
var otherRawList = other as InkList;
|
||||
if (otherRawList == null) return false;
|
||||
if (otherRawList.Count != Count) return false;
|
||||
|
||||
foreach (var kv in this) {
|
||||
if (!otherRawList.ContainsKey (kv.Key))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the hashcode for this object, used for comparisons and inserting into dictionaries.
|
||||
/// </summary>
|
||||
public override int GetHashCode ()
|
||||
{
|
||||
int ownHash = 0;
|
||||
foreach (var kv in this)
|
||||
ownHash += kv.Key.GetHashCode ();
|
||||
return ownHash;
|
||||
}
|
||||
|
||||
List<KeyValuePair<InkListItem, int>> orderedItems {
|
||||
get {
|
||||
var ordered = new List<KeyValuePair<InkListItem, int>>();
|
||||
ordered.AddRange(this);
|
||||
ordered.Sort((x, y) => {
|
||||
// Ensure consistent ordering of mixed lists.
|
||||
if( x.Value == y.Value ) {
|
||||
return x.Key.originName.CompareTo(y.Key.originName);
|
||||
} else {
|
||||
return x.Value.CompareTo(y.Value);
|
||||
}
|
||||
});
|
||||
return ordered;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string in the form "a, b, c" with the names of the items in the list, without
|
||||
/// the origin list definition names. Equivalent to writing {list} in ink.
|
||||
/// </summary>
|
||||
public override string ToString ()
|
||||
{
|
||||
var ordered = orderedItems;
|
||||
|
||||
var sb = new StringBuilder ();
|
||||
for (int i = 0; i < ordered.Count; i++) {
|
||||
if (i > 0)
|
||||
sb.Append (", ");
|
||||
|
||||
var item = ordered [i].Key;
|
||||
sb.Append (item.itemName);
|
||||
}
|
||||
|
||||
return sb.ToString ();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3a56f188ddfd3452386a797f878ab7ed
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,720 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ink.Runtime
|
||||
{
|
||||
public static class Json
|
||||
{
|
||||
public static List<T> JArrayToRuntimeObjList<T>(List<object> jArray, bool skipLast=false) where T : Runtime.Object
|
||||
{
|
||||
int count = jArray.Count;
|
||||
if (skipLast)
|
||||
count--;
|
||||
|
||||
var list = new List<T> (jArray.Count);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
var jTok = jArray [i];
|
||||
var runtimeObj = JTokenToRuntimeObject (jTok) as T;
|
||||
list.Add (runtimeObj);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public static List<Runtime.Object> JArrayToRuntimeObjList(List<object> jArray, bool skipLast=false)
|
||||
{
|
||||
return JArrayToRuntimeObjList<Runtime.Object> (jArray, skipLast);
|
||||
}
|
||||
|
||||
public static void WriteDictionaryRuntimeObjs(SimpleJson.Writer writer, Dictionary<string, Runtime.Object> dictionary)
|
||||
{
|
||||
writer.WriteObjectStart();
|
||||
foreach(var keyVal in dictionary) {
|
||||
writer.WritePropertyStart(keyVal.Key);
|
||||
WriteRuntimeObject(writer, keyVal.Value);
|
||||
writer.WritePropertyEnd();
|
||||
}
|
||||
writer.WriteObjectEnd();
|
||||
}
|
||||
|
||||
|
||||
public static void WriteListRuntimeObjs(SimpleJson.Writer writer, List<Runtime.Object> list)
|
||||
{
|
||||
writer.WriteArrayStart();
|
||||
foreach (var val in list)
|
||||
{
|
||||
WriteRuntimeObject(writer, val);
|
||||
}
|
||||
writer.WriteArrayEnd();
|
||||
}
|
||||
|
||||
public static void WriteIntDictionary(SimpleJson.Writer writer, Dictionary<string, int> dict)
|
||||
{
|
||||
writer.WriteObjectStart();
|
||||
foreach (var keyVal in dict)
|
||||
writer.WriteProperty(keyVal.Key, keyVal.Value);
|
||||
writer.WriteObjectEnd();
|
||||
}
|
||||
|
||||
public static void WriteRuntimeObject(SimpleJson.Writer writer, Runtime.Object obj)
|
||||
{
|
||||
var container = obj as Container;
|
||||
if (container) {
|
||||
WriteRuntimeContainer(writer, container);
|
||||
return;
|
||||
}
|
||||
|
||||
var divert = obj as Divert;
|
||||
if (divert)
|
||||
{
|
||||
string divTypeKey = "->";
|
||||
if (divert.isExternal)
|
||||
divTypeKey = "x()";
|
||||
else if (divert.pushesToStack)
|
||||
{
|
||||
if (divert.stackPushType == PushPopType.Function)
|
||||
divTypeKey = "f()";
|
||||
else if (divert.stackPushType == PushPopType.Tunnel)
|
||||
divTypeKey = "->t->";
|
||||
}
|
||||
|
||||
string targetStr;
|
||||
if (divert.hasVariableTarget)
|
||||
targetStr = divert.variableDivertName;
|
||||
else
|
||||
targetStr = divert.targetPathString;
|
||||
|
||||
writer.WriteObjectStart();
|
||||
|
||||
writer.WriteProperty(divTypeKey, targetStr);
|
||||
|
||||
if (divert.hasVariableTarget)
|
||||
writer.WriteProperty("var", true);
|
||||
|
||||
if (divert.isConditional)
|
||||
writer.WriteProperty("c", true);
|
||||
|
||||
if (divert.externalArgs > 0)
|
||||
writer.WriteProperty("exArgs", divert.externalArgs);
|
||||
|
||||
writer.WriteObjectEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
var choicePoint = obj as ChoicePoint;
|
||||
if (choicePoint)
|
||||
{
|
||||
writer.WriteObjectStart();
|
||||
writer.WriteProperty("*", choicePoint.pathStringOnChoice);
|
||||
writer.WriteProperty("flg", choicePoint.flags);
|
||||
writer.WriteObjectEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
var boolVal = obj as BoolValue;
|
||||
if (boolVal) {
|
||||
writer.Write(boolVal.value);
|
||||
return;
|
||||
}
|
||||
|
||||
var intVal = obj as IntValue;
|
||||
if (intVal) {
|
||||
writer.Write(intVal.value);
|
||||
return;
|
||||
}
|
||||
|
||||
var floatVal = obj as FloatValue;
|
||||
if (floatVal) {
|
||||
writer.Write(floatVal.value);
|
||||
return;
|
||||
}
|
||||
|
||||
var strVal = obj as StringValue;
|
||||
if (strVal)
|
||||
{
|
||||
if (strVal.isNewline)
|
||||
writer.Write("\\n", escape:false);
|
||||
else {
|
||||
writer.WriteStringStart();
|
||||
writer.WriteStringInner("^");
|
||||
writer.WriteStringInner(strVal.value);
|
||||
writer.WriteStringEnd();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var listVal = obj as ListValue;
|
||||
if (listVal)
|
||||
{
|
||||
WriteInkList(writer, listVal);
|
||||
return;
|
||||
}
|
||||
|
||||
var divTargetVal = obj as DivertTargetValue;
|
||||
if (divTargetVal)
|
||||
{
|
||||
writer.WriteObjectStart();
|
||||
writer.WriteProperty("^->", divTargetVal.value.componentsString);
|
||||
writer.WriteObjectEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
var varPtrVal = obj as VariablePointerValue;
|
||||
if (varPtrVal)
|
||||
{
|
||||
writer.WriteObjectStart();
|
||||
writer.WriteProperty("^var", varPtrVal.value);
|
||||
writer.WriteProperty("ci", varPtrVal.contextIndex);
|
||||
writer.WriteObjectEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
var glue = obj as Runtime.Glue;
|
||||
if (glue) {
|
||||
writer.Write("<>");
|
||||
return;
|
||||
}
|
||||
|
||||
var controlCmd = obj as ControlCommand;
|
||||
if (controlCmd)
|
||||
{
|
||||
writer.Write(_controlCommandNames[(int)controlCmd.commandType]);
|
||||
return;
|
||||
}
|
||||
|
||||
var nativeFunc = obj as Runtime.NativeFunctionCall;
|
||||
if (nativeFunc)
|
||||
{
|
||||
var name = nativeFunc.name;
|
||||
|
||||
// Avoid collision with ^ used to indicate a string
|
||||
if (name == "^") name = "L^";
|
||||
|
||||
writer.Write(name);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Variable reference
|
||||
var varRef = obj as VariableReference;
|
||||
if (varRef)
|
||||
{
|
||||
writer.WriteObjectStart();
|
||||
|
||||
string readCountPath = varRef.pathStringForCount;
|
||||
if (readCountPath != null)
|
||||
{
|
||||
writer.WriteProperty("CNT?", readCountPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteProperty("VAR?", varRef.name);
|
||||
}
|
||||
|
||||
writer.WriteObjectEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
// Variable assignment
|
||||
var varAss = obj as VariableAssignment;
|
||||
if (varAss)
|
||||
{
|
||||
writer.WriteObjectStart();
|
||||
|
||||
string key = varAss.isGlobal ? "VAR=" : "temp=";
|
||||
writer.WriteProperty(key, varAss.variableName);
|
||||
|
||||
// Reassignment?
|
||||
if (!varAss.isNewDeclaration)
|
||||
writer.WriteProperty("re", true);
|
||||
|
||||
writer.WriteObjectEnd();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Void
|
||||
var voidObj = obj as Void;
|
||||
if (voidObj) {
|
||||
writer.Write("void");
|
||||
return;
|
||||
}
|
||||
|
||||
// Tag
|
||||
var tag = obj as Tag;
|
||||
if (tag)
|
||||
{
|
||||
writer.WriteObjectStart();
|
||||
writer.WriteProperty("#", tag.text);
|
||||
writer.WriteObjectEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
// Used when serialising save state only
|
||||
var choice = obj as Choice;
|
||||
if (choice) {
|
||||
WriteChoice(writer, choice);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new System.Exception("Failed to write runtime object to JSON: " + obj);
|
||||
}
|
||||
|
||||
public static Dictionary<string, Runtime.Object> JObjectToDictionaryRuntimeObjs(Dictionary<string, object> jObject)
|
||||
{
|
||||
var dict = new Dictionary<string, Runtime.Object> (jObject.Count);
|
||||
|
||||
foreach (var keyVal in jObject) {
|
||||
dict [keyVal.Key] = JTokenToRuntimeObject(keyVal.Value);
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
public static Dictionary<string, int> JObjectToIntDictionary(Dictionary<string, object> jObject)
|
||||
{
|
||||
var dict = new Dictionary<string, int> (jObject.Count);
|
||||
foreach (var keyVal in jObject) {
|
||||
dict [keyVal.Key] = (int)keyVal.Value;
|
||||
}
|
||||
return dict;
|
||||
}
|
||||
|
||||
// ----------------------
|
||||
// JSON ENCODING SCHEME
|
||||
// ----------------------
|
||||
//
|
||||
// Glue: "<>", "G<", "G>"
|
||||
//
|
||||
// ControlCommand: "ev", "out", "/ev", "du" "pop", "->->", "~ret", "str", "/str", "nop",
|
||||
// "choiceCnt", "turns", "visit", "seq", "thread", "done", "end"
|
||||
//
|
||||
// NativeFunction: "+", "-", "/", "*", "%" "~", "==", ">", "<", ">=", "<=", "!=", "!"... etc
|
||||
//
|
||||
// Void: "void"
|
||||
//
|
||||
// Value: "^string value", "^^string value beginning with ^"
|
||||
// 5, 5.2
|
||||
// {"^->": "path.target"}
|
||||
// {"^var": "varname", "ci": 0}
|
||||
//
|
||||
// Container: [...]
|
||||
// [...,
|
||||
// {
|
||||
// "subContainerName": ...,
|
||||
// "#f": 5, // flags
|
||||
// "#n": "containerOwnName" // only if not redundant
|
||||
// }
|
||||
// ]
|
||||
//
|
||||
// Divert: {"->": "path.target", "c": true }
|
||||
// {"->": "path.target", "var": true}
|
||||
// {"f()": "path.func"}
|
||||
// {"->t->": "path.tunnel"}
|
||||
// {"x()": "externalFuncName", "exArgs": 5}
|
||||
//
|
||||
// Var Assign: {"VAR=": "varName", "re": true} // reassignment
|
||||
// {"temp=": "varName"}
|
||||
//
|
||||
// Var ref: {"VAR?": "varName"}
|
||||
// {"CNT?": "stitch name"}
|
||||
//
|
||||
// ChoicePoint: {"*": pathString,
|
||||
// "flg": 18 }
|
||||
//
|
||||
// Choice: Nothing too clever, it's only used in the save state,
|
||||
// there's not likely to be many of them.
|
||||
//
|
||||
// Tag: {"#": "the tag text"}
|
||||
public static Runtime.Object JTokenToRuntimeObject(object token)
|
||||
{
|
||||
if (token is int || token is float || token is bool) {
|
||||
return Value.Create (token);
|
||||
}
|
||||
|
||||
if (token is string) {
|
||||
string str = (string)token;
|
||||
|
||||
// String value
|
||||
char firstChar = str[0];
|
||||
if (firstChar == '^')
|
||||
return new StringValue (str.Substring (1));
|
||||
else if( firstChar == '\n' && str.Length == 1)
|
||||
return new StringValue ("\n");
|
||||
|
||||
// Glue
|
||||
if (str == "<>") return new Runtime.Glue ();
|
||||
|
||||
// Control commands (would looking up in a hash set be faster?)
|
||||
for (int i = 0; i < _controlCommandNames.Length; ++i) {
|
||||
string cmdName = _controlCommandNames [i];
|
||||
if (str == cmdName) {
|
||||
return new Runtime.ControlCommand ((ControlCommand.CommandType)i);
|
||||
}
|
||||
}
|
||||
|
||||
// Native functions
|
||||
// "^" conflicts with the way to identify strings, so now
|
||||
// we know it's not a string, we can convert back to the proper
|
||||
// symbol for the operator.
|
||||
if (str == "L^") str = "^";
|
||||
if( NativeFunctionCall.CallExistsWithName(str) )
|
||||
return NativeFunctionCall.CallWithName (str);
|
||||
|
||||
// Pop
|
||||
if (str == "->->")
|
||||
return Runtime.ControlCommand.PopTunnel ();
|
||||
else if (str == "~ret")
|
||||
return Runtime.ControlCommand.PopFunction ();
|
||||
|
||||
// Void
|
||||
if (str == "void")
|
||||
return new Runtime.Void ();
|
||||
}
|
||||
|
||||
if (token is Dictionary<string, object>) {
|
||||
|
||||
var obj = (Dictionary < string, object> )token;
|
||||
object propValue;
|
||||
|
||||
// Divert target value to path
|
||||
if (obj.TryGetValue ("^->", out propValue))
|
||||
return new DivertTargetValue (new Path ((string)propValue));
|
||||
|
||||
// VariablePointerValue
|
||||
if (obj.TryGetValue ("^var", out propValue)) {
|
||||
var varPtr = new VariablePointerValue ((string)propValue);
|
||||
if (obj.TryGetValue ("ci", out propValue))
|
||||
varPtr.contextIndex = (int)propValue;
|
||||
return varPtr;
|
||||
}
|
||||
|
||||
// Divert
|
||||
bool isDivert = false;
|
||||
bool pushesToStack = false;
|
||||
PushPopType divPushType = PushPopType.Function;
|
||||
bool external = false;
|
||||
if (obj.TryGetValue ("->", out propValue)) {
|
||||
isDivert = true;
|
||||
}
|
||||
else if (obj.TryGetValue ("f()", out propValue)) {
|
||||
isDivert = true;
|
||||
pushesToStack = true;
|
||||
divPushType = PushPopType.Function;
|
||||
}
|
||||
else if (obj.TryGetValue ("->t->", out propValue)) {
|
||||
isDivert = true;
|
||||
pushesToStack = true;
|
||||
divPushType = PushPopType.Tunnel;
|
||||
}
|
||||
else if (obj.TryGetValue ("x()", out propValue)) {
|
||||
isDivert = true;
|
||||
external = true;
|
||||
pushesToStack = false;
|
||||
divPushType = PushPopType.Function;
|
||||
}
|
||||
if (isDivert) {
|
||||
var divert = new Divert ();
|
||||
divert.pushesToStack = pushesToStack;
|
||||
divert.stackPushType = divPushType;
|
||||
divert.isExternal = external;
|
||||
|
||||
string target = propValue.ToString ();
|
||||
|
||||
if (obj.TryGetValue ("var", out propValue))
|
||||
divert.variableDivertName = target;
|
||||
else
|
||||
divert.targetPathString = target;
|
||||
|
||||
divert.isConditional = obj.TryGetValue("c", out propValue);
|
||||
|
||||
if (external) {
|
||||
if (obj.TryGetValue ("exArgs", out propValue))
|
||||
divert.externalArgs = (int)propValue;
|
||||
}
|
||||
|
||||
return divert;
|
||||
}
|
||||
|
||||
// Choice
|
||||
if (obj.TryGetValue ("*", out propValue)) {
|
||||
var choice = new ChoicePoint ();
|
||||
choice.pathStringOnChoice = propValue.ToString();
|
||||
|
||||
if (obj.TryGetValue ("flg", out propValue))
|
||||
choice.flags = (int)propValue;
|
||||
|
||||
return choice;
|
||||
}
|
||||
|
||||
// Variable reference
|
||||
if (obj.TryGetValue ("VAR?", out propValue)) {
|
||||
return new VariableReference (propValue.ToString ());
|
||||
} else if (obj.TryGetValue ("CNT?", out propValue)) {
|
||||
var readCountVarRef = new VariableReference ();
|
||||
readCountVarRef.pathStringForCount = propValue.ToString ();
|
||||
return readCountVarRef;
|
||||
}
|
||||
|
||||
// Variable assignment
|
||||
bool isVarAss = false;
|
||||
bool isGlobalVar = false;
|
||||
if (obj.TryGetValue ("VAR=", out propValue)) {
|
||||
isVarAss = true;
|
||||
isGlobalVar = true;
|
||||
} else if (obj.TryGetValue ("temp=", out propValue)) {
|
||||
isVarAss = true;
|
||||
isGlobalVar = false;
|
||||
}
|
||||
if (isVarAss) {
|
||||
var varName = propValue.ToString ();
|
||||
var isNewDecl = !obj.TryGetValue("re", out propValue);
|
||||
var varAss = new VariableAssignment (varName, isNewDecl);
|
||||
varAss.isGlobal = isGlobalVar;
|
||||
return varAss;
|
||||
}
|
||||
|
||||
// Tag
|
||||
if (obj.TryGetValue ("#", out propValue)) {
|
||||
return new Runtime.Tag ((string)propValue);
|
||||
}
|
||||
|
||||
// List value
|
||||
if (obj.TryGetValue ("list", out propValue)) {
|
||||
var listContent = (Dictionary<string, object>)propValue;
|
||||
var rawList = new InkList ();
|
||||
if (obj.TryGetValue ("origins", out propValue)) {
|
||||
var namesAsObjs = (List<object>)propValue;
|
||||
rawList.SetInitialOriginNames (namesAsObjs.Cast<string>().ToList());
|
||||
}
|
||||
foreach (var nameToVal in listContent) {
|
||||
var item = new InkListItem (nameToVal.Key);
|
||||
var val = (int)nameToVal.Value;
|
||||
rawList.Add (item, val);
|
||||
}
|
||||
return new ListValue (rawList);
|
||||
}
|
||||
|
||||
// Used when serialising save state only
|
||||
if (obj ["originalChoicePath"] != null)
|
||||
return JObjectToChoice (obj);
|
||||
}
|
||||
|
||||
// Array is always a Runtime.Container
|
||||
if (token is List<object>) {
|
||||
return JArrayToContainer((List<object>)token);
|
||||
}
|
||||
|
||||
if (token == null)
|
||||
return null;
|
||||
|
||||
throw new System.Exception ("Failed to convert token to runtime object: " + token);
|
||||
}
|
||||
|
||||
public static void WriteRuntimeContainer(SimpleJson.Writer writer, Container container, bool withoutName = false)
|
||||
{
|
||||
writer.WriteArrayStart();
|
||||
|
||||
foreach (var c in container.content)
|
||||
WriteRuntimeObject(writer, c);
|
||||
|
||||
// Container is always an array [...]
|
||||
// But the final element is always either:
|
||||
// - a dictionary containing the named content, as well as possibly
|
||||
// the key "#" with the count flags
|
||||
// - null, if neither of the above
|
||||
var namedOnlyContent = container.namedOnlyContent;
|
||||
var countFlags = container.countFlags;
|
||||
var hasNameProperty = container.name != null && !withoutName;
|
||||
|
||||
bool hasTerminator = namedOnlyContent != null || countFlags > 0 || hasNameProperty;
|
||||
|
||||
if( hasTerminator )
|
||||
writer.WriteObjectStart();
|
||||
|
||||
if ( namedOnlyContent != null ) {
|
||||
foreach(var namedContent in namedOnlyContent) {
|
||||
var name = namedContent.Key;
|
||||
var namedContainer = namedContent.Value as Container;
|
||||
writer.WritePropertyStart(name);
|
||||
WriteRuntimeContainer(writer, namedContainer, withoutName:true);
|
||||
writer.WritePropertyEnd();
|
||||
}
|
||||
}
|
||||
|
||||
if (countFlags > 0)
|
||||
writer.WriteProperty("#f", countFlags);
|
||||
|
||||
if (hasNameProperty)
|
||||
writer.WriteProperty("#n", container.name);
|
||||
|
||||
if (hasTerminator)
|
||||
writer.WriteObjectEnd();
|
||||
else
|
||||
writer.WriteNull();
|
||||
|
||||
writer.WriteArrayEnd();
|
||||
}
|
||||
|
||||
static Container JArrayToContainer(List<object> jArray)
|
||||
{
|
||||
var container = new Container ();
|
||||
container.content = JArrayToRuntimeObjList (jArray, skipLast:true);
|
||||
|
||||
// Final object in the array is always a combination of
|
||||
// - named content
|
||||
// - a "#f" key with the countFlags
|
||||
// (if either exists at all, otherwise null)
|
||||
var terminatingObj = jArray [jArray.Count - 1] as Dictionary<string, object>;
|
||||
if (terminatingObj != null) {
|
||||
|
||||
var namedOnlyContent = new Dictionary<string, Runtime.Object> (terminatingObj.Count);
|
||||
|
||||
foreach (var keyVal in terminatingObj) {
|
||||
if (keyVal.Key == "#f") {
|
||||
container.countFlags = (int)keyVal.Value;
|
||||
} else if (keyVal.Key == "#n") {
|
||||
container.name = keyVal.Value.ToString ();
|
||||
} else {
|
||||
var namedContentItem = JTokenToRuntimeObject(keyVal.Value);
|
||||
var namedSubContainer = namedContentItem as Container;
|
||||
if (namedSubContainer)
|
||||
namedSubContainer.name = keyVal.Key;
|
||||
namedOnlyContent [keyVal.Key] = namedContentItem;
|
||||
}
|
||||
}
|
||||
|
||||
container.namedOnlyContent = namedOnlyContent;
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
static Choice JObjectToChoice(Dictionary<string, object> jObj)
|
||||
{
|
||||
var choice = new Choice();
|
||||
choice.text = jObj ["text"].ToString();
|
||||
choice.index = (int)jObj ["index"];
|
||||
choice.sourcePath = jObj ["originalChoicePath"].ToString();
|
||||
choice.originalThreadIndex = (int)jObj ["originalThreadIndex"];
|
||||
choice.pathStringOnChoice = jObj ["targetPath"].ToString();
|
||||
return choice;
|
||||
}
|
||||
public static void WriteChoice(SimpleJson.Writer writer, Choice choice)
|
||||
{
|
||||
writer.WriteObjectStart();
|
||||
writer.WriteProperty("text", choice.text);
|
||||
writer.WriteProperty("index", choice.index);
|
||||
writer.WriteProperty("originalChoicePath", choice.sourcePath);
|
||||
writer.WriteProperty("originalThreadIndex", choice.originalThreadIndex);
|
||||
writer.WriteProperty("targetPath", choice.pathStringOnChoice);
|
||||
writer.WriteObjectEnd();
|
||||
}
|
||||
|
||||
static void WriteInkList(SimpleJson.Writer writer, ListValue listVal)
|
||||
{
|
||||
var rawList = listVal.value;
|
||||
|
||||
writer.WriteObjectStart();
|
||||
|
||||
writer.WritePropertyStart("list");
|
||||
|
||||
writer.WriteObjectStart();
|
||||
|
||||
foreach (var itemAndValue in rawList)
|
||||
{
|
||||
var item = itemAndValue.Key;
|
||||
int itemVal = itemAndValue.Value;
|
||||
|
||||
writer.WritePropertyNameStart();
|
||||
writer.WritePropertyNameInner(item.originName ?? "?");
|
||||
writer.WritePropertyNameInner(".");
|
||||
writer.WritePropertyNameInner(item.itemName);
|
||||
writer.WritePropertyNameEnd();
|
||||
|
||||
writer.Write(itemVal);
|
||||
|
||||
writer.WritePropertyEnd();
|
||||
}
|
||||
|
||||
writer.WriteObjectEnd();
|
||||
|
||||
writer.WritePropertyEnd();
|
||||
|
||||
if (rawList.Count == 0 && rawList.originNames != null && rawList.originNames.Count > 0)
|
||||
{
|
||||
writer.WritePropertyStart("origins");
|
||||
writer.WriteArrayStart();
|
||||
foreach (var name in rawList.originNames)
|
||||
writer.Write(name);
|
||||
writer.WriteArrayEnd();
|
||||
writer.WritePropertyEnd();
|
||||
}
|
||||
|
||||
writer.WriteObjectEnd();
|
||||
}
|
||||
|
||||
public static ListDefinitionsOrigin JTokenToListDefinitions (object obj)
|
||||
{
|
||||
var defsObj = (Dictionary<string, object>)obj;
|
||||
|
||||
var allDefs = new List<ListDefinition> ();
|
||||
|
||||
foreach (var kv in defsObj) {
|
||||
var name = (string) kv.Key;
|
||||
var listDefJson = (Dictionary<string, object>)kv.Value;
|
||||
|
||||
// Cast (string, object) to (string, int) for items
|
||||
var items = new Dictionary<string, int> ();
|
||||
foreach (var nameValue in listDefJson)
|
||||
items.Add(nameValue.Key, (int)nameValue.Value);
|
||||
|
||||
var def = new ListDefinition (name, items);
|
||||
allDefs.Add (def);
|
||||
}
|
||||
|
||||
return new ListDefinitionsOrigin (allDefs);
|
||||
}
|
||||
|
||||
static Json()
|
||||
{
|
||||
_controlCommandNames = new string[(int)ControlCommand.CommandType.TOTAL_VALUES];
|
||||
|
||||
_controlCommandNames [(int)ControlCommand.CommandType.EvalStart] = "ev";
|
||||
_controlCommandNames [(int)ControlCommand.CommandType.EvalOutput] = "out";
|
||||
_controlCommandNames [(int)ControlCommand.CommandType.EvalEnd] = "/ev";
|
||||
_controlCommandNames [(int)ControlCommand.CommandType.Duplicate] = "du";
|
||||
_controlCommandNames [(int)ControlCommand.CommandType.PopEvaluatedValue] = "pop";
|
||||
_controlCommandNames [(int)ControlCommand.CommandType.PopFunction] = "~ret";
|
||||
_controlCommandNames [(int)ControlCommand.CommandType.PopTunnel] = "->->";
|
||||
_controlCommandNames [(int)ControlCommand.CommandType.BeginString] = "str";
|
||||
_controlCommandNames [(int)ControlCommand.CommandType.EndString] = "/str";
|
||||
_controlCommandNames [(int)ControlCommand.CommandType.NoOp] = "nop";
|
||||
_controlCommandNames [(int)ControlCommand.CommandType.ChoiceCount] = "choiceCnt";
|
||||
_controlCommandNames [(int)ControlCommand.CommandType.Turns] = "turn";
|
||||
_controlCommandNames [(int)ControlCommand.CommandType.TurnsSince] = "turns";
|
||||
_controlCommandNames [(int)ControlCommand.CommandType.ReadCount] = "readc";
|
||||
_controlCommandNames [(int)ControlCommand.CommandType.Random] = "rnd";
|
||||
_controlCommandNames [(int)ControlCommand.CommandType.SeedRandom] = "srnd";
|
||||
_controlCommandNames [(int)ControlCommand.CommandType.VisitIndex] = "visit";
|
||||
_controlCommandNames [(int)ControlCommand.CommandType.SequenceShuffleIndex] = "seq";
|
||||
_controlCommandNames [(int)ControlCommand.CommandType.StartThread] = "thread";
|
||||
_controlCommandNames [(int)ControlCommand.CommandType.Done] = "done";
|
||||
_controlCommandNames [(int)ControlCommand.CommandType.End] = "end";
|
||||
_controlCommandNames [(int)ControlCommand.CommandType.ListFromInt] = "listInt";
|
||||
_controlCommandNames [(int)ControlCommand.CommandType.ListRange] = "range";
|
||||
_controlCommandNames [(int)ControlCommand.CommandType.ListRandom] = "lrnd";
|
||||
|
||||
for (int i = 0; i < (int)ControlCommand.CommandType.TOTAL_VALUES; ++i) {
|
||||
if (_controlCommandNames [i] == null)
|
||||
throw new System.Exception ("Control command not accounted for in serialisation");
|
||||
}
|
||||
}
|
||||
|
||||
static string[] _controlCommandNames;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1365b619ff45f4d12b7bf20cc52a4173
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,75 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ink.Runtime
|
||||
{
|
||||
public class ListDefinition
|
||||
{
|
||||
public string name { get { return _name; } }
|
||||
|
||||
public Dictionary<InkListItem, int> items {
|
||||
get {
|
||||
if (_items == null) {
|
||||
_items = new Dictionary<InkListItem, int> ();
|
||||
foreach (var itemNameAndValue in _itemNameToValues) {
|
||||
var item = new InkListItem (name, itemNameAndValue.Key);
|
||||
_items [item] = itemNameAndValue.Value;
|
||||
}
|
||||
}
|
||||
return _items;
|
||||
}
|
||||
}
|
||||
Dictionary<InkListItem, int> _items;
|
||||
|
||||
public int ValueForItem (InkListItem item)
|
||||
{
|
||||
int intVal;
|
||||
if (_itemNameToValues.TryGetValue (item.itemName, out intVal))
|
||||
return intVal;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
public bool ContainsItem (InkListItem item)
|
||||
{
|
||||
if (item.originName != name) return false;
|
||||
|
||||
return _itemNameToValues.ContainsKey (item.itemName);
|
||||
}
|
||||
|
||||
public bool ContainsItemWithName (string itemName)
|
||||
{
|
||||
return _itemNameToValues.ContainsKey (itemName);
|
||||
}
|
||||
|
||||
public bool TryGetItemWithValue (int val, out InkListItem item)
|
||||
{
|
||||
foreach (var namedItem in _itemNameToValues) {
|
||||
if (namedItem.Value == val) {
|
||||
item = new InkListItem (name, namedItem.Key);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
item = InkListItem.Null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetValueForItem (InkListItem item, out int intVal)
|
||||
{
|
||||
return _itemNameToValues.TryGetValue (item.itemName, out intVal);
|
||||
}
|
||||
|
||||
public ListDefinition (string name, Dictionary<string, int> items)
|
||||
{
|
||||
_name = name;
|
||||
_itemNameToValues = items;
|
||||
}
|
||||
|
||||
string _name;
|
||||
|
||||
// The main representation should be simple item names rather than a RawListItem,
|
||||
// since we mainly want to access items based on their simple name, since that's
|
||||
// how they'll be most commonly requested from ink.
|
||||
Dictionary<string, int> _itemNameToValues;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: efacbd9aad3e9480d9c82bbd58922944
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,53 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ink.Runtime
|
||||
{
|
||||
public class ListDefinitionsOrigin
|
||||
{
|
||||
public List<Runtime.ListDefinition> lists {
|
||||
get {
|
||||
var listOfLists = new List<Runtime.ListDefinition> ();
|
||||
foreach (var namedList in _lists) {
|
||||
listOfLists.Add (namedList.Value);
|
||||
}
|
||||
return listOfLists;
|
||||
}
|
||||
}
|
||||
|
||||
public ListDefinitionsOrigin (List<Runtime.ListDefinition> lists)
|
||||
{
|
||||
_lists = new Dictionary<string, ListDefinition> ();
|
||||
_allUnambiguousListValueCache = new Dictionary<string, ListValue>();
|
||||
|
||||
foreach (var list in lists) {
|
||||
_lists [list.name] = list;
|
||||
|
||||
foreach(var itemWithValue in list.items) {
|
||||
var item = itemWithValue.Key;
|
||||
var val = itemWithValue.Value;
|
||||
var listValue = new ListValue(item, val);
|
||||
|
||||
// May be ambiguous, but compiler should've caught that,
|
||||
// so we may be doing some replacement here, but that's okay.
|
||||
_allUnambiguousListValueCache[item.itemName] = listValue;
|
||||
_allUnambiguousListValueCache[item.fullName] = listValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryListGetDefinition (string name, out ListDefinition def)
|
||||
{
|
||||
return _lists.TryGetValue (name, out def);
|
||||
}
|
||||
|
||||
public ListValue FindSingleItemListWithName (string name)
|
||||
{
|
||||
ListValue val = null;
|
||||
_allUnambiguousListValueCache.TryGetValue(name, out val);
|
||||
return val;
|
||||
}
|
||||
|
||||
Dictionary<string, Runtime.ListDefinition> _lists;
|
||||
Dictionary<string, ListValue> _allUnambiguousListValueCache;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ec881a45ed342471fb87e412417b6343
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,508 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ink.Runtime
|
||||
{
|
||||
public class NativeFunctionCall : Runtime.Object
|
||||
{
|
||||
public const string Add = "+";
|
||||
public const string Subtract = "-";
|
||||
public const string Divide = "/";
|
||||
public const string Multiply = "*";
|
||||
public const string Mod = "%";
|
||||
public const string Negate = "_"; // distinguish from "-" for subtraction
|
||||
|
||||
public const string Equal = "==";
|
||||
public const string Greater = ">";
|
||||
public const string Less = "<";
|
||||
public const string GreaterThanOrEquals = ">=";
|
||||
public const string LessThanOrEquals = "<=";
|
||||
public const string NotEquals = "!=";
|
||||
public const string Not = "!";
|
||||
|
||||
|
||||
|
||||
public const string And = "&&";
|
||||
public const string Or = "||";
|
||||
|
||||
public const string Min = "MIN";
|
||||
public const string Max = "MAX";
|
||||
|
||||
public const string Pow = "POW";
|
||||
public const string Floor = "FLOOR";
|
||||
public const string Ceiling = "CEILING";
|
||||
public const string Int = "INT";
|
||||
public const string Float = "FLOAT";
|
||||
|
||||
public const string Has = "?";
|
||||
public const string Hasnt = "!?";
|
||||
public const string Intersect = "^";
|
||||
|
||||
public const string ListMin = "LIST_MIN";
|
||||
public const string ListMax = "LIST_MAX";
|
||||
public const string All = "LIST_ALL";
|
||||
public const string Count = "LIST_COUNT";
|
||||
public const string ValueOfList = "LIST_VALUE";
|
||||
public const string Invert = "LIST_INVERT";
|
||||
|
||||
public static NativeFunctionCall CallWithName(string functionName)
|
||||
{
|
||||
return new NativeFunctionCall (functionName);
|
||||
}
|
||||
|
||||
public static bool CallExistsWithName(string functionName)
|
||||
{
|
||||
GenerateNativeFunctionsIfNecessary ();
|
||||
return _nativeFunctions.ContainsKey (functionName);
|
||||
}
|
||||
|
||||
public string name {
|
||||
get {
|
||||
return _name;
|
||||
}
|
||||
protected set {
|
||||
_name = value;
|
||||
if( !_isPrototype )
|
||||
_prototype = _nativeFunctions [_name];
|
||||
}
|
||||
}
|
||||
string _name;
|
||||
|
||||
public int numberOfParameters {
|
||||
get {
|
||||
if (_prototype) {
|
||||
return _prototype.numberOfParameters;
|
||||
} else {
|
||||
return _numberOfParameters;
|
||||
}
|
||||
}
|
||||
protected set {
|
||||
_numberOfParameters = value;
|
||||
}
|
||||
}
|
||||
|
||||
int _numberOfParameters;
|
||||
|
||||
public Runtime.Object Call(List<Runtime.Object> parameters)
|
||||
{
|
||||
if (_prototype) {
|
||||
return _prototype.Call(parameters);
|
||||
}
|
||||
|
||||
if (numberOfParameters != parameters.Count) {
|
||||
throw new System.Exception ("Unexpected number of parameters");
|
||||
}
|
||||
|
||||
bool hasList = false;
|
||||
foreach (var p in parameters) {
|
||||
if (p is Void)
|
||||
throw new StoryException ("Attempting to perform operation on a void value. Did you forget to 'return' a value from a function you called here?");
|
||||
if (p is ListValue)
|
||||
hasList = true;
|
||||
}
|
||||
|
||||
// Binary operations on lists are treated outside of the standard coerscion rules
|
||||
if( parameters.Count == 2 && hasList )
|
||||
return CallBinaryListOperation (parameters);
|
||||
|
||||
var coercedParams = CoerceValuesToSingleType (parameters);
|
||||
ValueType coercedType = coercedParams[0].valueType;
|
||||
|
||||
if (coercedType == ValueType.Int) {
|
||||
return Call<int> (coercedParams);
|
||||
} else if (coercedType == ValueType.Float) {
|
||||
return Call<float> (coercedParams);
|
||||
} else if (coercedType == ValueType.String) {
|
||||
return Call<string> (coercedParams);
|
||||
} else if (coercedType == ValueType.DivertTarget) {
|
||||
return Call<Path> (coercedParams);
|
||||
} else if (coercedType == ValueType.List) {
|
||||
return Call<InkList> (coercedParams);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Value Call<T>(List<Value> parametersOfSingleType)
|
||||
{
|
||||
Value param1 = (Value) parametersOfSingleType [0];
|
||||
ValueType valType = param1.valueType;
|
||||
|
||||
var val1 = (Value<T>)param1;
|
||||
|
||||
int paramCount = parametersOfSingleType.Count;
|
||||
|
||||
if (paramCount == 2 || paramCount == 1) {
|
||||
|
||||
object opForTypeObj = null;
|
||||
if (!_operationFuncs.TryGetValue (valType, out opForTypeObj)) {
|
||||
throw new StoryException ("Cannot perform operation '"+this.name+"' on "+valType);
|
||||
}
|
||||
|
||||
// Binary
|
||||
if (paramCount == 2) {
|
||||
Value param2 = (Value) parametersOfSingleType [1];
|
||||
|
||||
var val2 = (Value<T>)param2;
|
||||
|
||||
var opForType = (BinaryOp<T>)opForTypeObj;
|
||||
|
||||
// Return value unknown until it's evaluated
|
||||
object resultVal = opForType (val1.value, val2.value);
|
||||
|
||||
return Value.Create (resultVal);
|
||||
}
|
||||
|
||||
// Unary
|
||||
else {
|
||||
|
||||
var opForType = (UnaryOp<T>)opForTypeObj;
|
||||
|
||||
var resultVal = opForType (val1.value);
|
||||
|
||||
return Value.Create (resultVal);
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
throw new System.Exception ("Unexpected number of parameters to NativeFunctionCall: " + parametersOfSingleType.Count);
|
||||
}
|
||||
}
|
||||
|
||||
Value CallBinaryListOperation (List<Runtime.Object> parameters)
|
||||
{
|
||||
// List-Int addition/subtraction returns a List (e.g. "alpha" + 1 = "beta")
|
||||
if ((name == "+" || name == "-") && parameters [0] is ListValue && parameters [1] is IntValue)
|
||||
return CallListIncrementOperation (parameters);
|
||||
|
||||
var v1 = parameters [0] as Value;
|
||||
var v2 = parameters [1] as Value;
|
||||
|
||||
// And/or with any other type requires coerscion to bool (int)
|
||||
if ((name == "&&" || name == "||") && (v1.valueType != ValueType.List || v2.valueType != ValueType.List)) {
|
||||
var op = _operationFuncs [ValueType.Int] as BinaryOp<int>;
|
||||
var result = (bool)op (v1.isTruthy ? 1 : 0, v2.isTruthy ? 1 : 0);
|
||||
return new BoolValue (result);
|
||||
}
|
||||
|
||||
// Normal (list • list) operation
|
||||
if (v1.valueType == ValueType.List && v2.valueType == ValueType.List)
|
||||
return Call<InkList> (new List<Value> { v1, v2 });
|
||||
|
||||
throw new StoryException ("Can not call use '" + name + "' operation on " + v1.valueType + " and " + v2.valueType);
|
||||
}
|
||||
|
||||
Value CallListIncrementOperation (List<Runtime.Object> listIntParams)
|
||||
{
|
||||
var listVal = (ListValue)listIntParams [0];
|
||||
var intVal = (IntValue)listIntParams [1];
|
||||
|
||||
|
||||
var resultRawList = new InkList ();
|
||||
|
||||
foreach (var listItemWithValue in listVal.value) {
|
||||
var listItem = listItemWithValue.Key;
|
||||
var listItemValue = listItemWithValue.Value;
|
||||
|
||||
// Find + or - operation
|
||||
var intOp = (BinaryOp<int>)_operationFuncs [ValueType.Int];
|
||||
|
||||
// Return value unknown until it's evaluated
|
||||
int targetInt = (int) intOp (listItemValue, intVal.value);
|
||||
|
||||
// Find this item's origin (linear search should be ok, should be short haha)
|
||||
ListDefinition itemOrigin = null;
|
||||
foreach (var origin in listVal.value.origins) {
|
||||
if (origin.name == listItem.originName) {
|
||||
itemOrigin = origin;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (itemOrigin != null) {
|
||||
InkListItem incrementedItem;
|
||||
if (itemOrigin.TryGetItemWithValue (targetInt, out incrementedItem))
|
||||
resultRawList.Add (incrementedItem, targetInt);
|
||||
}
|
||||
}
|
||||
|
||||
return new ListValue (resultRawList);
|
||||
}
|
||||
|
||||
List<Value> CoerceValuesToSingleType(List<Runtime.Object> parametersIn)
|
||||
{
|
||||
ValueType valType = ValueType.Int;
|
||||
|
||||
ListValue specialCaseList = null;
|
||||
|
||||
// Find out what the output type is
|
||||
// "higher level" types infect both so that binary operations
|
||||
// use the same type on both sides. e.g. binary operation of
|
||||
// int and float causes the int to be casted to a float.
|
||||
foreach (var obj in parametersIn) {
|
||||
var val = (Value)obj;
|
||||
if (val.valueType > valType) {
|
||||
valType = val.valueType;
|
||||
}
|
||||
|
||||
if (val.valueType == ValueType.List) {
|
||||
specialCaseList = val as ListValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Coerce to this chosen type
|
||||
var parametersOut = new List<Value> ();
|
||||
|
||||
// Special case: Coercing to Ints to Lists
|
||||
// We have to do it early when we have both parameters
|
||||
// to hand - so that we can make use of the List's origin
|
||||
if (valType == ValueType.List) {
|
||||
|
||||
foreach (Value val in parametersIn) {
|
||||
if (val.valueType == ValueType.List) {
|
||||
parametersOut.Add (val);
|
||||
} else if (val.valueType == ValueType.Int) {
|
||||
int intVal = (int)val.valueObject;
|
||||
var list = specialCaseList.value.originOfMaxItem;
|
||||
|
||||
InkListItem item;
|
||||
if (list.TryGetItemWithValue (intVal, out item)) {
|
||||
var castedValue = new ListValue (item, intVal);
|
||||
parametersOut.Add (castedValue);
|
||||
} else
|
||||
throw new StoryException ("Could not find List item with the value " + intVal + " in " + list.name);
|
||||
} else
|
||||
throw new StoryException ("Cannot mix Lists and " + val.valueType + " values in this operation");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Normal Coercing (with standard casting)
|
||||
else {
|
||||
foreach (Value val in parametersIn) {
|
||||
var castedValue = val.Cast (valType);
|
||||
parametersOut.Add (castedValue);
|
||||
}
|
||||
}
|
||||
|
||||
return parametersOut;
|
||||
}
|
||||
|
||||
public NativeFunctionCall(string name)
|
||||
{
|
||||
GenerateNativeFunctionsIfNecessary ();
|
||||
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
// Require default constructor for serialisation
|
||||
public NativeFunctionCall() {
|
||||
GenerateNativeFunctionsIfNecessary ();
|
||||
}
|
||||
|
||||
// Only called internally to generate prototypes
|
||||
NativeFunctionCall (string name, int numberOfParameters)
|
||||
{
|
||||
_isPrototype = true;
|
||||
this.name = name;
|
||||
this.numberOfParameters = numberOfParameters;
|
||||
}
|
||||
|
||||
// For defining operations that do nothing to the specific type
|
||||
// (but are still supported), such as floor/ceil on int and float
|
||||
// cast on float.
|
||||
static object Identity<T>(T t) {
|
||||
return t;
|
||||
}
|
||||
|
||||
static void GenerateNativeFunctionsIfNecessary()
|
||||
{
|
||||
if (_nativeFunctions == null) {
|
||||
_nativeFunctions = new Dictionary<string, NativeFunctionCall> ();
|
||||
|
||||
// Why no bool operations?
|
||||
// Before evaluation, all bools are coerced to ints in
|
||||
// CoerceValuesToSingleType (see default value for valType at top).
|
||||
// So, no operations are ever directly done in bools themselves.
|
||||
// This also means that 1 == true works, since true is always converted
|
||||
// to 1 first.
|
||||
// However, many operations return a "native" bool (equals, etc).
|
||||
|
||||
// Int operations
|
||||
AddIntBinaryOp(Add, (x, y) => x + y);
|
||||
AddIntBinaryOp(Subtract, (x, y) => x - y);
|
||||
AddIntBinaryOp(Multiply, (x, y) => x * y);
|
||||
AddIntBinaryOp(Divide, (x, y) => x / y);
|
||||
AddIntBinaryOp(Mod, (x, y) => x % y);
|
||||
AddIntUnaryOp (Negate, x => -x);
|
||||
|
||||
AddIntBinaryOp(Equal, (x, y) => x == y);
|
||||
AddIntBinaryOp(Greater, (x, y) => x > y);
|
||||
AddIntBinaryOp(Less, (x, y) => x < y);
|
||||
AddIntBinaryOp(GreaterThanOrEquals, (x, y) => x >= y);
|
||||
AddIntBinaryOp(LessThanOrEquals, (x, y) => x <= y);
|
||||
AddIntBinaryOp(NotEquals, (x, y) => x != y);
|
||||
AddIntUnaryOp (Not, x => x == 0);
|
||||
|
||||
AddIntBinaryOp(And, (x, y) => x != 0 && y != 0);
|
||||
AddIntBinaryOp(Or, (x, y) => x != 0 || y != 0);
|
||||
|
||||
AddIntBinaryOp(Max, (x, y) => Math.Max(x, y));
|
||||
AddIntBinaryOp(Min, (x, y) => Math.Min(x, y));
|
||||
|
||||
// Have to cast to float since you could do POW(2, -1)
|
||||
AddIntBinaryOp (Pow, (x, y) => (float) Math.Pow(x, y));
|
||||
AddIntUnaryOp(Floor, Identity);
|
||||
AddIntUnaryOp(Ceiling, Identity);
|
||||
AddIntUnaryOp(Int, Identity);
|
||||
AddIntUnaryOp (Float, x => (float)x);
|
||||
|
||||
// Float operations
|
||||
AddFloatBinaryOp(Add, (x, y) => x + y);
|
||||
AddFloatBinaryOp(Subtract, (x, y) => x - y);
|
||||
AddFloatBinaryOp(Multiply, (x, y) => x * y);
|
||||
AddFloatBinaryOp(Divide, (x, y) => x / y);
|
||||
AddFloatBinaryOp(Mod, (x, y) => x % y); // TODO: Is this the operation we want for floats?
|
||||
AddFloatUnaryOp (Negate, x => -x);
|
||||
|
||||
AddFloatBinaryOp(Equal, (x, y) => x == y);
|
||||
AddFloatBinaryOp(Greater, (x, y) => x > y);
|
||||
AddFloatBinaryOp(Less, (x, y) => x < y);
|
||||
AddFloatBinaryOp(GreaterThanOrEquals, (x, y) => x >= y);
|
||||
AddFloatBinaryOp(LessThanOrEquals, (x, y) => x <= y);
|
||||
AddFloatBinaryOp(NotEquals, (x, y) => x != y);
|
||||
AddFloatUnaryOp (Not, x => (x == 0.0f));
|
||||
|
||||
AddFloatBinaryOp(And, (x, y) => x != 0.0f && y != 0.0f);
|
||||
AddFloatBinaryOp(Or, (x, y) => x != 0.0f || y != 0.0f);
|
||||
|
||||
AddFloatBinaryOp(Max, (x, y) => Math.Max(x, y));
|
||||
AddFloatBinaryOp(Min, (x, y) => Math.Min(x, y));
|
||||
|
||||
AddFloatBinaryOp (Pow, (x, y) => (float)Math.Pow(x, y));
|
||||
AddFloatUnaryOp(Floor, x => (float)Math.Floor(x));
|
||||
AddFloatUnaryOp(Ceiling, x => (float)Math.Ceiling(x));
|
||||
AddFloatUnaryOp(Int, x => (int)x);
|
||||
AddFloatUnaryOp(Float, Identity);
|
||||
|
||||
// String operations
|
||||
AddStringBinaryOp(Add, (x, y) => x + y); // concat
|
||||
AddStringBinaryOp(Equal, (x, y) => x.Equals(y));
|
||||
AddStringBinaryOp (NotEquals, (x, y) => !x.Equals (y));
|
||||
AddStringBinaryOp (Has, (x, y) => x.Contains(y));
|
||||
AddStringBinaryOp (Hasnt, (x, y) => !x.Contains(y));
|
||||
|
||||
// List operations
|
||||
AddListBinaryOp (Add, (x, y) => x.Union (y));
|
||||
AddListBinaryOp (Subtract, (x, y) => x.Without(y));
|
||||
AddListBinaryOp (Has, (x, y) => x.Contains (y));
|
||||
AddListBinaryOp (Hasnt, (x, y) => !x.Contains (y));
|
||||
AddListBinaryOp (Intersect, (x, y) => x.Intersect (y));
|
||||
|
||||
AddListBinaryOp (Equal, (x, y) => x.Equals(y));
|
||||
AddListBinaryOp (Greater, (x, y) => x.GreaterThan(y));
|
||||
AddListBinaryOp (Less, (x, y) => x.LessThan(y));
|
||||
AddListBinaryOp (GreaterThanOrEquals, (x, y) => x.GreaterThanOrEquals(y));
|
||||
AddListBinaryOp (LessThanOrEquals, (x, y) => x.LessThanOrEquals(y));
|
||||
AddListBinaryOp (NotEquals, (x, y) => !x.Equals(y));
|
||||
|
||||
AddListBinaryOp (And, (x, y) => x.Count > 0 && y.Count > 0);
|
||||
AddListBinaryOp (Or, (x, y) => x.Count > 0 || y.Count > 0);
|
||||
|
||||
AddListUnaryOp (Not, x => x.Count == 0 ? (int)1 : (int)0);
|
||||
|
||||
// Placeholders to ensure that these special case functions can exist,
|
||||
// since these function is never actually run, and is special cased in Call
|
||||
AddListUnaryOp (Invert, x => x.inverse);
|
||||
AddListUnaryOp (All, x => x.all);
|
||||
AddListUnaryOp (ListMin, (x) => x.MinAsList());
|
||||
AddListUnaryOp (ListMax, (x) => x.MaxAsList());
|
||||
AddListUnaryOp (Count, (x) => x.Count);
|
||||
AddListUnaryOp (ValueOfList, (x) => x.maxItem.Value);
|
||||
|
||||
// Special case: The only operations you can do on divert target values
|
||||
BinaryOp<Path> divertTargetsEqual = (Path d1, Path d2) => {
|
||||
return d1.Equals (d2);
|
||||
};
|
||||
BinaryOp<Path> divertTargetsNotEqual = (Path d1, Path d2) => {
|
||||
return !d1.Equals (d2);
|
||||
};
|
||||
AddOpToNativeFunc (Equal, 2, ValueType.DivertTarget, divertTargetsEqual);
|
||||
AddOpToNativeFunc (NotEquals, 2, ValueType.DivertTarget, divertTargetsNotEqual);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void AddOpFuncForType(ValueType valType, object op)
|
||||
{
|
||||
if (_operationFuncs == null) {
|
||||
_operationFuncs = new Dictionary<ValueType, object> ();
|
||||
}
|
||||
|
||||
_operationFuncs [valType] = op;
|
||||
}
|
||||
|
||||
static void AddOpToNativeFunc(string name, int args, ValueType valType, object op)
|
||||
{
|
||||
NativeFunctionCall nativeFunc = null;
|
||||
if (!_nativeFunctions.TryGetValue (name, out nativeFunc)) {
|
||||
nativeFunc = new NativeFunctionCall (name, args);
|
||||
_nativeFunctions [name] = nativeFunc;
|
||||
}
|
||||
|
||||
nativeFunc.AddOpFuncForType (valType, op);
|
||||
}
|
||||
|
||||
static void AddIntBinaryOp(string name, BinaryOp<int> op)
|
||||
{
|
||||
AddOpToNativeFunc (name, 2, ValueType.Int, op);
|
||||
}
|
||||
|
||||
static void AddIntUnaryOp(string name, UnaryOp<int> op)
|
||||
{
|
||||
AddOpToNativeFunc (name, 1, ValueType.Int, op);
|
||||
}
|
||||
|
||||
static void AddFloatBinaryOp(string name, BinaryOp<float> op)
|
||||
{
|
||||
AddOpToNativeFunc (name, 2, ValueType.Float, op);
|
||||
}
|
||||
|
||||
static void AddStringBinaryOp(string name, BinaryOp<string> op)
|
||||
{
|
||||
AddOpToNativeFunc (name, 2, ValueType.String, op);
|
||||
}
|
||||
|
||||
static void AddListBinaryOp (string name, BinaryOp<InkList> op)
|
||||
{
|
||||
AddOpToNativeFunc (name, 2, ValueType.List, op);
|
||||
}
|
||||
|
||||
static void AddListUnaryOp (string name, UnaryOp<InkList> op)
|
||||
{
|
||||
AddOpToNativeFunc (name, 1, ValueType.List, op);
|
||||
}
|
||||
|
||||
static void AddFloatUnaryOp(string name, UnaryOp<float> op)
|
||||
{
|
||||
AddOpToNativeFunc (name, 1, ValueType.Float, op);
|
||||
}
|
||||
|
||||
public override string ToString ()
|
||||
{
|
||||
return "Native '" + name + "'";
|
||||
}
|
||||
|
||||
delegate object BinaryOp<T>(T left, T right);
|
||||
delegate object UnaryOp<T>(T val);
|
||||
|
||||
NativeFunctionCall _prototype;
|
||||
bool _isPrototype;
|
||||
|
||||
// Operations for each data type, for a single operation (e.g. "+")
|
||||
Dictionary<ValueType, object> _operationFuncs;
|
||||
|
||||
static Dictionary<string, NativeFunctionCall> _nativeFunctions;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b713d1356fec8458380a308bdc2e8b8f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,250 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Ink.Runtime
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for all ink runtime content.
|
||||
/// </summary>
|
||||
public /* TODO: abstract */ class Object
|
||||
{
|
||||
/// <summary>
|
||||
/// Runtime.Objects can be included in the main Story as a hierarchy.
|
||||
/// Usually parents are Container objects. (TODO: Always?)
|
||||
/// </summary>
|
||||
/// <value>The parent.</value>
|
||||
public Runtime.Object parent { get; set; }
|
||||
|
||||
public Runtime.DebugMetadata debugMetadata {
|
||||
get {
|
||||
if (_debugMetadata == null) {
|
||||
if (parent) {
|
||||
return parent.debugMetadata;
|
||||
}
|
||||
}
|
||||
|
||||
return _debugMetadata;
|
||||
}
|
||||
|
||||
set {
|
||||
_debugMetadata = value;
|
||||
}
|
||||
}
|
||||
|
||||
public Runtime.DebugMetadata ownDebugMetadata {
|
||||
get {
|
||||
return _debugMetadata;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Come up with some clever solution for not having
|
||||
// to have debug metadata on the object itself, perhaps
|
||||
// for serialisation purposes at least.
|
||||
DebugMetadata _debugMetadata;
|
||||
|
||||
public int? DebugLineNumberOfPath(Path path)
|
||||
{
|
||||
if (path == null)
|
||||
return null;
|
||||
|
||||
// Try to get a line number from debug metadata
|
||||
var root = this.rootContentContainer;
|
||||
if (root) {
|
||||
Runtime.Object targetContent = root.ContentAtPath (path).obj;
|
||||
if (targetContent) {
|
||||
var dm = targetContent.debugMetadata;
|
||||
if (dm != null) {
|
||||
return dm.startLineNumber;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Path path
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_path == null) {
|
||||
|
||||
if (parent == null) {
|
||||
_path = new Path ();
|
||||
} else {
|
||||
// Maintain a Stack so that the order of the components
|
||||
// is reversed when they're added to the Path.
|
||||
// We're iterating up the hierarchy from the leaves/children to the root.
|
||||
var comps = new Stack<Path.Component> ();
|
||||
|
||||
var child = this;
|
||||
Container container = child.parent as Container;
|
||||
|
||||
while (container) {
|
||||
|
||||
var namedChild = child as INamedContent;
|
||||
if (namedChild != null && namedChild.hasValidName) {
|
||||
comps.Push (new Path.Component (namedChild.name));
|
||||
} else {
|
||||
comps.Push (new Path.Component (container.content.IndexOf(child)));
|
||||
}
|
||||
|
||||
child = container;
|
||||
container = container.parent as Container;
|
||||
}
|
||||
|
||||
_path = new Path (comps);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return _path;
|
||||
}
|
||||
}
|
||||
Path _path;
|
||||
|
||||
public SearchResult ResolvePath(Path path)
|
||||
{
|
||||
if (path.isRelative) {
|
||||
|
||||
Container nearestContainer = this as Container;
|
||||
if (!nearestContainer) {
|
||||
Debug.Assert (this.parent != null, "Can't resolve relative path because we don't have a parent");
|
||||
nearestContainer = this.parent as Container;
|
||||
Debug.Assert (nearestContainer != null, "Expected parent to be a container");
|
||||
Debug.Assert (path.GetComponent(0).isParent);
|
||||
path = path.tail;
|
||||
}
|
||||
|
||||
return nearestContainer.ContentAtPath (path);
|
||||
} else {
|
||||
return this.rootContentContainer.ContentAtPath (path);
|
||||
}
|
||||
}
|
||||
|
||||
public Path ConvertPathToRelative(Path globalPath)
|
||||
{
|
||||
// 1. Find last shared ancestor
|
||||
// 2. Drill up using ".." style (actually represented as "^")
|
||||
// 3. Re-build downward chain from common ancestor
|
||||
|
||||
var ownPath = this.path;
|
||||
|
||||
int minPathLength = Math.Min (globalPath.length, ownPath.length);
|
||||
int lastSharedPathCompIndex = -1;
|
||||
|
||||
for (int i = 0; i < minPathLength; ++i) {
|
||||
var ownComp = ownPath.GetComponent(i);
|
||||
var otherComp = globalPath.GetComponent(i);
|
||||
|
||||
if (ownComp.Equals (otherComp)) {
|
||||
lastSharedPathCompIndex = i;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// No shared path components, so just use global path
|
||||
if (lastSharedPathCompIndex == -1)
|
||||
return globalPath;
|
||||
|
||||
int numUpwardsMoves = (ownPath.length-1) - lastSharedPathCompIndex;
|
||||
|
||||
var newPathComps = new List<Path.Component> ();
|
||||
|
||||
for(int up=0; up<numUpwardsMoves; ++up)
|
||||
newPathComps.Add (Path.Component.ToParent ());
|
||||
|
||||
for (int down = lastSharedPathCompIndex + 1; down < globalPath.length; ++down)
|
||||
newPathComps.Add (globalPath.GetComponent(down));
|
||||
|
||||
var relativePath = new Path (newPathComps, relative:true);
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
// Find most compact representation for a path, whether relative or global
|
||||
public string CompactPathString(Path otherPath)
|
||||
{
|
||||
string globalPathStr = null;
|
||||
string relativePathStr = null;
|
||||
if (otherPath.isRelative) {
|
||||
relativePathStr = otherPath.componentsString;
|
||||
globalPathStr = this.path.PathByAppendingPath(otherPath).componentsString;
|
||||
} else {
|
||||
var relativePath = ConvertPathToRelative (otherPath);
|
||||
relativePathStr = relativePath.componentsString;
|
||||
globalPathStr = otherPath.componentsString;
|
||||
}
|
||||
|
||||
if (relativePathStr.Length < globalPathStr.Length)
|
||||
return relativePathStr;
|
||||
else
|
||||
return globalPathStr;
|
||||
}
|
||||
|
||||
public Container rootContentContainer
|
||||
{
|
||||
get
|
||||
{
|
||||
Runtime.Object ancestor = this;
|
||||
while (ancestor.parent) {
|
||||
ancestor = ancestor.parent;
|
||||
}
|
||||
return ancestor as Container;
|
||||
}
|
||||
}
|
||||
|
||||
public Object ()
|
||||
{
|
||||
}
|
||||
|
||||
public virtual Object Copy()
|
||||
{
|
||||
throw new System.NotImplementedException (GetType ().Name + " doesn't support copying");
|
||||
}
|
||||
|
||||
public void SetChild<T>(ref T obj, T value) where T : Runtime.Object
|
||||
{
|
||||
if (obj)
|
||||
obj.parent = null;
|
||||
|
||||
obj = value;
|
||||
|
||||
if( obj )
|
||||
obj.parent = this;
|
||||
}
|
||||
|
||||
/// Allow implicit conversion to bool so you don't have to do:
|
||||
/// if( myObj != null ) ...
|
||||
public static implicit operator bool (Object obj)
|
||||
{
|
||||
var isNull = object.ReferenceEquals (obj, null);
|
||||
return !isNull;
|
||||
}
|
||||
|
||||
/// Required for implicit bool comparison
|
||||
public static bool operator ==(Object a, Object b)
|
||||
{
|
||||
return object.ReferenceEquals (a, b);
|
||||
}
|
||||
|
||||
/// Required for implicit bool comparison
|
||||
public static bool operator !=(Object a, Object b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
/// Required for implicit bool comparison
|
||||
public override bool Equals (object obj)
|
||||
{
|
||||
return object.ReferenceEquals (obj, this);
|
||||
}
|
||||
|
||||
/// Required for implicit bool comparison
|
||||
public override int GetHashCode ()
|
||||
{
|
||||
return base.GetHashCode ();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 20b56f60ae0f44147bccbae6ef3d0054
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
277
Unity/Alternate Genre Jam/Assets/Ink/InkLibs/InkRuntime/Path.cs
Normal file
277
Unity/Alternate Genre Jam/Assets/Ink/InkLibs/InkRuntime/Path.cs
Normal file
@@ -0,0 +1,277 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Diagnostics;
|
||||
using Ink.Runtime;
|
||||
|
||||
namespace Ink.Runtime
|
||||
{
|
||||
public class Path : IEquatable<Path>
|
||||
{
|
||||
static string parentId = "^";
|
||||
|
||||
// Immutable Component
|
||||
public class Component : IEquatable<Component>
|
||||
{
|
||||
public int index { get; private set; }
|
||||
public string name { get; private set; }
|
||||
public bool isIndex { get { return index >= 0; } }
|
||||
public bool isParent {
|
||||
get {
|
||||
return name == Path.parentId;
|
||||
}
|
||||
}
|
||||
|
||||
public Component(int index)
|
||||
{
|
||||
Debug.Assert(index >= 0);
|
||||
this.index = index;
|
||||
this.name = null;
|
||||
}
|
||||
|
||||
public Component(string name)
|
||||
{
|
||||
Debug.Assert(name != null && name.Length > 0);
|
||||
this.name = name;
|
||||
this.index = -1;
|
||||
}
|
||||
|
||||
public static Component ToParent()
|
||||
{
|
||||
return new Component (parentId);
|
||||
}
|
||||
|
||||
public override string ToString ()
|
||||
{
|
||||
if (isIndex) {
|
||||
return index.ToString ();
|
||||
} else {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Equals (object obj)
|
||||
{
|
||||
return Equals (obj as Component);
|
||||
}
|
||||
|
||||
public bool Equals(Component otherComp)
|
||||
{
|
||||
if (otherComp != null && otherComp.isIndex == this.isIndex) {
|
||||
if (isIndex) {
|
||||
return index == otherComp.index;
|
||||
} else {
|
||||
return name == otherComp.name;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override int GetHashCode ()
|
||||
{
|
||||
if (isIndex)
|
||||
return this.index;
|
||||
else
|
||||
return this.name.GetHashCode ();
|
||||
}
|
||||
}
|
||||
|
||||
public Component GetComponent(int index)
|
||||
{
|
||||
return _components[index];
|
||||
}
|
||||
|
||||
public bool isRelative { get; private set; }
|
||||
|
||||
public Component head
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_components.Count > 0) {
|
||||
return _components.First ();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Path tail
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_components.Count >= 2) {
|
||||
List<Component> tailComps = _components.GetRange (1, _components.Count - 1);
|
||||
return new Path(tailComps);
|
||||
}
|
||||
|
||||
else {
|
||||
return Path.self;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public int length { get { return _components.Count; } }
|
||||
|
||||
public Component lastComponent
|
||||
{
|
||||
get
|
||||
{
|
||||
var lastComponentIdx = _components.Count-1;
|
||||
if( lastComponentIdx >= 0 )
|
||||
return _components[lastComponentIdx];
|
||||
else
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool containsNamedComponent {
|
||||
get {
|
||||
foreach(var comp in _components) {
|
||||
if( !comp.isIndex ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Path()
|
||||
{
|
||||
_components = new List<Component> ();
|
||||
}
|
||||
|
||||
public Path(Component head, Path tail) : this()
|
||||
{
|
||||
_components.Add (head);
|
||||
_components.AddRange (tail._components);
|
||||
}
|
||||
|
||||
public Path(IEnumerable<Component> components, bool relative = false) : this()
|
||||
{
|
||||
this._components.AddRange (components);
|
||||
this.isRelative = relative;
|
||||
}
|
||||
|
||||
public Path(string componentsString) : this()
|
||||
{
|
||||
this.componentsString = componentsString;
|
||||
}
|
||||
|
||||
public static Path self {
|
||||
get {
|
||||
var path = new Path ();
|
||||
path.isRelative = true;
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
public Path PathByAppendingPath(Path pathToAppend)
|
||||
{
|
||||
Path p = new Path ();
|
||||
|
||||
int upwardMoves = 0;
|
||||
for (int i = 0; i < pathToAppend._components.Count; ++i) {
|
||||
if (pathToAppend._components [i].isParent) {
|
||||
upwardMoves++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < this._components.Count - upwardMoves; ++i) {
|
||||
p._components.Add (this._components [i]);
|
||||
}
|
||||
|
||||
for(int i=upwardMoves; i<pathToAppend._components.Count; ++i) {
|
||||
p._components.Add (pathToAppend._components [i]);
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
public Path PathByAppendingComponent (Component c)
|
||||
{
|
||||
Path p = new Path ();
|
||||
p._components.AddRange (_components);
|
||||
p._components.Add (c);
|
||||
return p;
|
||||
}
|
||||
|
||||
public string componentsString {
|
||||
get {
|
||||
if( _componentsString == null ) {
|
||||
_componentsString = StringExt.Join (".", _components);
|
||||
if (isRelative) _componentsString = "." + _componentsString;
|
||||
}
|
||||
return _componentsString;
|
||||
}
|
||||
private set {
|
||||
_components.Clear ();
|
||||
|
||||
_componentsString = value;
|
||||
|
||||
// Empty path, empty components
|
||||
// (path is to root, like "/" in file system)
|
||||
if (string.IsNullOrEmpty(_componentsString))
|
||||
return;
|
||||
|
||||
// When components start with ".", it indicates a relative path, e.g.
|
||||
// .^.^.hello.5
|
||||
// is equivalent to file system style path:
|
||||
// ../../hello/5
|
||||
if (_componentsString [0] == '.') {
|
||||
this.isRelative = true;
|
||||
_componentsString = _componentsString.Substring (1);
|
||||
} else {
|
||||
this.isRelative = false;
|
||||
}
|
||||
|
||||
var componentStrings = _componentsString.Split('.');
|
||||
foreach (var str in componentStrings) {
|
||||
int index;
|
||||
if (int.TryParse (str , out index)) {
|
||||
_components.Add (new Component (index));
|
||||
} else {
|
||||
_components.Add (new Component (str));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
string _componentsString;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return componentsString;
|
||||
}
|
||||
|
||||
public override bool Equals (object obj)
|
||||
{
|
||||
return Equals (obj as Path);
|
||||
}
|
||||
|
||||
public bool Equals (Path otherPath)
|
||||
{
|
||||
if (otherPath == null)
|
||||
return false;
|
||||
|
||||
if (otherPath._components.Count != this._components.Count)
|
||||
return false;
|
||||
|
||||
if (otherPath.isRelative != this.isRelative)
|
||||
return false;
|
||||
|
||||
return otherPath._components.SequenceEqual (this._components);
|
||||
}
|
||||
|
||||
public override int GetHashCode ()
|
||||
{
|
||||
// TODO: Better way to make a hash code!
|
||||
return this.ToString ().GetHashCode ();
|
||||
}
|
||||
|
||||
List<Component> _components;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 62ce05aceee3c42cc90313aec848e171
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,70 @@
|
||||
using Ink.Runtime;
|
||||
|
||||
namespace Ink.Runtime
|
||||
{
|
||||
/// <summary>
|
||||
/// Internal structure used to point to a particular / current point in the story.
|
||||
/// Where Path is a set of components that make content fully addressable, this is
|
||||
/// a reference to the current container, and the index of the current piece of
|
||||
/// content within that container. This scheme makes it as fast and efficient as
|
||||
/// possible to increment the pointer (move the story forwards) in a way that's as
|
||||
/// native to the internal engine as possible.
|
||||
/// </summary>
|
||||
public struct Pointer
|
||||
{
|
||||
public Container container;
|
||||
public int index;
|
||||
|
||||
public Pointer (Container container, int index)
|
||||
{
|
||||
this.container = container;
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
public Runtime.Object Resolve ()
|
||||
{
|
||||
if (index < 0) return container;
|
||||
if (container == null) return null;
|
||||
if (container.content.Count == 0) return container;
|
||||
if (index >= container.content.Count) return null;
|
||||
return container.content [index];
|
||||
|
||||
}
|
||||
|
||||
public bool isNull {
|
||||
get {
|
||||
return container == null;
|
||||
}
|
||||
}
|
||||
|
||||
public Path path {
|
||||
get {
|
||||
if( isNull ) return null;
|
||||
|
||||
if (index >= 0)
|
||||
return container.path.PathByAppendingComponent (new Path.Component(index));
|
||||
else
|
||||
return container.path;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString ()
|
||||
{
|
||||
if (container == null)
|
||||
return "Ink Pointer (null)";
|
||||
|
||||
return "Ink Pointer -> " + container.path.ToString () + " -- index " + index;
|
||||
}
|
||||
|
||||
public static Pointer StartOf (Container container)
|
||||
{
|
||||
return new Pointer {
|
||||
container = container,
|
||||
index = 0
|
||||
};
|
||||
}
|
||||
|
||||
public static Pointer Null = new Pointer { container = null, index = -1 };
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 802f84ed94f0e4f518e4be47877e9a33
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,394 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ink.Runtime
|
||||
{
|
||||
/// <summary>
|
||||
/// Simple ink profiler that logs every instruction in the story and counts frequency and timing.
|
||||
/// To use:
|
||||
///
|
||||
/// var profiler = story.StartProfiling(),
|
||||
///
|
||||
/// (play your story for a bit)
|
||||
///
|
||||
/// var reportStr = profiler.Report();
|
||||
///
|
||||
/// story.EndProfiling();
|
||||
///
|
||||
/// </summary>
|
||||
public class Profiler
|
||||
{
|
||||
/// <summary>
|
||||
/// The root node in the hierarchical tree of recorded ink timings.
|
||||
/// </summary>
|
||||
public ProfileNode rootNode {
|
||||
get {
|
||||
return _rootNode;
|
||||
}
|
||||
}
|
||||
|
||||
public Profiler() {
|
||||
_rootNode = new ProfileNode();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a printable report based on the data recording during profiling.
|
||||
/// </summary>
|
||||
public string Report() {
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendFormat("{0} CONTINUES / LINES:\n", _numContinues);
|
||||
sb.AppendFormat("TOTAL TIME: {0}\n", FormatMillisecs(_continueTotal));
|
||||
sb.AppendFormat("SNAPSHOTTING: {0}\n", FormatMillisecs(_snapTotal));
|
||||
sb.AppendFormat("OTHER: {0}\n", FormatMillisecs(_continueTotal - (_stepTotal + _snapTotal)));
|
||||
sb.Append(_rootNode.ToString());
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public void PreContinue() {
|
||||
_continueWatch.Reset();
|
||||
_continueWatch.Start();
|
||||
}
|
||||
|
||||
public void PostContinue() {
|
||||
_continueWatch.Stop();
|
||||
_continueTotal += Millisecs(_continueWatch);
|
||||
_numContinues++;
|
||||
}
|
||||
|
||||
public void PreStep() {
|
||||
_currStepStack = null;
|
||||
_stepWatch.Reset();
|
||||
_stepWatch.Start();
|
||||
}
|
||||
|
||||
public void Step(CallStack callstack)
|
||||
{
|
||||
_stepWatch.Stop();
|
||||
|
||||
var stack = new string[callstack.elements.Count];
|
||||
for(int i=0; i<stack.Length; i++) {
|
||||
string stackElementName = "";
|
||||
if(!callstack.elements[i].currentPointer.isNull) {
|
||||
var objPath = callstack.elements[i].currentPointer.path;
|
||||
|
||||
for(int c=0; c<objPath.length; c++) {
|
||||
var comp = objPath.GetComponent(c);
|
||||
if( !comp.isIndex ) {
|
||||
stackElementName = comp.name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
stack[i] = stackElementName;
|
||||
}
|
||||
|
||||
_currStepStack = stack;
|
||||
|
||||
var currObj = callstack.currentElement.currentPointer.Resolve();
|
||||
|
||||
string stepType = null;
|
||||
var controlCommandStep = currObj as ControlCommand;
|
||||
if( controlCommandStep )
|
||||
stepType = controlCommandStep.commandType.ToString() + " CC";
|
||||
else
|
||||
stepType = currObj.GetType().Name;
|
||||
|
||||
_currStepDetails = new StepDetails {
|
||||
type = stepType,
|
||||
obj = currObj
|
||||
};
|
||||
|
||||
_stepWatch.Start();
|
||||
}
|
||||
|
||||
public void PostStep() {
|
||||
_stepWatch.Stop();
|
||||
|
||||
var duration = Millisecs(_stepWatch);
|
||||
_stepTotal += duration;
|
||||
|
||||
_rootNode.AddSample(_currStepStack, duration);
|
||||
|
||||
_currStepDetails.time = duration;
|
||||
_stepDetails.Add(_currStepDetails);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a printable report specifying the average and maximum times spent
|
||||
/// stepping over different internal ink instruction types.
|
||||
/// This report type is primarily used to profile the ink engine itself rather
|
||||
/// than your own specific ink.
|
||||
/// </summary>
|
||||
public string StepLengthReport()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine("TOTAL: "+_rootNode.totalMillisecs+"ms");
|
||||
|
||||
var averageStepTimes = _stepDetails
|
||||
.GroupBy(s => s.type)
|
||||
.Select(typeToDetails => new KeyValuePair<string, double>(typeToDetails.Key, typeToDetails.Average(d => d.time)))
|
||||
.OrderByDescending(stepTypeToAverage => stepTypeToAverage.Value)
|
||||
.Select(stepTypeToAverage => {
|
||||
var typeName = stepTypeToAverage.Key;
|
||||
var time = stepTypeToAverage.Value;
|
||||
return typeName + ": " + time + "ms";
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
sb.AppendLine("AVERAGE STEP TIMES: "+string.Join(", ", averageStepTimes));
|
||||
|
||||
var accumStepTimes = _stepDetails
|
||||
.GroupBy(s => s.type)
|
||||
.Select(typeToDetails => new KeyValuePair<string, double>(typeToDetails.Key + " (x"+typeToDetails.Count()+")", typeToDetails.Sum(d => d.time)))
|
||||
.OrderByDescending(stepTypeToAccum => stepTypeToAccum.Value)
|
||||
.Select(stepTypeToAccum => {
|
||||
var typeName = stepTypeToAccum.Key;
|
||||
var time = stepTypeToAccum.Value;
|
||||
return typeName + ": " + time;
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
sb.AppendLine("ACCUMULATED STEP TIMES: "+string.Join(", ", accumStepTimes));
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a large log of all the internal instructions that were evaluated while profiling was active.
|
||||
/// Log is in a tab-separated format, for easy loading into a spreadsheet application.
|
||||
/// </summary>
|
||||
public string Megalog()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine("Step type\tDescription\tPath\tTime");
|
||||
|
||||
foreach(var step in _stepDetails) {
|
||||
sb.Append(step.type);
|
||||
sb.Append("\t");
|
||||
sb.Append(step.obj.ToString());
|
||||
sb.Append("\t");
|
||||
sb.Append(step.obj.path);
|
||||
sb.Append("\t");
|
||||
sb.AppendLine(step.time.ToString("F8"));
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public void PreSnapshot() {
|
||||
_snapWatch.Reset();
|
||||
_snapWatch.Start();
|
||||
}
|
||||
|
||||
public void PostSnapshot() {
|
||||
_snapWatch.Stop();
|
||||
_snapTotal += Millisecs(_snapWatch);
|
||||
}
|
||||
|
||||
double Millisecs(Stopwatch watch)
|
||||
{
|
||||
var ticks = watch.ElapsedTicks;
|
||||
return ticks * _millisecsPerTick;
|
||||
}
|
||||
|
||||
public static string FormatMillisecs(double num) {
|
||||
if( num > 5000 ) {
|
||||
return string.Format("{0:N1} secs", num / 1000.0);
|
||||
} if( num > 1000 ) {
|
||||
return string.Format("{0:N2} secs", num / 1000.0);
|
||||
} else if( num > 100 ) {
|
||||
return string.Format("{0:N0} ms", num);
|
||||
} else if( num > 1 ) {
|
||||
return string.Format("{0:N1} ms", num);
|
||||
} else if( num > 0.01 ) {
|
||||
return string.Format("{0:N3} ms", num);
|
||||
} else {
|
||||
return string.Format("{0:N} ms", num);
|
||||
}
|
||||
}
|
||||
|
||||
Stopwatch _continueWatch = new Stopwatch();
|
||||
Stopwatch _stepWatch = new Stopwatch();
|
||||
Stopwatch _snapWatch = new Stopwatch();
|
||||
|
||||
double _continueTotal;
|
||||
double _snapTotal;
|
||||
double _stepTotal;
|
||||
|
||||
string[] _currStepStack;
|
||||
StepDetails _currStepDetails;
|
||||
ProfileNode _rootNode;
|
||||
int _numContinues;
|
||||
|
||||
struct StepDetails {
|
||||
public string type;
|
||||
public Runtime.Object obj;
|
||||
public double time;
|
||||
}
|
||||
List<StepDetails> _stepDetails = new List<StepDetails>();
|
||||
|
||||
static double _millisecsPerTick = 1000.0 / Stopwatch.Frequency;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Node used in the hierarchical tree of timings used by the Profiler.
|
||||
/// Each node corresponds to a single line viewable in a UI-based representation.
|
||||
/// </summary>
|
||||
public class ProfileNode {
|
||||
|
||||
/// <summary>
|
||||
/// The key for the node corresponds to the printable name of the callstack element.
|
||||
/// </summary>
|
||||
public readonly string key;
|
||||
|
||||
|
||||
#pragma warning disable 0649
|
||||
/// <summary>
|
||||
/// Horribly hacky field only used by ink unity integration,
|
||||
/// but saves constructing an entire data structure that mirrors
|
||||
/// the one in here purely to store the state of whether each
|
||||
/// node in the UI has been opened or not /// </summary>
|
||||
public bool openInUI;
|
||||
#pragma warning restore 0649
|
||||
|
||||
/// <summary>
|
||||
/// Whether this node contains any sub-nodes - i.e. does it call anything else
|
||||
/// that has been recorded?
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if has children; otherwise, <c>false</c>.</value>
|
||||
public bool hasChildren {
|
||||
get {
|
||||
return _nodes != null && _nodes.Count > 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Total number of milliseconds this node has been active for.
|
||||
/// </summary>
|
||||
public int totalMillisecs {
|
||||
get {
|
||||
return (int)_totalMillisecs;
|
||||
}
|
||||
}
|
||||
|
||||
public ProfileNode() {
|
||||
|
||||
}
|
||||
|
||||
public ProfileNode(string key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public void AddSample(string[] stack, double duration) {
|
||||
AddSample(stack, -1, duration);
|
||||
}
|
||||
|
||||
void AddSample(string[] stack, int stackIdx, double duration) {
|
||||
|
||||
_totalSampleCount++;
|
||||
_totalMillisecs += duration;
|
||||
|
||||
if( stackIdx == stack.Length-1 ) {
|
||||
_selfSampleCount++;
|
||||
_selfMillisecs += duration;
|
||||
}
|
||||
|
||||
if( stackIdx+1 < stack.Length )
|
||||
AddSampleToNode(stack, stackIdx+1, duration);
|
||||
}
|
||||
|
||||
void AddSampleToNode(string[] stack, int stackIdx, double duration)
|
||||
{
|
||||
var nodeKey = stack[stackIdx];
|
||||
if( _nodes == null ) _nodes = new Dictionary<string, ProfileNode>();
|
||||
|
||||
ProfileNode node;
|
||||
if( !_nodes.TryGetValue(nodeKey, out node) ) {
|
||||
node = new ProfileNode(nodeKey);
|
||||
_nodes[nodeKey] = node;
|
||||
}
|
||||
|
||||
node.AddSample(stack, stackIdx, duration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a sorted enumerable of the nodes in descending order of
|
||||
/// how long they took to run.
|
||||
/// </summary>
|
||||
public IEnumerable<KeyValuePair<string, ProfileNode>> descendingOrderedNodes {
|
||||
get {
|
||||
if( _nodes == null ) return null;
|
||||
return _nodes.OrderByDescending(keyNode => keyNode.Value._totalMillisecs);
|
||||
}
|
||||
}
|
||||
|
||||
void PrintHierarchy(StringBuilder sb, int indent)
|
||||
{
|
||||
Pad(sb, indent);
|
||||
|
||||
sb.Append(key);
|
||||
sb.Append(": ");
|
||||
sb.AppendLine(ownReport);
|
||||
|
||||
if( _nodes == null ) return;
|
||||
|
||||
foreach(var keyNode in descendingOrderedNodes) {
|
||||
keyNode.Value.PrintHierarchy(sb, indent+1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a string giving timing information for this single node, including
|
||||
/// total milliseconds spent on the piece of ink, the time spent within itself
|
||||
/// (v.s. spent in children), as well as the number of samples (instruction steps)
|
||||
/// recorded for both too.
|
||||
/// </summary>
|
||||
/// <value>The own report.</value>
|
||||
public string ownReport {
|
||||
get {
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("total ");
|
||||
sb.Append(Profiler.FormatMillisecs(_totalMillisecs));
|
||||
sb.Append(", self ");
|
||||
sb.Append(Profiler.FormatMillisecs(_selfMillisecs));
|
||||
sb.Append(" (");
|
||||
sb.Append(_selfSampleCount);
|
||||
sb.Append(" self samples, ");
|
||||
sb.Append(_totalSampleCount);
|
||||
sb.Append(" total)");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Pad(StringBuilder sb, int spaces)
|
||||
{
|
||||
for(int i=0; i<spaces; i++) sb.Append(" ");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// String is a report of the sub-tree from this node, but without any of the header information
|
||||
/// that's prepended by the Profiler in its Report() method.
|
||||
/// </summary>
|
||||
public override string ToString ()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
PrintHierarchy(sb, 0);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
Dictionary<string, ProfileNode> _nodes;
|
||||
double _selfMillisecs;
|
||||
double _totalMillisecs;
|
||||
int _selfSampleCount;
|
||||
int _totalSampleCount;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8323a1136a1484abebab14150a06453a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ink.Runtime
|
||||
{
|
||||
public enum PushPopType
|
||||
{
|
||||
Tunnel,
|
||||
Function,
|
||||
FunctionEvaluationFromGame
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b6f42c73ddbc24b8fadf5f6c84287fc0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
namespace Ink.Runtime
|
||||
{
|
||||
// When looking up content within the story (e.g. in Container.ContentAtPath),
|
||||
// the result is generally found, but if the story is modified, then when loading
|
||||
// up an old save state, then some old paths may still exist. In this case we
|
||||
// try to recover by finding an approximate result by working up the story hierarchy
|
||||
// in the path to find the closest valid container. Instead of crashing horribly,
|
||||
// we might see some slight oddness in the content, but hopefully it recovers!
|
||||
public struct SearchResult
|
||||
{
|
||||
public Runtime.Object obj;
|
||||
public bool approximate;
|
||||
|
||||
public Runtime.Object correctObj { get { return approximate ? null : obj; } }
|
||||
public Container container { get { return obj as Container; } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 40f07931ec6ee409e97714e5567fd5c6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,647 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace Ink.Runtime
|
||||
{
|
||||
/// <summary>
|
||||
/// Simple custom JSON serialisation implementation that takes JSON-able System.Collections that
|
||||
/// are produced by the ink engine and converts to and from JSON text.
|
||||
/// </summary>
|
||||
public static class SimpleJson
|
||||
{
|
||||
public static Dictionary<string, object> TextToDictionary (string text)
|
||||
{
|
||||
return new Reader (text).ToDictionary ();
|
||||
}
|
||||
|
||||
public static List<object> TextToArray(string text)
|
||||
{
|
||||
return new Reader(text).ToArray();
|
||||
}
|
||||
|
||||
class Reader
|
||||
{
|
||||
public Reader (string text)
|
||||
{
|
||||
_text = text;
|
||||
_offset = 0;
|
||||
|
||||
SkipWhitespace ();
|
||||
|
||||
_rootObject = ReadObject ();
|
||||
}
|
||||
|
||||
public Dictionary<string, object> ToDictionary ()
|
||||
{
|
||||
return (Dictionary<string, object>)_rootObject;
|
||||
}
|
||||
|
||||
public List<object> ToArray()
|
||||
{
|
||||
return (List<object>)_rootObject;
|
||||
}
|
||||
|
||||
bool IsNumberChar (char c)
|
||||
{
|
||||
return c >= '0' && c <= '9' || c == '.' || c == '-' || c == '+' || c == 'E' || c == 'e';
|
||||
}
|
||||
|
||||
bool IsFirstNumberChar(char c)
|
||||
{
|
||||
return c >= '0' && c <= '9' || c == '-' || c == '+';
|
||||
}
|
||||
|
||||
object ReadObject ()
|
||||
{
|
||||
var currentChar = _text [_offset];
|
||||
|
||||
if( currentChar == '{' )
|
||||
return ReadDictionary ();
|
||||
|
||||
else if (currentChar == '[')
|
||||
return ReadArray ();
|
||||
|
||||
else if (currentChar == '"')
|
||||
return ReadString ();
|
||||
|
||||
else if (IsFirstNumberChar(currentChar))
|
||||
return ReadNumber ();
|
||||
|
||||
else if (TryRead ("true"))
|
||||
return true;
|
||||
|
||||
else if (TryRead ("false"))
|
||||
return false;
|
||||
|
||||
else if (TryRead ("null"))
|
||||
return null;
|
||||
|
||||
throw new System.Exception ("Unhandled object type in JSON: "+_text.Substring (_offset, 30));
|
||||
}
|
||||
|
||||
Dictionary<string, object> ReadDictionary ()
|
||||
{
|
||||
var dict = new Dictionary<string, object> ();
|
||||
|
||||
Expect ("{");
|
||||
|
||||
SkipWhitespace ();
|
||||
|
||||
// Empty dictionary?
|
||||
if (TryRead ("}"))
|
||||
return dict;
|
||||
|
||||
do {
|
||||
|
||||
SkipWhitespace ();
|
||||
|
||||
// Key
|
||||
var key = ReadString ();
|
||||
Expect (key != null, "dictionary key");
|
||||
|
||||
SkipWhitespace ();
|
||||
|
||||
// :
|
||||
Expect (":");
|
||||
|
||||
SkipWhitespace ();
|
||||
|
||||
// Value
|
||||
var val = ReadObject ();
|
||||
Expect (val != null, "dictionary value");
|
||||
|
||||
// Add to dictionary
|
||||
dict [key] = val;
|
||||
|
||||
SkipWhitespace ();
|
||||
|
||||
} while ( TryRead (",") );
|
||||
|
||||
Expect ("}");
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
List<object> ReadArray ()
|
||||
{
|
||||
var list = new List<object> ();
|
||||
|
||||
Expect ("[");
|
||||
|
||||
SkipWhitespace ();
|
||||
|
||||
// Empty list?
|
||||
if (TryRead ("]"))
|
||||
return list;
|
||||
|
||||
do {
|
||||
|
||||
SkipWhitespace ();
|
||||
|
||||
// Value
|
||||
var val = ReadObject ();
|
||||
|
||||
// Add to array
|
||||
list.Add (val);
|
||||
|
||||
SkipWhitespace ();
|
||||
|
||||
} while (TryRead (","));
|
||||
|
||||
Expect ("]");
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
string ReadString ()
|
||||
{
|
||||
Expect ("\"");
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
for (; _offset < _text.Length; _offset++) {
|
||||
var c = _text [_offset];
|
||||
|
||||
if (c == '\\') {
|
||||
// Escaped character
|
||||
_offset++;
|
||||
if (_offset >= _text.Length) {
|
||||
throw new Exception("Unexpected EOF while reading string");
|
||||
}
|
||||
c = _text[_offset];
|
||||
switch (c)
|
||||
{
|
||||
case '"':
|
||||
case '\\':
|
||||
case '/': // Yes, JSON allows this to be escaped
|
||||
sb.Append(c);
|
||||
break;
|
||||
case 'n':
|
||||
sb.Append('\n');
|
||||
break;
|
||||
case 't':
|
||||
sb.Append('\t');
|
||||
break;
|
||||
case 'r':
|
||||
case 'b':
|
||||
case 'f':
|
||||
// Ignore other control characters
|
||||
break;
|
||||
case 'u':
|
||||
// 4-digit Unicode
|
||||
if (_offset + 4 >=_text.Length) {
|
||||
throw new Exception("Unexpected EOF while reading string");
|
||||
}
|
||||
var digits = _text.Substring(_offset + 1, 4);
|
||||
int uchar;
|
||||
if (int.TryParse(digits, System.Globalization.NumberStyles.AllowHexSpecifier, System.Globalization.CultureInfo.InvariantCulture, out uchar)) {
|
||||
sb.Append((char)uchar);
|
||||
_offset += 4;
|
||||
} else {
|
||||
throw new Exception("Invalid Unicode escape character at offset " + (_offset - 1));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// The escaped character is invalid per json spec
|
||||
throw new Exception("Invalid Unicode escape character at offset " + (_offset - 1));
|
||||
}
|
||||
} else if( c == '"' ) {
|
||||
break;
|
||||
} else {
|
||||
sb.Append(c);
|
||||
}
|
||||
}
|
||||
|
||||
Expect ("\"");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
object ReadNumber ()
|
||||
{
|
||||
var startOffset = _offset;
|
||||
|
||||
bool isFloat = false;
|
||||
for (; _offset < _text.Length; _offset++) {
|
||||
var c = _text [_offset];
|
||||
if (c == '.' || c == 'e' || c == 'E') isFloat = true;
|
||||
if (IsNumberChar (c))
|
||||
continue;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
string numStr = _text.Substring (startOffset, _offset - startOffset);
|
||||
|
||||
if (isFloat) {
|
||||
float f;
|
||||
if (float.TryParse (numStr, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out f)) {
|
||||
return f;
|
||||
}
|
||||
} else {
|
||||
int i;
|
||||
if (int.TryParse (numStr, out i)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
throw new System.Exception ("Failed to parse number value: "+numStr);
|
||||
}
|
||||
|
||||
bool TryRead (string textToRead)
|
||||
{
|
||||
if (_offset + textToRead.Length > _text.Length)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < textToRead.Length; i++) {
|
||||
if (textToRead [i] != _text [_offset + i])
|
||||
return false;
|
||||
}
|
||||
|
||||
_offset += textToRead.Length;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Expect (string expectedStr)
|
||||
{
|
||||
if (!TryRead (expectedStr))
|
||||
Expect (false, expectedStr);
|
||||
}
|
||||
|
||||
void Expect (bool condition, string message = null)
|
||||
{
|
||||
if (!condition) {
|
||||
if (message == null) {
|
||||
message = "Unexpected token";
|
||||
} else {
|
||||
message = "Expected " + message;
|
||||
}
|
||||
message += " at offset " + _offset;
|
||||
|
||||
throw new System.Exception (message);
|
||||
}
|
||||
}
|
||||
|
||||
void SkipWhitespace ()
|
||||
{
|
||||
while (_offset < _text.Length) {
|
||||
var c = _text [_offset];
|
||||
if (c == ' ' || c == '\t' || c == '\n' || c == '\r')
|
||||
_offset++;
|
||||
else
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
string _text;
|
||||
int _offset;
|
||||
|
||||
object _rootObject;
|
||||
}
|
||||
|
||||
|
||||
public class Writer
|
||||
{
|
||||
public Writer()
|
||||
{
|
||||
_writer = new StringWriter();
|
||||
}
|
||||
|
||||
public Writer(Stream stream)
|
||||
{
|
||||
_writer = new System.IO.StreamWriter(stream, Encoding.UTF8);
|
||||
}
|
||||
|
||||
public void WriteObject(Action<Writer> inner)
|
||||
{
|
||||
WriteObjectStart();
|
||||
inner(this);
|
||||
WriteObjectEnd();
|
||||
}
|
||||
|
||||
public void WriteObjectStart()
|
||||
{
|
||||
StartNewObject(container: true);
|
||||
_stateStack.Push(new StateElement { type = State.Object });
|
||||
_writer.Write("{");
|
||||
}
|
||||
|
||||
public void WriteObjectEnd()
|
||||
{
|
||||
Assert(state == State.Object);
|
||||
_writer.Write("}");
|
||||
_stateStack.Pop();
|
||||
}
|
||||
|
||||
public void WriteProperty(string name, Action<Writer> inner)
|
||||
{
|
||||
WriteProperty<string>(name, inner);
|
||||
}
|
||||
|
||||
public void WriteProperty(int id, Action<Writer> inner)
|
||||
{
|
||||
WriteProperty<int>(id, inner);
|
||||
}
|
||||
|
||||
public void WriteProperty(string name, string content)
|
||||
{
|
||||
WritePropertyStart(name);
|
||||
Write(content);
|
||||
WritePropertyEnd();
|
||||
}
|
||||
|
||||
public void WriteProperty(string name, int content)
|
||||
{
|
||||
WritePropertyStart(name);
|
||||
Write(content);
|
||||
WritePropertyEnd();
|
||||
}
|
||||
|
||||
public void WriteProperty(string name, bool content)
|
||||
{
|
||||
WritePropertyStart(name);
|
||||
Write(content);
|
||||
WritePropertyEnd();
|
||||
}
|
||||
|
||||
public void WritePropertyStart(string name)
|
||||
{
|
||||
WritePropertyStart<string>(name);
|
||||
}
|
||||
|
||||
public void WritePropertyStart(int id)
|
||||
{
|
||||
WritePropertyStart<int>(id);
|
||||
}
|
||||
|
||||
public void WritePropertyEnd()
|
||||
{
|
||||
Assert(state == State.Property);
|
||||
Assert(childCount == 1);
|
||||
_stateStack.Pop();
|
||||
}
|
||||
|
||||
public void WritePropertyNameStart()
|
||||
{
|
||||
Assert(state == State.Object);
|
||||
|
||||
if (childCount > 0)
|
||||
_writer.Write(",");
|
||||
|
||||
_writer.Write("\"");
|
||||
|
||||
IncrementChildCount();
|
||||
|
||||
_stateStack.Push(new StateElement { type = State.Property });
|
||||
_stateStack.Push(new StateElement { type = State.PropertyName });
|
||||
}
|
||||
|
||||
public void WritePropertyNameEnd()
|
||||
{
|
||||
Assert(state == State.PropertyName);
|
||||
|
||||
_writer.Write("\":");
|
||||
|
||||
// Pop PropertyName, leaving Property state
|
||||
_stateStack.Pop();
|
||||
}
|
||||
|
||||
public void WritePropertyNameInner(string str)
|
||||
{
|
||||
Assert(state == State.PropertyName);
|
||||
_writer.Write(str);
|
||||
}
|
||||
|
||||
void WritePropertyStart<T>(T name)
|
||||
{
|
||||
Assert(state == State.Object);
|
||||
|
||||
if (childCount > 0)
|
||||
_writer.Write(",");
|
||||
|
||||
_writer.Write("\"");
|
||||
_writer.Write(name);
|
||||
_writer.Write("\":");
|
||||
|
||||
IncrementChildCount();
|
||||
|
||||
_stateStack.Push(new StateElement { type = State.Property });
|
||||
}
|
||||
|
||||
|
||||
// allow name to be string or int
|
||||
void WriteProperty<T>(T name, Action<Writer> inner)
|
||||
{
|
||||
WritePropertyStart(name);
|
||||
|
||||
inner(this);
|
||||
|
||||
WritePropertyEnd();
|
||||
}
|
||||
|
||||
public void WriteArrayStart()
|
||||
{
|
||||
StartNewObject(container: true);
|
||||
_stateStack.Push(new StateElement { type = State.Array });
|
||||
_writer.Write("[");
|
||||
}
|
||||
|
||||
public void WriteArrayEnd()
|
||||
{
|
||||
Assert(state == State.Array);
|
||||
_writer.Write("]");
|
||||
_stateStack.Pop();
|
||||
}
|
||||
|
||||
public void Write(int i)
|
||||
{
|
||||
StartNewObject(container: false);
|
||||
_writer.Write(i);
|
||||
}
|
||||
|
||||
public void Write(float f)
|
||||
{
|
||||
StartNewObject(container: false);
|
||||
|
||||
// TODO: Find an heap-allocation-free way to do this please!
|
||||
// _writer.Write(formatStr, obj (the float)) requires boxing
|
||||
// Following implementation seems to work ok but requires creating temporary garbage string.
|
||||
string floatStr = f.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
if( floatStr == "Infinity" ) {
|
||||
_writer.Write("3.4E+38"); // JSON doesn't support, do our best alternative
|
||||
} else if (floatStr == "-Infinity") {
|
||||
_writer.Write("-3.4E+38"); // JSON doesn't support, do our best alternative
|
||||
} else if ( floatStr == "NaN" ) {
|
||||
_writer.Write("0.0"); // JSON doesn't support, not much we can do
|
||||
} else {
|
||||
_writer.Write(floatStr);
|
||||
if (!floatStr.Contains(".") && !floatStr.Contains("E"))
|
||||
_writer.Write(".0"); // ensure it gets read back in as a floating point value
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(string str, bool escape = true)
|
||||
{
|
||||
StartNewObject(container: false);
|
||||
|
||||
_writer.Write("\"");
|
||||
if (escape)
|
||||
WriteEscapedString(str);
|
||||
else
|
||||
_writer.Write(str);
|
||||
_writer.Write("\"");
|
||||
}
|
||||
|
||||
public void Write(bool b)
|
||||
{
|
||||
StartNewObject(container: false);
|
||||
_writer.Write(b ? "true" : "false");
|
||||
}
|
||||
|
||||
public void WriteNull()
|
||||
{
|
||||
StartNewObject(container: false);
|
||||
_writer.Write("null");
|
||||
}
|
||||
|
||||
public void WriteStringStart()
|
||||
{
|
||||
StartNewObject(container: false);
|
||||
_stateStack.Push(new StateElement { type = State.String });
|
||||
_writer.Write("\"");
|
||||
}
|
||||
|
||||
public void WriteStringEnd()
|
||||
{
|
||||
Assert(state == State.String);
|
||||
_writer.Write("\"");
|
||||
_stateStack.Pop();
|
||||
}
|
||||
|
||||
public void WriteStringInner(string str, bool escape = true)
|
||||
{
|
||||
Assert(state == State.String);
|
||||
if (escape)
|
||||
WriteEscapedString(str);
|
||||
else
|
||||
_writer.Write(str);
|
||||
}
|
||||
|
||||
void WriteEscapedString(string str)
|
||||
{
|
||||
foreach (var c in str)
|
||||
{
|
||||
if (c < ' ')
|
||||
{
|
||||
// Don't write any control characters except \n and \t
|
||||
switch (c)
|
||||
{
|
||||
case '\n':
|
||||
_writer.Write("\\n");
|
||||
break;
|
||||
case '\t':
|
||||
_writer.Write("\\t");
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case '\\':
|
||||
case '"':
|
||||
_writer.Write("\\");
|
||||
_writer.Write(c);
|
||||
break;
|
||||
default:
|
||||
_writer.Write(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void StartNewObject(bool container)
|
||||
{
|
||||
|
||||
if (container)
|
||||
Assert(state == State.None || state == State.Property || state == State.Array);
|
||||
else
|
||||
Assert(state == State.Property || state == State.Array);
|
||||
|
||||
if (state == State.Array && childCount > 0)
|
||||
_writer.Write(",");
|
||||
|
||||
if (state == State.Property)
|
||||
Assert(childCount == 0);
|
||||
|
||||
if (state == State.Array || state == State.Property)
|
||||
IncrementChildCount();
|
||||
}
|
||||
|
||||
State state
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_stateStack.Count > 0) return _stateStack.Peek().type;
|
||||
else return State.None;
|
||||
}
|
||||
}
|
||||
|
||||
int childCount
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_stateStack.Count > 0) return _stateStack.Peek().childCount;
|
||||
else return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void IncrementChildCount()
|
||||
{
|
||||
Assert(_stateStack.Count > 0);
|
||||
var currEl = _stateStack.Pop();
|
||||
currEl.childCount++;
|
||||
_stateStack.Push(currEl);
|
||||
}
|
||||
|
||||
// Shouldn't hit this assert outside of initial JSON development,
|
||||
// so it's save to make it debug-only.
|
||||
[System.Diagnostics.Conditional("DEBUG")]
|
||||
void Assert(bool condition)
|
||||
{
|
||||
if (!condition)
|
||||
throw new System.Exception("Assert failed while writing JSON");
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return _writer.ToString();
|
||||
}
|
||||
|
||||
enum State
|
||||
{
|
||||
None,
|
||||
Object,
|
||||
Array,
|
||||
Property,
|
||||
PropertyName,
|
||||
String
|
||||
};
|
||||
|
||||
struct StateElement
|
||||
{
|
||||
public State type;
|
||||
public int childCount;
|
||||
}
|
||||
|
||||
Stack<StateElement> _stateStack = new Stack<StateElement>();
|
||||
TextWriter _writer;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5f2b378ccb9e148929c90bae41bb2c9d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,66 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ink.Runtime
|
||||
{
|
||||
public class StatePatch
|
||||
{
|
||||
public Dictionary<string, Runtime.Object> globals { get { return _globals; } }
|
||||
public HashSet<string> changedVariables { get { return _changedVariables; } }
|
||||
public Dictionary<Container, int> visitCounts { get { return _visitCounts; } }
|
||||
public Dictionary<Container, int> turnIndices { get { return _turnIndices; } }
|
||||
|
||||
public StatePatch(StatePatch toCopy)
|
||||
{
|
||||
if( toCopy != null ) {
|
||||
_globals = new Dictionary<string, Object>(toCopy._globals);
|
||||
_changedVariables = new HashSet<string>(toCopy._changedVariables);
|
||||
_visitCounts = new Dictionary<Container, int>(toCopy._visitCounts);
|
||||
_turnIndices = new Dictionary<Container, int>(toCopy._turnIndices);
|
||||
} else {
|
||||
_globals = new Dictionary<string, Object>();
|
||||
_changedVariables = new HashSet<string>();
|
||||
_visitCounts = new Dictionary<Container, int>();
|
||||
_turnIndices = new Dictionary<Container, int>();
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetGlobal(string name, out Runtime.Object value)
|
||||
{
|
||||
return _globals.TryGetValue(name, out value);
|
||||
}
|
||||
|
||||
public void SetGlobal(string name, Runtime.Object value){
|
||||
_globals[name] = value;
|
||||
}
|
||||
|
||||
public void AddChangedVariable(string name)
|
||||
{
|
||||
_changedVariables.Add(name);
|
||||
}
|
||||
|
||||
public bool TryGetVisitCount(Container container, out int count)
|
||||
{
|
||||
return _visitCounts.TryGetValue(container, out count);
|
||||
}
|
||||
|
||||
public void SetVisitCount(Container container, int count)
|
||||
{
|
||||
_visitCounts[container] = count;
|
||||
}
|
||||
|
||||
public void SetTurnIndex(Container container, int index)
|
||||
{
|
||||
_turnIndices[container] = index;
|
||||
}
|
||||
|
||||
public bool TryGetTurnIndex(Container container, out int index)
|
||||
{
|
||||
return _turnIndices.TryGetValue(container, out index);
|
||||
}
|
||||
|
||||
Dictionary<string, Runtime.Object> _globals;
|
||||
HashSet<string> _changedVariables = new HashSet<string>();
|
||||
Dictionary<Container, int> _visitCounts = new Dictionary<Container, int>();
|
||||
Dictionary<Container, int> _turnIndices = new Dictionary<Container, int>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: be1efe20e2058440e8efa8927902041f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
2763
Unity/Alternate Genre Jam/Assets/Ink/InkLibs/InkRuntime/Story.cs
Normal file
2763
Unity/Alternate Genre Jam/Assets/Ink/InkLibs/InkRuntime/Story.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9996ae35e3e4647e8b8ee0c5aeb61489
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace Ink.Runtime
|
||||
{
|
||||
/// <summary>
|
||||
/// Exception that represents an error when running a Story at runtime.
|
||||
/// An exception being thrown of this type is typically when there's
|
||||
/// a bug in your ink, rather than in the ink engine itself!
|
||||
/// </summary>
|
||||
public class StoryException : System.Exception
|
||||
{
|
||||
public bool useEndLineNumber;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a default instance of a StoryException without a message.
|
||||
/// </summary>
|
||||
public StoryException () { }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs an instance of a StoryException with a message.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message.</param>
|
||||
public StoryException(string message) : base(message) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 83b6443aded8c49e1960594d5f64e114
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 69b5ba5009dea4a3d8f67fbe1ebbc902
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Ink.Runtime
|
||||
{
|
||||
public static class StringExt
|
||||
{
|
||||
public static string Join<T>(string separator, List<T> objects)
|
||||
{
|
||||
var sb = new StringBuilder ();
|
||||
|
||||
var isFirst = true;
|
||||
foreach (var o in objects) {
|
||||
|
||||
if (!isFirst)
|
||||
sb.Append (separator);
|
||||
|
||||
sb.Append (o.ToString ());
|
||||
|
||||
isFirst = false;
|
||||
}
|
||||
|
||||
return sb.ToString ();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4c907c30d0b7c4e5aa2dfe477bba8a89
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
|
||||
namespace Ink.Runtime
|
||||
{
|
||||
public class Tag : Runtime.Object
|
||||
{
|
||||
public string text { get; private set; }
|
||||
|
||||
public Tag (string tagText)
|
||||
{
|
||||
this.text = tagText;
|
||||
}
|
||||
|
||||
public override string ToString ()
|
||||
{
|
||||
return "# " + text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b8a631dc795b748dfbe8f53304cd9675
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
405
Unity/Alternate Genre Jam/Assets/Ink/InkLibs/InkRuntime/Value.cs
Normal file
405
Unity/Alternate Genre Jam/Assets/Ink/InkLibs/InkRuntime/Value.cs
Normal file
@@ -0,0 +1,405 @@
|
||||
using System.ComponentModel;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Ink.Runtime
|
||||
{
|
||||
// Order is significant for type coersion.
|
||||
// If types aren't directly compatible for an operation,
|
||||
// they're coerced to the same type, downward.
|
||||
// Higher value types "infect" an operation.
|
||||
// (This may not be the most sensible thing to do, but it's worked so far!)
|
||||
public enum ValueType
|
||||
{
|
||||
// Bool is new addition, keep enum values the same, with Int==0, Float==1 etc,
|
||||
// but for coersion rules, we want to keep bool with a lower value than Int
|
||||
// so that it converts in the right direction
|
||||
Bool = -1,
|
||||
// Used in coersion
|
||||
Int,
|
||||
Float,
|
||||
List,
|
||||
String,
|
||||
|
||||
// Not used for coersion described above
|
||||
DivertTarget,
|
||||
VariablePointer
|
||||
}
|
||||
|
||||
public abstract class Value : Runtime.Object
|
||||
{
|
||||
public abstract ValueType valueType { get; }
|
||||
public abstract bool isTruthy { get; }
|
||||
|
||||
public abstract Value Cast(ValueType newType);
|
||||
|
||||
public abstract object valueObject { get; }
|
||||
|
||||
public static Value Create(object val)
|
||||
{
|
||||
// Implicitly lose precision from any doubles we get passed in
|
||||
if (val is double) {
|
||||
double doub = (double)val;
|
||||
val = (float)doub;
|
||||
}
|
||||
|
||||
if( val is bool ) {
|
||||
return new BoolValue((bool)val);
|
||||
} else if (val is int) {
|
||||
return new IntValue ((int)val);
|
||||
} else if (val is long) {
|
||||
return new IntValue ((int)(long)val);
|
||||
} else if (val is float) {
|
||||
return new FloatValue ((float)val);
|
||||
} else if (val is double) {
|
||||
return new FloatValue ((float)(double)val);
|
||||
} else if (val is string) {
|
||||
return new StringValue ((string)val);
|
||||
} else if (val is Path) {
|
||||
return new DivertTargetValue ((Path)val);
|
||||
} else if (val is InkList) {
|
||||
return new ListValue ((InkList)val);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override Object Copy()
|
||||
{
|
||||
return Create (valueObject);
|
||||
}
|
||||
|
||||
protected StoryException BadCastException (ValueType targetType)
|
||||
{
|
||||
return new StoryException ("Can't cast "+this.valueObject+" from " + this.valueType+" to "+targetType);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class Value<T> : Value
|
||||
{
|
||||
public T value { get; set; }
|
||||
|
||||
public override object valueObject {
|
||||
get {
|
||||
return (object)value;
|
||||
}
|
||||
}
|
||||
|
||||
public Value (T val)
|
||||
{
|
||||
value = val;
|
||||
}
|
||||
|
||||
public override string ToString ()
|
||||
{
|
||||
return value.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public class BoolValue : Value<bool>
|
||||
{
|
||||
public override ValueType valueType { get { return ValueType.Bool; } }
|
||||
public override bool isTruthy { get { return value; } }
|
||||
|
||||
public BoolValue(bool boolVal) : base(boolVal)
|
||||
{
|
||||
}
|
||||
|
||||
public BoolValue() : this(false) {}
|
||||
|
||||
public override Value Cast(ValueType newType)
|
||||
{
|
||||
if (newType == valueType) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (newType == ValueType.Int) {
|
||||
return new IntValue (this.value ? 1 : 0);
|
||||
}
|
||||
|
||||
if (newType == ValueType.Float) {
|
||||
return new FloatValue (this.value ? 1.0f : 0.0f);
|
||||
}
|
||||
|
||||
if (newType == ValueType.String) {
|
||||
return new StringValue(this.value ? "true" : "false");
|
||||
}
|
||||
|
||||
throw BadCastException (newType);
|
||||
}
|
||||
|
||||
public override string ToString ()
|
||||
{
|
||||
// Instead of C# "True" / "False"
|
||||
return value ? "true" : "false";
|
||||
}
|
||||
}
|
||||
|
||||
public class IntValue : Value<int>
|
||||
{
|
||||
public override ValueType valueType { get { return ValueType.Int; } }
|
||||
public override bool isTruthy { get { return value != 0; } }
|
||||
|
||||
public IntValue(int intVal) : base(intVal)
|
||||
{
|
||||
}
|
||||
|
||||
public IntValue() : this(0) {}
|
||||
|
||||
public override Value Cast(ValueType newType)
|
||||
{
|
||||
if (newType == valueType) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (newType == ValueType.Bool) {
|
||||
return new BoolValue (this.value == 0 ? false : true);
|
||||
}
|
||||
|
||||
if (newType == ValueType.Float) {
|
||||
return new FloatValue ((float)this.value);
|
||||
}
|
||||
|
||||
if (newType == ValueType.String) {
|
||||
return new StringValue("" + this.value);
|
||||
}
|
||||
|
||||
throw BadCastException (newType);
|
||||
}
|
||||
}
|
||||
|
||||
public class FloatValue : Value<float>
|
||||
{
|
||||
public override ValueType valueType { get { return ValueType.Float; } }
|
||||
public override bool isTruthy { get { return value != 0.0f; } }
|
||||
|
||||
public FloatValue(float val) : base(val)
|
||||
{
|
||||
}
|
||||
|
||||
public FloatValue() : this(0.0f) {}
|
||||
|
||||
public override Value Cast(ValueType newType)
|
||||
{
|
||||
if (newType == valueType) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (newType == ValueType.Bool) {
|
||||
return new BoolValue (this.value == 0.0f ? false : true);
|
||||
}
|
||||
|
||||
if (newType == ValueType.Int) {
|
||||
return new IntValue ((int)this.value);
|
||||
}
|
||||
|
||||
if (newType == ValueType.String) {
|
||||
return new StringValue("" + this.value.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
throw BadCastException (newType);
|
||||
}
|
||||
}
|
||||
|
||||
public class StringValue : Value<string>
|
||||
{
|
||||
public override ValueType valueType { get { return ValueType.String; } }
|
||||
public override bool isTruthy { get { return value.Length > 0; } }
|
||||
|
||||
public bool isNewline { get; private set; }
|
||||
public bool isInlineWhitespace { get; private set; }
|
||||
public bool isNonWhitespace {
|
||||
get {
|
||||
return !isNewline && !isInlineWhitespace;
|
||||
}
|
||||
}
|
||||
|
||||
public StringValue(string str) : base(str)
|
||||
{
|
||||
// Classify whitespace status
|
||||
isNewline = value == "\n";
|
||||
isInlineWhitespace = true;
|
||||
foreach (var c in value) {
|
||||
if (c != ' ' && c != '\t') {
|
||||
isInlineWhitespace = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public StringValue() : this("") {}
|
||||
|
||||
public override Value Cast(ValueType newType)
|
||||
{
|
||||
if (newType == valueType) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (newType == ValueType.Int) {
|
||||
|
||||
int parsedInt;
|
||||
if (int.TryParse (value, out parsedInt)) {
|
||||
return new IntValue (parsedInt);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (newType == ValueType.Float) {
|
||||
float parsedFloat;
|
||||
if (float.TryParse (value, System.Globalization.NumberStyles.Float ,System.Globalization.CultureInfo.InvariantCulture, out parsedFloat)) {
|
||||
return new FloatValue (parsedFloat);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
throw BadCastException (newType);
|
||||
}
|
||||
}
|
||||
|
||||
public class DivertTargetValue : Value<Path>
|
||||
{
|
||||
public Path targetPath { get { return this.value; } set { this.value = value; } }
|
||||
public override ValueType valueType { get { return ValueType.DivertTarget; } }
|
||||
public override bool isTruthy { get { throw new System.Exception("Shouldn't be checking the truthiness of a divert target"); } }
|
||||
|
||||
public DivertTargetValue(Path targetPath) : base(targetPath)
|
||||
{
|
||||
}
|
||||
|
||||
public DivertTargetValue() : base(null)
|
||||
{}
|
||||
|
||||
public override Value Cast(ValueType newType)
|
||||
{
|
||||
if (newType == valueType)
|
||||
return this;
|
||||
|
||||
throw BadCastException (newType);
|
||||
}
|
||||
|
||||
public override string ToString ()
|
||||
{
|
||||
return "DivertTargetValue(" + targetPath + ")";
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Think: Erm, I get that this contains a string, but should
|
||||
// we really derive from Value<string>? That seems a bit misleading to me.
|
||||
public class VariablePointerValue : Value<string>
|
||||
{
|
||||
public string variableName { get { return this.value; } set { this.value = value; } }
|
||||
public override ValueType valueType { get { return ValueType.VariablePointer; } }
|
||||
public override bool isTruthy { get { throw new System.Exception("Shouldn't be checking the truthiness of a variable pointer"); } }
|
||||
|
||||
// Where the variable is located
|
||||
// -1 = default, unknown, yet to be determined
|
||||
// 0 = in global scope
|
||||
// 1+ = callstack element index + 1 (so that the first doesn't conflict with special global scope)
|
||||
public int contextIndex { get; set; }
|
||||
|
||||
public VariablePointerValue(string variableName, int contextIndex = -1) : base(variableName)
|
||||
{
|
||||
this.contextIndex = contextIndex;
|
||||
}
|
||||
|
||||
public VariablePointerValue() : this(null)
|
||||
{
|
||||
}
|
||||
|
||||
public override Value Cast(ValueType newType)
|
||||
{
|
||||
if (newType == valueType)
|
||||
return this;
|
||||
|
||||
throw BadCastException (newType);
|
||||
}
|
||||
|
||||
public override string ToString ()
|
||||
{
|
||||
return "VariablePointerValue(" + variableName + ")";
|
||||
}
|
||||
|
||||
public override Object Copy()
|
||||
{
|
||||
return new VariablePointerValue (variableName, contextIndex);
|
||||
}
|
||||
}
|
||||
|
||||
public class ListValue : Value<InkList>
|
||||
{
|
||||
public override ValueType valueType {
|
||||
get {
|
||||
return ValueType.List;
|
||||
}
|
||||
}
|
||||
|
||||
// Truthy if it is non-empty
|
||||
public override bool isTruthy {
|
||||
get {
|
||||
return value.Count > 0;
|
||||
}
|
||||
}
|
||||
|
||||
public override Value Cast (ValueType newType)
|
||||
{
|
||||
if (newType == ValueType.Int) {
|
||||
var max = value.maxItem;
|
||||
if( max.Key.isNull )
|
||||
return new IntValue (0);
|
||||
else
|
||||
return new IntValue (max.Value);
|
||||
}
|
||||
|
||||
else if (newType == ValueType.Float) {
|
||||
var max = value.maxItem;
|
||||
if (max.Key.isNull)
|
||||
return new FloatValue (0.0f);
|
||||
else
|
||||
return new FloatValue ((float)max.Value);
|
||||
}
|
||||
|
||||
else if (newType == ValueType.String) {
|
||||
var max = value.maxItem;
|
||||
if (max.Key.isNull)
|
||||
return new StringValue ("");
|
||||
else {
|
||||
return new StringValue (max.Key.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
if (newType == valueType)
|
||||
return this;
|
||||
|
||||
throw BadCastException (newType);
|
||||
}
|
||||
|
||||
public ListValue () : base(null) {
|
||||
value = new InkList ();
|
||||
}
|
||||
|
||||
public ListValue (InkList list) : base (null)
|
||||
{
|
||||
value = new InkList (list);
|
||||
}
|
||||
|
||||
public ListValue (InkListItem singleItem, int singleValue) : base (null)
|
||||
{
|
||||
value = new InkList {
|
||||
{singleItem, singleValue}
|
||||
};
|
||||
}
|
||||
|
||||
public static void RetainListOriginsForAssignment (Runtime.Object oldValue, Runtime.Object newValue)
|
||||
{
|
||||
var oldList = oldValue as ListValue;
|
||||
var newList = newValue as ListValue;
|
||||
|
||||
// When assigning the emtpy list, try to retain any initial origin names
|
||||
if (oldList && newList && newList.value.Count == 0)
|
||||
newList.value.SetInitialOriginNames (oldList.value.originNames);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 57357686b389f45c9ad0659798094c5d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Ink.Runtime
|
||||
{
|
||||
// The value to be assigned is popped off the evaluation stack, so no need to keep it here
|
||||
public class VariableAssignment : Runtime.Object
|
||||
{
|
||||
public string variableName { get; protected set; }
|
||||
public bool isNewDeclaration { get; protected set; }
|
||||
public bool isGlobal { get; set; }
|
||||
|
||||
public VariableAssignment (string variableName, bool isNewDeclaration)
|
||||
{
|
||||
this.variableName = variableName;
|
||||
this.isNewDeclaration = isNewDeclaration;
|
||||
}
|
||||
|
||||
// Require default constructor for serialisation
|
||||
public VariableAssignment() : this(null, false) {}
|
||||
|
||||
public override string ToString ()
|
||||
{
|
||||
return "VarAssign to " + variableName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fb2c978a16e604e18b1c0ca3ea64db84
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,51 @@
|
||||
namespace Ink.Runtime
|
||||
{
|
||||
public class VariableReference : Runtime.Object
|
||||
{
|
||||
// Normal named variable
|
||||
public string name { get; set; }
|
||||
|
||||
// Variable reference is actually a path for a visit (read) count
|
||||
public Path pathForCount { get; set; }
|
||||
|
||||
public Container containerForCount {
|
||||
get {
|
||||
return this.ResolvePath (pathForCount).container;
|
||||
}
|
||||
}
|
||||
|
||||
public string pathStringForCount {
|
||||
get {
|
||||
if( pathForCount == null )
|
||||
return null;
|
||||
|
||||
return CompactPathString(pathForCount);
|
||||
}
|
||||
set {
|
||||
if (value == null)
|
||||
pathForCount = null;
|
||||
else
|
||||
pathForCount = new Path (value);
|
||||
}
|
||||
}
|
||||
|
||||
public VariableReference (string name)
|
||||
{
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
// Require default constructor for serialisation
|
||||
public VariableReference() {}
|
||||
|
||||
public override string ToString ()
|
||||
{
|
||||
if (name != null) {
|
||||
return string.Format ("var({0})", name);
|
||||
} else {
|
||||
var pathStr = pathStringForCount;
|
||||
return string.Format("read_count({0})", pathStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3c9a8ec523da34a6196730896d7a2c1d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,417 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 98a3826d0e499440992195956701397e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Ink.Runtime
|
||||
{
|
||||
public class Void : Runtime.Object
|
||||
{
|
||||
public Void ()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 28e3cd581fd59451fb8dacea1b19f477
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user