Committed everything

This commit is contained in:
2021-06-30 21:39:19 +10:00
commit fcfa8e7213
525 changed files with 49440 additions and 0 deletions

View File

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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 152213b0affe5410c9b7ef8eac085a64
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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()
{
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8db545ff917fa46d7aa5f472a9fdbad8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b790cb942d6b84ca28e93a2b46d8c48f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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 ();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2739750c31df04462a8dedfa904e8760
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 41100189b7af04624a6c92ee13e18232
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c1ae8e933de9541df9d12af663906aaf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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 ();
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e98dd0cea4d0a4ddcbc9a1f135b486d4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5119822ad16c24c40b1f80c639c17671
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 625dee430631288439017a5dc0349c86
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,13 @@
namespace Ink.Runtime
{
public class Glue : Runtime.Object
{
public Glue() { }
public override string ToString ()
{
return "Glue";
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 60dc9de8c1e834c6eade8d924a7afe45
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,10 @@

namespace Ink.Runtime
{
public interface INamedContent
{
string name { get; }
bool hasValidName { get; }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: db1f7c668efd4422d8437ac965745242
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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) &amp;&amp; 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 &lt; 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 &lt;= list2) in ink,
/// or LIST_MAX(list1) &lt;= LIST_MAX(list2) &amp;&amp; LIST_MIN(list1) &lt;= 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 ();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3a56f188ddfd3452386a797f878ab7ed
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1365b619ff45f4d12b7bf20cc52a4173
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: efacbd9aad3e9480d9c82bbd58922944
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ec881a45ed342471fb87e412417b6343
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b713d1356fec8458380a308bdc2e8b8f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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 ();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 20b56f60ae0f44147bccbae6ef3d0054
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 62ce05aceee3c42cc90313aec848e171
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 802f84ed94f0e4f518e4be47877e9a33
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8323a1136a1484abebab14150a06453a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
namespace Ink.Runtime
{
public enum PushPopType
{
Tunnel,
Function,
FunctionEvaluationFromGame
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b6f42c73ddbc24b8fadf5f6c84287fc0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 40f07931ec6ee409e97714e5567fd5c6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5f2b378ccb9e148929c90bae41bb2c9d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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>();
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: be1efe20e2058440e8efa8927902041f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9996ae35e3e4647e8b8ee0c5aeb61489
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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) {}
}
}

View File

@@ -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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 69b5ba5009dea4a3d8f67fbe1ebbc902
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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 ();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4c907c30d0b7c4e5aa2dfe477bba8a89
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b8a631dc795b748dfbe8f53304cd9675
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 57357686b389f45c9ad0659798094c5d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fb2c978a16e604e18b1c0ca3ea64db84
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3c9a8ec523da34a6196730896d7a2c1d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 98a3826d0e499440992195956701397e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,10 @@
namespace Ink.Runtime
{
public class Void : Runtime.Object
{
public Void ()
{
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 28e3cd581fd59451fb8dacea1b19f477
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: