diff --git a/gradle.properties b/gradle.properties index fe2bf22..456a3ee 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -# Matching Ink v1.0.0 -version=1.0.1 +# Matching Ink v1.1.1 +version=1.1.0 diff --git a/src/main/java/com/bladecoder/ink/runtime/Choice.java b/src/main/java/com/bladecoder/ink/runtime/Choice.java index 18d09b1..2c2b73c 100644 --- a/src/main/java/com/bladecoder/ink/runtime/Choice.java +++ b/src/main/java/com/bladecoder/ink/runtime/Choice.java @@ -1,68 +1,78 @@ package com.bladecoder.ink.runtime; +import java.util.List; + /** * 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. */ public class Choice extends RTObject { - Path targetPath; - boolean isInvisibleDefault; - - /** - * The original index into currentChoices list on the Story when this Choice - * was generated, for convenience. - */ - private int index = 0; - - int originalThreadIndex = 0; - - /** - * The main text to presented to the player for this Choice. - */ - private String text; - - private CallStack.Thread threadAtGeneration; - - String sourcePath; - - public Choice() throws Exception { - } - - public int getIndex() { - return index; - } - - /** - * The target path that the Story should be diverted to if this Choice is - * chosen. - */ - public String getPathStringOnChoice() throws Exception { - return targetPath.toString (); - } - - public void setPathStringOnChoice(String value) throws Exception { - targetPath = new Path (value); - } - - public String getText() { - return text; - } - - public CallStack.Thread getThreadAtGeneration() { - return threadAtGeneration; - } - - public void setIndex(int value) { - index = value; - } - - public void setText(String value) { - text = value; - } - - public void setThreadAtGeneration(CallStack.Thread value) { - threadAtGeneration = value; - } + Path targetPath; + boolean isInvisibleDefault; + + List tags; + + /** + * The original index into currentChoices list on the Story when this Choice + * was generated, for convenience. + */ + private int index = 0; + + int originalThreadIndex = 0; + + /** + * The main text to presented to the player for this Choice. + */ + private String text; + + private CallStack.Thread threadAtGeneration; + + String sourcePath; + + public Choice() throws Exception { + } + + public int getIndex() { + return index; + } + + /** + * The target path that the Story should be diverted to if this Choice is + * chosen. + */ + public String getPathStringOnChoice() throws Exception { + return targetPath.toString(); + } + + public void setPathStringOnChoice(String value) throws Exception { + targetPath = new Path(value); + } + + public String getText() { + return text; + } + + public List getTags() { + return tags; + } + + ; + + public CallStack.Thread getThreadAtGeneration() { + return threadAtGeneration; + } + + public void setIndex(int value) { + index = value; + } + + public void setText(String value) { + text = value; + } + + public void setThreadAtGeneration(CallStack.Thread value) { + threadAtGeneration = value; + } } diff --git a/src/main/java/com/bladecoder/ink/runtime/ControlCommand.java b/src/main/java/com/bladecoder/ink/runtime/ControlCommand.java index d213026..9bbc81f 100644 --- a/src/main/java/com/bladecoder/ink/runtime/ControlCommand.java +++ b/src/main/java/com/bladecoder/ink/runtime/ControlCommand.java @@ -1,146 +1,154 @@ package com.bladecoder.ink.runtime; -import com.bladecoder.ink.runtime.ControlCommand; -import com.bladecoder.ink.runtime.RTObject; - public class ControlCommand extends RTObject { - public enum CommandType { - NotSet, EvalStart, EvalOutput, EvalEnd, Duplicate, PopEvaluatedValue, PopFunction, PopTunnel, BeginString, EndString, NoOp, ChoiceCount, Turns, TurnsSince, ReadCount, Random, SeedRandom, VisitIndex, SequenceShuffleIndex, StartThread, Done, End, ListFromInt, ListRange, ListRandom - } - - private CommandType commandType = CommandType.NotSet; - - public CommandType getCommandType() { - return commandType; - } - - public void setCommandType(CommandType value) { - commandType = value; - } - - public ControlCommand(CommandType commandType) { - this.setCommandType(commandType); - } - - // Require default constructor for serialisation - public ControlCommand() { - this(CommandType.NotSet); - } - - @Override - RTObject copy() { - return new ControlCommand(getCommandType()); - } - - // The following static factory methods are to make generating these - // RTObjects - // 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); - } - - @Override - public String toString() { - return getCommandType().toString(); - } + public enum CommandType { + NotSet, EvalStart, EvalOutput, EvalEnd, Duplicate, PopEvaluatedValue, PopFunction, PopTunnel, BeginString, + EndString, NoOp, ChoiceCount, Turns, TurnsSince, ReadCount, Random, SeedRandom, VisitIndex, + SequenceShuffleIndex, StartThread, Done, End, + ListFromInt, ListRange, ListRandom, BeginTag, EndTag + } + + private CommandType commandType = CommandType.NotSet; + + public CommandType getCommandType() { + return commandType; + } + + public void setCommandType(CommandType value) { + commandType = value; + } + + public ControlCommand(CommandType commandType) { + this.setCommandType(commandType); + } + + // Require default constructor for serialisation + public ControlCommand() { + this(CommandType.NotSet); + } + + @Override + RTObject copy() { + return new ControlCommand(getCommandType()); + } + + // The following static factory methods are to make generating these + // RTObjects + // 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 static ControlCommand beginTag() { + return new ControlCommand(CommandType.BeginTag); + } + + public static ControlCommand endTag() { + return new ControlCommand(CommandType.EndTag); + } + + @Override + public String toString() { + return getCommandType().toString(); + } } diff --git a/src/main/java/com/bladecoder/ink/runtime/InkList.java b/src/main/java/com/bladecoder/ink/runtime/InkList.java index 8955f95..4ca36fa 100644 --- a/src/main/java/com/bladecoder/ink/runtime/InkList.java +++ b/src/main/java/com/bladecoder/ink/runtime/InkList.java @@ -60,7 +60,9 @@ public InkList(String singleOriginListName, Story originStory) throws Exception */ public InkList(InkList otherList) { super(otherList); - this.originNames = otherList.originNames; + + if (otherList.originNames != null) + this.originNames = new ArrayList<>(otherList.originNames); if (otherList.origins != null) { origins = new ArrayList<>(otherList.origins); diff --git a/src/main/java/com/bladecoder/ink/runtime/Json.java b/src/main/java/com/bladecoder/ink/runtime/Json.java index d42f8cc..d3dd0dd 100644 --- a/src/main/java/com/bladecoder/ink/runtime/Json.java +++ b/src/main/java/com/bladecoder/ink/runtime/Json.java @@ -1,771 +1,773 @@ package com.bladecoder.ink.runtime; +import com.bladecoder.ink.runtime.ControlCommand.CommandType; + import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map.Entry; -import com.bladecoder.ink.runtime.ControlCommand.CommandType; - public class Json { - public static List jArrayToRuntimeObjList(List jArray, boolean skipLast) throws Exception { - int count = jArray.size(); - - if (skipLast) - count--; - - List list = new ArrayList<>(jArray.size()); - - for (int i = 0; i < count; i++) { - Object jTok = jArray.get(i); - RTObject runtimeObj = jTokenToRuntimeObject(jTok); - list.add(runtimeObj); - } - - return list; - } - - @SuppressWarnings("unchecked") - public static List jArrayToRuntimeObjList(List jArray) throws Exception { - return (List) jArrayToRuntimeObjList(jArray, false); - } - - public static void writeDictionaryRuntimeObjs(SimpleJson.Writer writer, HashMap dictionary) - throws Exception { - writer.writeObjectStart(); - for (Entry keyVal : dictionary.entrySet()) { - writer.writePropertyStart(keyVal.getKey()); - writeRuntimeObject(writer, keyVal.getValue()); - writer.writePropertyEnd(); - } - writer.writeObjectEnd(); - } - - public static void writeListRuntimeObjs(SimpleJson.Writer writer, List list) throws Exception { - writer.writeArrayStart(); - for (RTObject val : list) { - writeRuntimeObject(writer, val); - } - writer.writeArrayEnd(); - } - - public static void writeIntDictionary(SimpleJson.Writer writer, HashMap dict) throws Exception { - writer.writeObjectStart(); - - for (Entry keyVal : dict.entrySet()) - writer.writeProperty(keyVal.getKey(), keyVal.getValue()); - - writer.writeObjectEnd(); - } - - public static void writeRuntimeObject(SimpleJson.Writer writer, RTObject obj) throws Exception { - - if (obj instanceof Container) { - writeRuntimeContainer(writer, (Container) obj); - return; - } - - if (obj instanceof Divert) { - Divert divert = (Divert) obj; - String divTypeKey = "->"; - if (divert.isExternal()) - divTypeKey = "x()"; - else if (divert.getPushesToStack()) { - if (divert.getStackPushType() == PushPopType.Function) - divTypeKey = "f()"; - else if (divert.getStackPushType() == PushPopType.Tunnel) - divTypeKey = "->t->"; - } - - String targetStr; - if (divert.hasVariableTarget()) - targetStr = divert.getVariableDivertName(); - else - targetStr = divert.getTargetPathString(); - - writer.writeObjectStart(); - - writer.writeProperty(divTypeKey, targetStr); - - if (divert.hasVariableTarget()) - writer.writeProperty("var", true); - - if (divert.isConditional()) - writer.writeProperty("c", true); - - if (divert.getExternalArgs() > 0) - writer.writeProperty("exArgs", divert.getExternalArgs()); - - writer.writeObjectEnd(); - return; - } - - if (obj instanceof ChoicePoint) { - ChoicePoint choicePoint = (ChoicePoint) obj; - writer.writeObjectStart(); - writer.writeProperty("*", choicePoint.getPathStringOnChoice()); - writer.writeProperty("flg", choicePoint.getFlags()); - writer.writeObjectEnd(); - return; - } - - if (obj instanceof BoolValue) { - BoolValue boolVal = (BoolValue) obj; - writer.write(boolVal.value); - return; - } - - if (obj instanceof IntValue) { - IntValue intVal = (IntValue) obj; - writer.write(intVal.value); - return; - } - - if (obj instanceof FloatValue) { - FloatValue floatVal = (FloatValue) obj; - - writer.write(floatVal.value); - return; - } - - if (obj instanceof StringValue) { - StringValue strVal = (StringValue) obj; - if (strVal.isNewline()) - writer.write("\\n", false); - else { - writer.writeStringStart(); - writer.writeStringInner("^"); - writer.writeStringInner(strVal.value); - writer.writeStringEnd(); - } - return; - } - - if (obj instanceof ListValue) { - writeInkList(writer, (ListValue) obj); - return; - } - - if (obj instanceof DivertTargetValue) { - DivertTargetValue divTargetVal = (DivertTargetValue) obj; - writer.writeObjectStart(); - writer.writeProperty("^->", divTargetVal.value.getComponentsString()); - writer.writeObjectEnd(); - return; - } - - if (obj instanceof VariablePointerValue) { - VariablePointerValue varPtrVal = (VariablePointerValue) obj; - writer.writeObjectStart(); - writer.writeProperty("^var", varPtrVal.value); - writer.writeProperty("ci", varPtrVal.getContextIndex()); - writer.writeObjectEnd(); - return; - } - - if (obj instanceof Glue) { - writer.write("<>"); - return; - } - - if (obj instanceof ControlCommand) { - ControlCommand controlCmd = (ControlCommand) obj; - writer.write(controlCommandNames[controlCmd.getCommandType().ordinal()]); - return; - } - - if (obj instanceof NativeFunctionCall) { - NativeFunctionCall nativeFunc = (NativeFunctionCall) obj; - String name = nativeFunc.getName(); - - // Avoid collision with ^ used to indicate a string - if (name == "^") - name = "L^"; - - writer.write(name); - return; - } - - // Variable reference - if (obj instanceof VariableReference) { - VariableReference varRef = (VariableReference) obj; - writer.writeObjectStart(); - - String readCountPath = varRef.getPathStringForCount(); - if (readCountPath != null) { - writer.writeProperty("CNT?", readCountPath); - } else { - writer.writeProperty("VAR?", varRef.getName()); - } - - writer.writeObjectEnd(); - return; - } - - // Variable assignment - if (obj instanceof VariableAssignment) { - VariableAssignment varAss = (VariableAssignment) obj; - writer.writeObjectStart(); - - String key = varAss.isGlobal() ? "VAR=" : "temp="; - writer.writeProperty(key, varAss.getVariableName()); - - // Reassignment? - if (!varAss.isNewDeclaration()) - writer.writeProperty("re", true); - - writer.writeObjectEnd(); - - return; - } - - // Void - if (obj instanceof Void) { - writer.write("void"); - return; - } - - // Tag - if (obj instanceof Tag) { - Tag tag = (Tag) obj; - writer.writeObjectStart(); - writer.writeProperty("#", tag.getText()); - writer.writeObjectEnd(); - return; - } - - // Used when serialising save state only - - if (obj instanceof Choice) { - Choice choice = (Choice) obj; - writeChoice(writer, choice); - return; - } - - throw new Exception("Failed to write runtime object to JSON: " + obj); - } - - public static HashMap jObjectToHashMapRuntimeObjs(HashMap jRTObject) - throws Exception { - HashMap dict = new HashMap<>(jRTObject.size()); - - for (Entry keyVal : jRTObject.entrySet()) { - dict.put(keyVal.getKey(), jTokenToRuntimeObject(keyVal.getValue())); - } - - return dict; - } - - public static HashMap jObjectToIntHashMap(HashMap jRTObject) throws Exception { - HashMap dict = new HashMap<>(jRTObject.size()); - - for (Entry keyVal : jRTObject.entrySet()) { - dict.put(keyVal.getKey(), (Integer) keyVal.getValue()); - } - - 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"} - @SuppressWarnings("unchecked") - public static RTObject jTokenToRuntimeObject(Object token) throws Exception { - if (token instanceof Integer || token instanceof Float || token instanceof Boolean) { - return AbstractValue.create(token); - } - - if (token instanceof String) { - String str = (String) token; - // String value - char firstChar = str.charAt(0); - if (firstChar == '^') - return new StringValue(str.substring(1)); - else if (firstChar == '\n' && str.length() == 1) - return new StringValue("\n"); - - // Glue - if ("<>".equals(str)) - return new Glue(); - - for (int i = 0; i < controlCommandNames.length; ++i) { - // Control commands (would looking up in a hash set be faster?) - String cmdName = controlCommandNames[i]; - if (str.equals(cmdName)) { - return new ControlCommand(CommandType.values()[i + 1]); - } - - } - - // 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 ("L^".equals(str)) - str = "^"; - if (NativeFunctionCall.callExistsWithName(str)) - return NativeFunctionCall.callWithName(str); - - // Pop - if ("->->".equals(str)) - return ControlCommand.popTunnel(); - else if ("~ret".equals(str)) - return ControlCommand.popFunction(); - - // Void - if ("void".equals(str)) - return new Void(); - - } - - if (token instanceof HashMap) { - HashMap obj = (HashMap) token; - - Object propValue; - - // Divert target value to path - propValue = obj.get("^->"); - - if (propValue != null) { - return new DivertTargetValue(new Path((String) propValue)); - } - - // VariablePointerValue - propValue = obj.get("^var"); - if (propValue != null) { - VariablePointerValue varPtr = new VariablePointerValue((String) propValue); - - propValue = obj.get("ci"); - - if (propValue != null) - varPtr.setContextIndex((Integer) propValue); - - return varPtr; - } - - // Divert - boolean isDivert = false; - boolean pushesToStack = false; - PushPopType divPushType = PushPopType.Function; - boolean external = false; - - propValue = obj.get("->"); - if (propValue != null) { - isDivert = true; - } else { - propValue = obj.get("f()"); - if (propValue != null) { - isDivert = true; - pushesToStack = true; - divPushType = PushPopType.Function; - } else { - propValue = obj.get("->t->"); - if (propValue != null) { - isDivert = true; - pushesToStack = true; - divPushType = PushPopType.Tunnel; - } else { - propValue = obj.get("x()"); - if (propValue != null) { - isDivert = true; - external = true; - pushesToStack = false; - divPushType = PushPopType.Function; - } - - } - } - } - - if (isDivert) { - Divert divert = new Divert(); - divert.setPushesToStack(pushesToStack); - divert.setStackPushType(divPushType); - divert.setExternal(external); - String target = propValue.toString(); - - propValue = obj.get("var"); - if (propValue != null) { - divert.setVariableDivertName(target); - } else { - divert.setTargetPathString(target); - } - - propValue = obj.get("c"); - divert.setConditional(propValue != null); - - if (external) { - propValue = obj.get("exArgs"); - if (propValue != null) { - divert.setExternalArgs((Integer) propValue); - } - - } - - return divert; - } - - // Choice - propValue = obj.get("*"); - if (propValue != null) { - ChoicePoint choice = new ChoicePoint(); - choice.setPathStringOnChoice(propValue.toString()); - propValue = obj.get("flg"); - - if (propValue != null) { - choice.setFlags((Integer) propValue); - } - - return choice; - } - - // Variable reference - propValue = obj.get("VAR?"); - if (propValue != null) { - return new VariableReference(propValue.toString()); - } else { - propValue = obj.get("CNT?"); - if (propValue != null) { - VariableReference readCountVarRef = new VariableReference(); - readCountVarRef.setPathStringForCount(propValue.toString()); - return readCountVarRef; - } - - } - // Variable assignment - boolean isVarAss = false; - boolean isGlobalVar = false; - - propValue = obj.get("VAR="); - if (propValue != null) { - isVarAss = true; - isGlobalVar = true; - } else { - propValue = obj.get("temp="); - if (propValue != null) { - isVarAss = true; - isGlobalVar = false; - } - - } - if (isVarAss) { - String varName = propValue.toString(); - propValue = obj.get("re"); - boolean isNewDecl = propValue == null; - - VariableAssignment varAss = new VariableAssignment(varName, isNewDecl); - varAss.setIsGlobal(isGlobalVar); - return varAss; - } - - // Tag - propValue = obj.get("#"); - if (propValue != null) { - return new Tag((String) propValue); - } - - // List value - propValue = obj.get("list"); - - if (propValue != null) { - HashMap listContent = (HashMap) propValue; - InkList rawList = new InkList(); - - propValue = obj.get("origins"); - - if (propValue != null) { - List namesAsObjs = (List) propValue; - - rawList.setInitialOriginNames(namesAsObjs); - } - - for (Entry nameToVal : listContent.entrySet()) { - InkListItem item = new InkListItem(nameToVal.getKey()); - int val = (int) nameToVal.getValue(); - rawList.put(item, val); - } - - return new ListValue(rawList); - } - - // Used when serialising save state only - if (obj.get("originalChoicePath") != null) - return jObjectToChoice(obj); - - } - - // Array is always a Runtime.Container - if (token instanceof List) { - return jArrayToContainer((List) token); - } - - if (token == null) - return null; - - throw new Exception("Failed to convert token to runtime RTObject: " + token); - } - - public static void writeRuntimeContainer(SimpleJson.Writer writer, Container container) throws Exception { - writeRuntimeContainer(writer, container, false); - } - - public static void writeRuntimeContainer(SimpleJson.Writer writer, Container container, boolean withoutName) - throws Exception { - writer.writeArrayStart(); - - for (RTObject c : container.getContent()) - 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 - HashMap namedOnlyContent = container.getNamedOnlyContent(); - int countFlags = container.getCountFlags(); - boolean hasNameProperty = container.getName() != null && !withoutName; - - boolean hasTerminator = namedOnlyContent != null || countFlags > 0 || hasNameProperty; - - if (hasTerminator) - writer.writeObjectStart(); - - if (namedOnlyContent != null) { - - for (Entry namedContent : namedOnlyContent.entrySet()) { - String name = namedContent.getKey(); - Container namedContainer = namedContent.getValue() instanceof Container - ? (Container) namedContent.getValue() - : null; - - writer.writePropertyStart(name); - writeRuntimeContainer(writer, namedContainer, true); - writer.writePropertyEnd(); - } - } - - if (countFlags > 0) - writer.writeProperty("#f", countFlags); - - if (hasNameProperty) - writer.writeProperty("#n", container.getName()); - - if (hasTerminator) - writer.writeObjectEnd(); - else - writer.writeNull(); - - writer.writeArrayEnd(); - - } - - @SuppressWarnings("unchecked") - static Container jArrayToContainer(List jArray) throws Exception { - Container container = new Container(); - container.setContent(jArrayToRuntimeObjList(jArray, true)); - // Final RTObject in the array is always a combination of - // - named content - // - a "#" key with the countFlags - // (if either exists at all, otherwise null) - HashMap terminatingObj = (HashMap) jArray.get(jArray.size() - 1); - if (terminatingObj != null) { - HashMap namedOnlyContent = new HashMap<>(terminatingObj.size()); - for (Entry keyVal : terminatingObj.entrySet()) { - if ("#f".equals(keyVal.getKey())) { - container.setCountFlags((int) keyVal.getValue()); - } else if ("#n".equals(keyVal.getKey())) { - container.setName(keyVal.getValue().toString()); - } else { - RTObject namedContentItem = jTokenToRuntimeObject(keyVal.getValue()); - Container namedSubContainer = namedContentItem instanceof Container ? (Container) namedContentItem - : (Container) null; - if (namedSubContainer != null) - namedSubContainer.setName(keyVal.getKey()); - - namedOnlyContent.put(keyVal.getKey(), namedContentItem); - } - } - container.setNamedOnlyContent(namedOnlyContent); - } - - return container; - } - - static Choice jObjectToChoice(HashMap jObj) throws Exception { - Choice choice = new Choice(); - choice.setText(jObj.get("text").toString()); - choice.setIndex((int) jObj.get("index")); - choice.sourcePath = jObj.get("originalChoicePath").toString(); - choice.originalThreadIndex = (int) jObj.get("originalThreadIndex"); - choice.setPathStringOnChoice(jObj.get("targetPath").toString()); - return choice; - } - - public static void writeChoice(SimpleJson.Writer writer, Choice choice) throws Exception { - writer.writeObjectStart(); - writer.writeProperty("text", choice.getText()); - writer.writeProperty("index", choice.getIndex()); - writer.writeProperty("originalChoicePath", choice.sourcePath); - writer.writeProperty("originalThreadIndex", choice.originalThreadIndex); - writer.writeProperty("targetPath", choice.getPathStringOnChoice()); - writer.writeObjectEnd(); - } - - static void writeInkList(SimpleJson.Writer writer, ListValue listVal) throws Exception { - InkList rawList = listVal.getValue(); - - writer.writeObjectStart(); - - writer.writePropertyStart("list"); - - writer.writeObjectStart(); - - for (Entry itemAndValue : rawList.entrySet()) { - InkListItem item = itemAndValue.getKey(); - int itemVal = itemAndValue.getValue(); - - writer.writePropertyNameStart(); - writer.writePropertyNameInner(item.getOriginName() != null ? item.getOriginName() : "?"); - writer.writePropertyNameInner("."); - writer.writePropertyNameInner(item.getItemName()); - writer.writePropertyNameEnd(); - - writer.write(itemVal); - - writer.writePropertyEnd(); - } - - writer.writeObjectEnd(); - - writer.writePropertyEnd(); - - if (rawList.size() == 0 && rawList.getOriginNames() != null && rawList.getOriginNames().size() > 0) { - writer.writePropertyStart("origins"); - writer.writeArrayStart(); - for (String name : rawList.getOriginNames()) - writer.write(name); - writer.writeArrayEnd(); - writer.writePropertyEnd(); - } - - writer.writeObjectEnd(); - } - - public static HashMap listDefinitionsToJToken(ListDefinitionsOrigin origin) { - HashMap result = new HashMap<>(); - for (ListDefinition def : origin.getLists()) { - HashMap listDefJson = new HashMap<>(); - for (Entry itemToVal : def.getItems().entrySet()) { - InkListItem item = itemToVal.getKey(); - int val = itemToVal.getValue(); - listDefJson.put(item.getItemName(), val); - } - result.put(def.getName(), listDefJson); - } - return result; - } - - @SuppressWarnings("unchecked") - public static ListDefinitionsOrigin jTokenToListDefinitions(Object obj) { - HashMap defsObj = (HashMap) obj; - - List allDefs = new ArrayList<>(); - - for (Entry kv : defsObj.entrySet()) { - String name = kv.getKey(); - HashMap listDefJson = (HashMap) kv.getValue(); - - // Cast (string, object) to (string, int) for items - HashMap items = new HashMap<>(); - for (Entry nameValue : listDefJson.entrySet()) - items.put(nameValue.getKey(), (int) nameValue.getValue()); - - ListDefinition def = new ListDefinition(name, items); - allDefs.add(def); - } - - return new ListDefinitionsOrigin(allDefs); - } - - private final static String[] controlCommandNames; - - static { - controlCommandNames = new String[CommandType.values().length - 1]; - controlCommandNames[CommandType.EvalStart.ordinal() - 1] = "ev"; - controlCommandNames[CommandType.EvalOutput.ordinal() - 1] = "out"; - controlCommandNames[CommandType.EvalEnd.ordinal() - 1] = "/ev"; - controlCommandNames[CommandType.Duplicate.ordinal() - 1] = "du"; - controlCommandNames[CommandType.PopEvaluatedValue.ordinal() - 1] = "pop"; - controlCommandNames[CommandType.PopFunction.ordinal() - 1] = "~ret"; - controlCommandNames[CommandType.PopTunnel.ordinal() - 1] = "->->"; - controlCommandNames[CommandType.BeginString.ordinal() - 1] = "str"; - controlCommandNames[CommandType.EndString.ordinal() - 1] = "/str"; - controlCommandNames[CommandType.NoOp.ordinal() - 1] = "nop"; - controlCommandNames[CommandType.ChoiceCount.ordinal() - 1] = "choiceCnt"; - controlCommandNames[CommandType.Turns.ordinal() - 1] = "turn"; - controlCommandNames[CommandType.TurnsSince.ordinal() - 1] = "turns"; - controlCommandNames[CommandType.ReadCount.ordinal() - 1] = "readc"; - controlCommandNames[CommandType.Random.ordinal() - 1] = "rnd"; - controlCommandNames[CommandType.SeedRandom.ordinal() - 1] = "srnd"; - controlCommandNames[CommandType.VisitIndex.ordinal() - 1] = "visit"; - controlCommandNames[CommandType.SequenceShuffleIndex.ordinal() - 1] = "seq"; - controlCommandNames[CommandType.StartThread.ordinal() - 1] = "thread"; - controlCommandNames[CommandType.Done.ordinal() - 1] = "done"; - controlCommandNames[CommandType.End.ordinal() - 1] = "end"; - controlCommandNames[CommandType.ListFromInt.ordinal() - 1] = "listInt"; - controlCommandNames[CommandType.ListRange.ordinal() - 1] = "range"; - controlCommandNames[CommandType.ListRandom.ordinal() - 1] = "lrnd"; - - for (int i = 0; i < CommandType.values().length - 1; ++i) { - if (controlCommandNames[i] == null) - throw new ExceptionInInitializerError("Control command not accounted for in serialisation"); - } - - } + public static List jArrayToRuntimeObjList(List jArray, boolean skipLast) throws Exception { + int count = jArray.size(); + + if (skipLast) + count--; + + List list = new ArrayList<>(jArray.size()); + + for (int i = 0; i < count; i++) { + Object jTok = jArray.get(i); + RTObject runtimeObj = jTokenToRuntimeObject(jTok); + list.add(runtimeObj); + } + + return list; + } + + @SuppressWarnings("unchecked") + public static List jArrayToRuntimeObjList(List jArray) throws Exception { + return (List) jArrayToRuntimeObjList(jArray, false); + } + + public static void writeDictionaryRuntimeObjs(SimpleJson.Writer writer, HashMap dictionary) + throws Exception { + writer.writeObjectStart(); + for (Entry keyVal : dictionary.entrySet()) { + writer.writePropertyStart(keyVal.getKey()); + writeRuntimeObject(writer, keyVal.getValue()); + writer.writePropertyEnd(); + } + writer.writeObjectEnd(); + } + + public static void writeListRuntimeObjs(SimpleJson.Writer writer, List list) throws Exception { + writer.writeArrayStart(); + for (RTObject val : list) { + writeRuntimeObject(writer, val); + } + writer.writeArrayEnd(); + } + + public static void writeIntDictionary(SimpleJson.Writer writer, HashMap dict) throws Exception { + writer.writeObjectStart(); + + for (Entry keyVal : dict.entrySet()) + writer.writeProperty(keyVal.getKey(), keyVal.getValue()); + + writer.writeObjectEnd(); + } + + public static void writeRuntimeObject(SimpleJson.Writer writer, RTObject obj) throws Exception { + + if (obj instanceof Container) { + writeRuntimeContainer(writer, (Container) obj); + return; + } + + if (obj instanceof Divert) { + Divert divert = (Divert) obj; + String divTypeKey = "->"; + if (divert.isExternal()) + divTypeKey = "x()"; + else if (divert.getPushesToStack()) { + if (divert.getStackPushType() == PushPopType.Function) + divTypeKey = "f()"; + else if (divert.getStackPushType() == PushPopType.Tunnel) + divTypeKey = "->t->"; + } + + String targetStr; + if (divert.hasVariableTarget()) + targetStr = divert.getVariableDivertName(); + else + targetStr = divert.getTargetPathString(); + + writer.writeObjectStart(); + + writer.writeProperty(divTypeKey, targetStr); + + if (divert.hasVariableTarget()) + writer.writeProperty("var", true); + + if (divert.isConditional()) + writer.writeProperty("c", true); + + if (divert.getExternalArgs() > 0) + writer.writeProperty("exArgs", divert.getExternalArgs()); + + writer.writeObjectEnd(); + return; + } + + if (obj instanceof ChoicePoint) { + ChoicePoint choicePoint = (ChoicePoint) obj; + writer.writeObjectStart(); + writer.writeProperty("*", choicePoint.getPathStringOnChoice()); + writer.writeProperty("flg", choicePoint.getFlags()); + writer.writeObjectEnd(); + return; + } + + if (obj instanceof BoolValue) { + BoolValue boolVal = (BoolValue) obj; + writer.write(boolVal.value); + return; + } + + if (obj instanceof IntValue) { + IntValue intVal = (IntValue) obj; + writer.write(intVal.value); + return; + } + + if (obj instanceof FloatValue) { + FloatValue floatVal = (FloatValue) obj; + + writer.write(floatVal.value); + return; + } + + if (obj instanceof StringValue) { + StringValue strVal = (StringValue) obj; + if (strVal.isNewline()) + writer.write("\\n", false); + else { + writer.writeStringStart(); + writer.writeStringInner("^"); + writer.writeStringInner(strVal.value); + writer.writeStringEnd(); + } + return; + } + + if (obj instanceof ListValue) { + writeInkList(writer, (ListValue) obj); + return; + } + + if (obj instanceof DivertTargetValue) { + DivertTargetValue divTargetVal = (DivertTargetValue) obj; + writer.writeObjectStart(); + writer.writeProperty("^->", divTargetVal.value.getComponentsString()); + writer.writeObjectEnd(); + return; + } + + if (obj instanceof VariablePointerValue) { + VariablePointerValue varPtrVal = (VariablePointerValue) obj; + writer.writeObjectStart(); + writer.writeProperty("^var", varPtrVal.value); + writer.writeProperty("ci", varPtrVal.getContextIndex()); + writer.writeObjectEnd(); + return; + } + + if (obj instanceof Glue) { + writer.write("<>"); + return; + } + + if (obj instanceof ControlCommand) { + ControlCommand controlCmd = (ControlCommand) obj; + writer.write(controlCommandNames[controlCmd.getCommandType().ordinal()]); + return; + } + + if (obj instanceof NativeFunctionCall) { + NativeFunctionCall nativeFunc = (NativeFunctionCall) obj; + String name = nativeFunc.getName(); + + // Avoid collision with ^ used to indicate a string + if ("^".equals(name)) + name = "L^"; + + writer.write(name); + return; + } + + // Variable reference + if (obj instanceof VariableReference) { + VariableReference varRef = (VariableReference) obj; + writer.writeObjectStart(); + + String readCountPath = varRef.getPathStringForCount(); + if (readCountPath != null) { + writer.writeProperty("CNT?", readCountPath); + } else { + writer.writeProperty("VAR?", varRef.getName()); + } + + writer.writeObjectEnd(); + return; + } + + // Variable assignment + if (obj instanceof VariableAssignment) { + VariableAssignment varAss = (VariableAssignment) obj; + writer.writeObjectStart(); + + String key = varAss.isGlobal() ? "VAR=" : "temp="; + writer.writeProperty(key, varAss.getVariableName()); + + // Reassignment? + if (!varAss.isNewDeclaration()) + writer.writeProperty("re", true); + + writer.writeObjectEnd(); + + return; + } + + // Void + if (obj instanceof Void) { + writer.write("void"); + return; + } + + // Legacy tag + if (obj instanceof Tag) { + Tag tag = (Tag) obj; + writer.writeObjectStart(); + writer.writeProperty("#", tag.getText()); + writer.writeObjectEnd(); + return; + } + + // Used when serialising save state only + + if (obj instanceof Choice) { + Choice choice = (Choice) obj; + writeChoice(writer, choice); + return; + } + + throw new Exception("Failed to write runtime object to JSON: " + obj); + } + + public static HashMap jObjectToHashMapRuntimeObjs(HashMap jRTObject) + throws Exception { + HashMap dict = new HashMap<>(jRTObject.size()); + + for (Entry keyVal : jRTObject.entrySet()) { + dict.put(keyVal.getKey(), jTokenToRuntimeObject(keyVal.getValue())); + } + + return dict; + } + + public static HashMap jObjectToIntHashMap(HashMap jRTObject) throws Exception { + HashMap dict = new HashMap<>(jRTObject.size()); + + for (Entry keyVal : jRTObject.entrySet()) { + dict.put(keyVal.getKey(), (Integer) keyVal.getValue()); + } + + 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"} + @SuppressWarnings("unchecked") + public static RTObject jTokenToRuntimeObject(Object token) throws Exception { + if (token instanceof Integer || token instanceof Float || token instanceof Boolean) { + return AbstractValue.create(token); + } + + if (token instanceof String) { + String str = (String) token; + // String value + char firstChar = str.charAt(0); + if (firstChar == '^') + return new StringValue(str.substring(1)); + else if (firstChar == '\n' && str.length() == 1) + return new StringValue("\n"); + + // Glue + if ("<>".equals(str)) + return new Glue(); + + for (int i = 0; i < controlCommandNames.length; ++i) { + // Control commands (would looking up in a hash set be faster?) + String cmdName = controlCommandNames[i]; + if (str.equals(cmdName)) { + return new ControlCommand(CommandType.values()[i + 1]); + } + + } + + // 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 ("L^".equals(str)) + str = "^"; + if (NativeFunctionCall.callExistsWithName(str)) + return NativeFunctionCall.callWithName(str); + + // Pop + if ("->->".equals(str)) + return ControlCommand.popTunnel(); + else if ("~ret".equals(str)) + return ControlCommand.popFunction(); + + // Void + if ("void".equals(str)) + return new Void(); + + } + + if (token instanceof HashMap) { + HashMap obj = (HashMap) token; + + Object propValue; + + // Divert target value to path + propValue = obj.get("^->"); + + if (propValue != null) { + return new DivertTargetValue(new Path((String) propValue)); + } + + // VariablePointerValue + propValue = obj.get("^var"); + if (propValue != null) { + VariablePointerValue varPtr = new VariablePointerValue((String) propValue); + + propValue = obj.get("ci"); + + if (propValue != null) + varPtr.setContextIndex((Integer) propValue); + + return varPtr; + } + + // Divert + boolean isDivert = false; + boolean pushesToStack = false; + PushPopType divPushType = PushPopType.Function; + boolean external = false; + + propValue = obj.get("->"); + if (propValue != null) { + isDivert = true; + } else { + propValue = obj.get("f()"); + if (propValue != null) { + isDivert = true; + pushesToStack = true; + divPushType = PushPopType.Function; + } else { + propValue = obj.get("->t->"); + if (propValue != null) { + isDivert = true; + pushesToStack = true; + divPushType = PushPopType.Tunnel; + } else { + propValue = obj.get("x()"); + if (propValue != null) { + isDivert = true; + external = true; + pushesToStack = false; + divPushType = PushPopType.Function; + } + + } + } + } + + if (isDivert) { + Divert divert = new Divert(); + divert.setPushesToStack(pushesToStack); + divert.setStackPushType(divPushType); + divert.setExternal(external); + String target = propValue.toString(); + + propValue = obj.get("var"); + if (propValue != null) { + divert.setVariableDivertName(target); + } else { + divert.setTargetPathString(target); + } + + propValue = obj.get("c"); + divert.setConditional(propValue != null); + + if (external) { + propValue = obj.get("exArgs"); + if (propValue != null) { + divert.setExternalArgs((Integer) propValue); + } + + } + + return divert; + } + + // Choice + propValue = obj.get("*"); + if (propValue != null) { + ChoicePoint choice = new ChoicePoint(); + choice.setPathStringOnChoice(propValue.toString()); + propValue = obj.get("flg"); + + if (propValue != null) { + choice.setFlags((Integer) propValue); + } + + return choice; + } + + // Variable reference + propValue = obj.get("VAR?"); + if (propValue != null) { + return new VariableReference(propValue.toString()); + } else { + propValue = obj.get("CNT?"); + if (propValue != null) { + VariableReference readCountVarRef = new VariableReference(); + readCountVarRef.setPathStringForCount(propValue.toString()); + return readCountVarRef; + } + + } + // Variable assignment + boolean isVarAss = false; + boolean isGlobalVar = false; + + propValue = obj.get("VAR="); + if (propValue != null) { + isVarAss = true; + isGlobalVar = true; + } else { + propValue = obj.get("temp="); + if (propValue != null) { + isVarAss = true; + isGlobalVar = false; + } + + } + if (isVarAss) { + String varName = propValue.toString(); + propValue = obj.get("re"); + boolean isNewDecl = propValue == null; + + VariableAssignment varAss = new VariableAssignment(varName, isNewDecl); + varAss.setIsGlobal(isGlobalVar); + return varAss; + } + + // Legacy Tag + propValue = obj.get("#"); + if (propValue != null) { + return new Tag((String) propValue); + } + + // List value + propValue = obj.get("list"); + + if (propValue != null) { + HashMap listContent = (HashMap) propValue; + InkList rawList = new InkList(); + + propValue = obj.get("origins"); + + if (propValue != null) { + List namesAsObjs = (List) propValue; + + rawList.setInitialOriginNames(namesAsObjs); + } + + for (Entry nameToVal : listContent.entrySet()) { + InkListItem item = new InkListItem(nameToVal.getKey()); + int val = (int) nameToVal.getValue(); + rawList.put(item, val); + } + + return new ListValue(rawList); + } + + // Used when serialising save state only + if (obj.get("originalChoicePath") != null) + return jObjectToChoice(obj); + + } + + // Array is always a Runtime.Container + if (token instanceof List) { + return jArrayToContainer((List) token); + } + + if (token == null) + return null; + + throw new Exception("Failed to convert token to runtime RTObject: " + token); + } + + public static void writeRuntimeContainer(SimpleJson.Writer writer, Container container) throws Exception { + writeRuntimeContainer(writer, container, false); + } + + public static void writeRuntimeContainer(SimpleJson.Writer writer, Container container, boolean withoutName) + throws Exception { + writer.writeArrayStart(); + + for (RTObject c : container.getContent()) + 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 + HashMap namedOnlyContent = container.getNamedOnlyContent(); + int countFlags = container.getCountFlags(); + boolean hasNameProperty = container.getName() != null && !withoutName; + + boolean hasTerminator = namedOnlyContent != null || countFlags > 0 || hasNameProperty; + + if (hasTerminator) + writer.writeObjectStart(); + + if (namedOnlyContent != null) { + + for (Entry namedContent : namedOnlyContent.entrySet()) { + String name = namedContent.getKey(); + Container namedContainer = namedContent.getValue() instanceof Container + ? (Container) namedContent.getValue() + : null; + + writer.writePropertyStart(name); + writeRuntimeContainer(writer, namedContainer, true); + writer.writePropertyEnd(); + } + } + + if (countFlags > 0) + writer.writeProperty("#f", countFlags); + + if (hasNameProperty) + writer.writeProperty("#n", container.getName()); + + if (hasTerminator) + writer.writeObjectEnd(); + else + writer.writeNull(); + + writer.writeArrayEnd(); + + } + + @SuppressWarnings("unchecked") + static Container jArrayToContainer(List jArray) throws Exception { + Container container = new Container(); + container.setContent(jArrayToRuntimeObjList(jArray, true)); + // Final RTObject in the array is always a combination of + // - named content + // - a "#" key with the countFlags + // (if either exists at all, otherwise null) + HashMap terminatingObj = (HashMap) jArray.get(jArray.size() - 1); + if (terminatingObj != null) { + HashMap namedOnlyContent = new HashMap<>(terminatingObj.size()); + for (Entry keyVal : terminatingObj.entrySet()) { + if ("#f".equals(keyVal.getKey())) { + container.setCountFlags((int) keyVal.getValue()); + } else if ("#n".equals(keyVal.getKey())) { + container.setName(keyVal.getValue().toString()); + } else { + RTObject namedContentItem = jTokenToRuntimeObject(keyVal.getValue()); + Container namedSubContainer = namedContentItem instanceof Container ? (Container) namedContentItem + : (Container) null; + if (namedSubContainer != null) + namedSubContainer.setName(keyVal.getKey()); + + namedOnlyContent.put(keyVal.getKey(), namedContentItem); + } + } + container.setNamedOnlyContent(namedOnlyContent); + } + + return container; + } + + static Choice jObjectToChoice(HashMap jObj) throws Exception { + Choice choice = new Choice(); + choice.setText(jObj.get("text").toString()); + choice.setIndex((int) jObj.get("index")); + choice.sourcePath = jObj.get("originalChoicePath").toString(); + choice.originalThreadIndex = (int) jObj.get("originalThreadIndex"); + choice.setPathStringOnChoice(jObj.get("targetPath").toString()); + return choice; + } + + public static void writeChoice(SimpleJson.Writer writer, Choice choice) throws Exception { + writer.writeObjectStart(); + writer.writeProperty("text", choice.getText()); + writer.writeProperty("index", choice.getIndex()); + writer.writeProperty("originalChoicePath", choice.sourcePath); + writer.writeProperty("originalThreadIndex", choice.originalThreadIndex); + writer.writeProperty("targetPath", choice.getPathStringOnChoice()); + writer.writeObjectEnd(); + } + + static void writeInkList(SimpleJson.Writer writer, ListValue listVal) throws Exception { + InkList rawList = listVal.getValue(); + + writer.writeObjectStart(); + + writer.writePropertyStart("list"); + + writer.writeObjectStart(); + + for (Entry itemAndValue : rawList.entrySet()) { + InkListItem item = itemAndValue.getKey(); + int itemVal = itemAndValue.getValue(); + + writer.writePropertyNameStart(); + writer.writePropertyNameInner(item.getOriginName() != null ? item.getOriginName() : "?"); + writer.writePropertyNameInner("."); + writer.writePropertyNameInner(item.getItemName()); + writer.writePropertyNameEnd(); + + writer.write(itemVal); + + writer.writePropertyEnd(); + } + + writer.writeObjectEnd(); + + writer.writePropertyEnd(); + + if (rawList.size() == 0 && rawList.getOriginNames() != null && rawList.getOriginNames().size() > 0) { + writer.writePropertyStart("origins"); + writer.writeArrayStart(); + for (String name : rawList.getOriginNames()) + writer.write(name); + writer.writeArrayEnd(); + writer.writePropertyEnd(); + } + + writer.writeObjectEnd(); + } + + public static HashMap listDefinitionsToJToken(ListDefinitionsOrigin origin) { + HashMap result = new HashMap<>(); + for (ListDefinition def : origin.getLists()) { + HashMap listDefJson = new HashMap<>(); + for (Entry itemToVal : def.getItems().entrySet()) { + InkListItem item = itemToVal.getKey(); + int val = itemToVal.getValue(); + listDefJson.put(item.getItemName(), val); + } + result.put(def.getName(), listDefJson); + } + return result; + } + + @SuppressWarnings("unchecked") + public static ListDefinitionsOrigin jTokenToListDefinitions(Object obj) { + HashMap defsObj = (HashMap) obj; + + List allDefs = new ArrayList<>(); + + for (Entry kv : defsObj.entrySet()) { + String name = kv.getKey(); + HashMap listDefJson = (HashMap) kv.getValue(); + + // Cast (string, object) to (string, int) for items + HashMap items = new HashMap<>(); + for (Entry nameValue : listDefJson.entrySet()) + items.put(nameValue.getKey(), (int) nameValue.getValue()); + + ListDefinition def = new ListDefinition(name, items); + allDefs.add(def); + } + + return new ListDefinitionsOrigin(allDefs); + } + + private final static String[] controlCommandNames; + + static { + controlCommandNames = new String[CommandType.values().length - 1]; + controlCommandNames[CommandType.EvalStart.ordinal() - 1] = "ev"; + controlCommandNames[CommandType.EvalOutput.ordinal() - 1] = "out"; + controlCommandNames[CommandType.EvalEnd.ordinal() - 1] = "/ev"; + controlCommandNames[CommandType.Duplicate.ordinal() - 1] = "du"; + controlCommandNames[CommandType.PopEvaluatedValue.ordinal() - 1] = "pop"; + controlCommandNames[CommandType.PopFunction.ordinal() - 1] = "~ret"; + controlCommandNames[CommandType.PopTunnel.ordinal() - 1] = "->->"; + controlCommandNames[CommandType.BeginString.ordinal() - 1] = "str"; + controlCommandNames[CommandType.EndString.ordinal() - 1] = "/str"; + controlCommandNames[CommandType.NoOp.ordinal() - 1] = "nop"; + controlCommandNames[CommandType.ChoiceCount.ordinal() - 1] = "choiceCnt"; + controlCommandNames[CommandType.Turns.ordinal() - 1] = "turn"; + controlCommandNames[CommandType.TurnsSince.ordinal() - 1] = "turns"; + controlCommandNames[CommandType.ReadCount.ordinal() - 1] = "readc"; + controlCommandNames[CommandType.Random.ordinal() - 1] = "rnd"; + controlCommandNames[CommandType.SeedRandom.ordinal() - 1] = "srnd"; + controlCommandNames[CommandType.VisitIndex.ordinal() - 1] = "visit"; + controlCommandNames[CommandType.SequenceShuffleIndex.ordinal() - 1] = "seq"; + controlCommandNames[CommandType.StartThread.ordinal() - 1] = "thread"; + controlCommandNames[CommandType.Done.ordinal() - 1] = "done"; + controlCommandNames[CommandType.End.ordinal() - 1] = "end"; + controlCommandNames[CommandType.ListFromInt.ordinal() - 1] = "listInt"; + controlCommandNames[CommandType.ListRange.ordinal() - 1] = "range"; + controlCommandNames[CommandType.ListRandom.ordinal() - 1] = "lrnd"; + controlCommandNames[CommandType.BeginTag.ordinal() - 1] = "#"; + controlCommandNames[CommandType.EndTag.ordinal() - 1] = "/#"; + + for (int i = 0; i < CommandType.values().length - 1; ++i) { + if (controlCommandNames[i] == null) + throw new ExceptionInInitializerError("Control command not accounted for in serialisation"); + } + + } } diff --git a/src/main/java/com/bladecoder/ink/runtime/Story.java b/src/main/java/com/bladecoder/ink/runtime/Story.java index 940be1d..fa1f59b 100644 --- a/src/main/java/com/bladecoder/ink/runtime/Story.java +++ b/src/main/java/com/bladecoder/ink/runtime/Story.java @@ -1,5 +1,9 @@ package com.bladecoder.ink.runtime; +import com.bladecoder.ink.runtime.Error.ErrorType; +import com.bladecoder.ink.runtime.SimpleJson.InnerWriter; +import com.bladecoder.ink.runtime.SimpleJson.Writer; + import java.io.OutputStream; import java.util.ArrayList; import java.util.Collections; @@ -12,2777 +16,2906 @@ import java.util.Random; import java.util.Stack; -import com.bladecoder.ink.runtime.Error.ErrorType; -import com.bladecoder.ink.runtime.SimpleJson.InnerWriter; -import com.bladecoder.ink.runtime.SimpleJson.Writer; - /** * A Story is the core class that represents a complete Ink narrative, and * manages the evaluation and state of it. */ public class Story extends RTObject implements VariablesState.VariableChanged { - /** - * General purpose delegate definition for bound EXTERNAL function definitions - * from ink. Note that this version isn't necessary if you have a function with - * three arguments or less. - * - * @param the result type - * @see ExternalFunction0 - * @see ExternalFunction1 - * @see ExternalFunction2 - * @see ExternalFunction3 - */ - public interface ExternalFunction { - R call(Object... args) throws Exception; - } - - /** - * EXTERNAL function delegate with zero arguments. - * - * @param the result type - */ - public abstract static class ExternalFunction0 implements ExternalFunction { - @Override - public final R call(Object... args) throws Exception { - if (args.length != 0) { - throw new IllegalArgumentException("Expecting 0 arguments."); - } - return call(); - } - - protected abstract R call() throws Exception; - } - - /** - * EXTERNAL function delegate with one argument. - * - * @param the argument type - * @param the result type - */ - public abstract static class ExternalFunction1 implements ExternalFunction { - @Override - public final R call(Object... args) throws Exception { - if (args.length != 1) { - throw new IllegalArgumentException("Expecting 1 argument."); - } - return call(coerceArg(args[0])); - } - - protected abstract R call(T t) throws Exception; - - @SuppressWarnings("unchecked") - protected T coerceArg(Object arg) throws Exception { - return (T) arg; - } - } - - /** - * EXTERNAL function delegate with two arguments. - * - * @param the first argument type - * @param the second argument type - * @param the result type - */ - public abstract static class ExternalFunction2 implements ExternalFunction { - @Override - public final R call(Object... args) throws Exception { - if (args.length != 2) { - throw new IllegalArgumentException("Expecting 2 arguments."); - } - return call(coerceArg0(args[0]), coerceArg1(args[1])); - } - - protected abstract R call(T1 t1, T2 t2) throws Exception; - - @SuppressWarnings("unchecked") - protected T1 coerceArg0(Object arg) throws Exception { - return (T1) arg; - } - - @SuppressWarnings("unchecked") - protected T2 coerceArg1(Object arg) throws Exception { - return (T2) arg; - } - } - - /** - * EXTERNAL function delegate with three arguments. - * - * @param the first argument type - * @param the second argument type - * @param the third argument type - * @param the result type - */ - public abstract static class ExternalFunction3 implements ExternalFunction { - @Override - public final R call(Object... args) throws Exception { - if (args.length != 3) { - throw new IllegalArgumentException("Expecting 3 arguments."); - } - return call(coerceArg0(args[0]), coerceArg1(args[1]), coerceArg2(args[2])); - } - - protected abstract R call(T1 t1, T2 t2, T3 t3) throws Exception; - - @SuppressWarnings("unchecked") - protected T1 coerceArg0(Object arg) throws Exception { - return (T1) arg; - } - - @SuppressWarnings("unchecked") - protected T2 coerceArg1(Object arg) throws Exception { - return (T2) arg; - } - - @SuppressWarnings("unchecked") - protected T3 coerceArg2(Object arg) throws Exception { - return (T3) arg; - } - } - - class ExternalFunctionDef { - public ExternalFunction function; - public boolean lookaheadSafe; - } - - // Version numbers are for engine itself and story file, rather - // than the story state save format - // -- old engine, new format: always fail - // -- new engine, old format: possibly cope, based on this number - // When incrementing the version number above, the question you - // should ask yourself is: - // -- Will the engine be able to load an old story file from - // before I made these changes to the engine? - // If possible, you should support it, though it's not as - // critical as loading old save games, since it's an - // in-development problem only. - - /** - * Delegate definition for variable observation - see ObserveVariable. - */ - public interface VariableObserver { - void call(String variableName, Object newValue); - } - - /** - * The current version of the ink story file format. - */ - public static final int inkVersionCurrent = 20; - - /** - * The minimum legacy version of ink that can be loaded by the current version - * of the code. - */ - public static final int inkVersionMinimumCompatible = 18; - - private Container mainContentContainer; - private ListDefinitionsOrigin listDefinitions; - - /** - * An ink file can provide a fallback functions for when when an EXTERNAL has - * been left unbound by the client, and the fallback function will be called - * instead. Useful when testing a story in playmode, when it's not possible to - * write a client-side C# external function, but you don't want it to fail to - * run. - */ - private boolean allowExternalFunctionFallbacks; - - private HashMap externals; - - private boolean hasValidatedExternals; - - private StoryState state; - - private Container temporaryEvaluationContainer; - - private HashMap> variableObservers; - - private List prevContainers = new ArrayList<>(); - - private Profiler profiler; - - private boolean asyncContinueActive; - private StoryState stateSnapshotAtLastNewline = null; - - private int recursiveContinueCount = 0; - - private boolean asyncSaving; - - private boolean sawLookaheadUnsafeFunctionAfterNewline = false; - - public Error.ErrorHandler onError = null; - - // Warning: When creating a Story using this constructor, you need to - // call ResetState on it before use. Intended for compiler use only. - // For normal use, use the constructor that takes a json string. - public Story(Container contentContainer, List lists) { - mainContentContainer = contentContainer; - - if (lists != null) { - listDefinitions = new ListDefinitionsOrigin(lists); - } - - externals = new HashMap<>(); - } - - public Story(Container contentContainer) { - this(contentContainer, null); - } - - /** - * Construct a Story Object using a JSON String compiled through inklecate. - */ - public Story(String jsonString) throws Exception { - this((Container) null); - HashMap rootObject = SimpleJson.textToDictionary(jsonString); - - Object versionObj = rootObject.get("inkVersion"); - if (versionObj == null) - throw new Exception("ink version number not found. Are you sure it's a valid .ink.json file?"); - - int formatFromFile = versionObj instanceof String ? Integer.parseInt((String) versionObj) : (int) versionObj; - - if (formatFromFile > inkVersionCurrent) { - throw new Exception("Version of ink used to build story was newer than the current version of the engine"); - } else if (formatFromFile < inkVersionMinimumCompatible) { - throw new Exception( - "Version of ink used to build story is too old to be loaded by this version of the engine"); - } else if (formatFromFile != inkVersionCurrent) { - System.out.println( - "WARNING: Version of ink used to build story doesn't match current version of engine. Non-critical, but recommend synchronising."); - } - - Object rootToken = rootObject.get("root"); - if (rootToken == null) - throw new Exception("Root node for ink not found. Are you sure it's a valid .ink.json file?"); - - Object listDefsObj = rootObject.get("listDefs"); - if (listDefsObj != null) { - listDefinitions = Json.jTokenToListDefinitions(listDefsObj); - } - - RTObject runtimeObject = Json.jTokenToRuntimeObject(rootToken); - mainContentContainer = runtimeObject instanceof Container ? (Container) runtimeObject : null; - - resetState(); - } - - void addError(String message) throws Exception { - addError(message, false, false); - } - - void warning(String message) throws Exception { - addError(message, true, false); - } - - void addError(String message, boolean isWarning, boolean useEndLineNumber) throws Exception { - DebugMetadata dm = currentDebugMetadata(); - - String errorTypeStr = isWarning ? "WARNING" : "ERROR"; - - if (dm != null) { - int lineNum = useEndLineNumber ? dm.endLineNumber : dm.startLineNumber; - message = String.format("RUNTIME %s: '%s' line %d: %s", errorTypeStr, dm.fileName, lineNum, message); - } else if (!state.getCurrentPointer().isNull()) { - message = String.format("RUNTIME %s: (%s): %s", errorTypeStr, - state.getCurrentPointer().getPath().toString(), message); - } else { - message = "RUNTIME " + errorTypeStr + ": " + message; - } - - state.addError(message, isWarning); - - // In a broken state don't need to know about any other errors. - if (!isWarning) - state.forceEnd(); - } - - /** - * Start recording ink profiling information during calls to Continue on Story. - * Return a Profiler instance that you can request a report from when you're - * finished. - * - * @throws Exception - */ - public Profiler startProfiling() throws Exception { - ifAsyncWeCant("start profiling"); - profiler = new Profiler(); - - return profiler; - } - - /** - * Stop recording ink profiling information during calls to Continue on Story. - * To generate a report from the profiler, call - */ - public void endProfiling() { - profiler = null; - } - - void Assert(boolean condition, Object... formatParams) throws Exception { - Assert(condition, null, formatParams); - } - - void Assert(boolean condition, String message, Object... formatParams) throws Exception { - if (!condition) { - if (message == null) { - message = "Story assert"; - } - if (formatParams != null && formatParams.length > 0) { - message = String.format(message, formatParams); - } - - throw new Exception(message + " " + currentDebugMetadata()); - } - } - - /** - * Binds a Java function to an ink EXTERNAL function. - * - * @param funcName EXTERNAL ink function name to bind to. - * @param func The Java function to bind. - * @param lookaheadSafe The ink engine often evaluates further than you might - * expect beyond the current line just in case it sees glue - * that will cause the two lines to become one. In this - * case it's possible that a function can appear to be - * called twice instead of just once, and earlier than you - * expect. If it's safe for your function to be called in - * this way (since the result and side effect of the - * function will not change), then you can pass 'true'. - * Usually, you want to pass 'false', especially if you - * want some action to be performed in game code when this - * function is called. - */ - public void bindExternalFunction(String funcName, ExternalFunction func, boolean lookaheadSafe) - throws Exception { - ifAsyncWeCant("bind an external function"); - Assert(!externals.containsKey(funcName), "Function '" + funcName + "' has already been bound."); - ExternalFunctionDef externalFunctionDef = new ExternalFunctionDef(); - externalFunctionDef.function = func; - externalFunctionDef.lookaheadSafe = lookaheadSafe; - - externals.put(funcName, externalFunctionDef); - } - - public void bindExternalFunction(String funcName, ExternalFunction func) throws Exception { - bindExternalFunction(funcName, func, true); - } - - @SuppressWarnings("unchecked") - public T tryCoerce(Object value, Class type) throws Exception { - - if (value == null) - return null; - - if (type.isAssignableFrom(value.getClass())) - return (T) value; - - if (value instanceof Float && type == Integer.class) { - Integer intVal = (int) Math.round((Float) value); - return (T) intVal; - } - - if (value instanceof Integer && type == Float.class) { - Float floatVal = Float.valueOf((Integer) value); - return (T) floatVal; - } - - if (value instanceof Integer && type == Boolean.class) { - int intVal = (Integer) value; - return (T) (intVal == 0 ? Boolean.FALSE : Boolean.TRUE); - } - - if (value instanceof Boolean && type == Integer.class) { - boolean val = (Boolean) value; - return (T) (val ? (Integer) 1 : (Integer) 0); - } - - if (type == String.class) { - return (T) value.toString(); - } - - Assert(false, "Failed to cast " + value.getClass().getCanonicalName() + " to " + type.getCanonicalName()); - - return null; - } - - /** - * Get any global tags associated with the story. These are defined as hash tags - * defined at the very top of the story. - * - * @throws Exception - */ - public List getGlobalTags() throws Exception { - return tagsAtStartOfFlowContainerWithPathString(""); - } - - /** - * Gets any tags associated with a particular knot or knot.stitch. These are - * defined as hash tags defined at the very top of a knot or stitch. - * - * @param path The path of the knot or stitch, in the form "knot" or - * "knot.stitch". - * @throws Exception - */ - public List tagsForContentAtPath(String path) throws Exception { - return tagsAtStartOfFlowContainerWithPathString(path); - } - - List tagsAtStartOfFlowContainerWithPathString(String pathString) throws Exception { - Path path = new Path(pathString); - - // Expected to be global story, knot or stitch - Container flowContainer = null; - RTObject c = contentAtPath(path).getContainer(); - - if (c instanceof Container) - flowContainer = (Container) c; - - while (true) { - RTObject firstContent = flowContainer.getContent().get(0); - if (firstContent instanceof Container) - flowContainer = (Container) firstContent; - else - break; - } - - // Any initial tag objects count as the "main tags" associated with that - // story/knot/stitch - List tags = null; - for (RTObject c2 : flowContainer.getContent()) { - Tag tag = null; - - if (c2 instanceof Tag) - tag = (Tag) c2; - - if (tag != null) { - if (tags == null) - tags = new ArrayList<>(); - tags.add(tag.getText()); - } else - break; - } - - return tags; - } - - /** - * Useful when debugging a (very short) story, to visualise the state of the - * story. Add this call as a watch and open the extended text. A left-arrow mark - * will denote the current point of the story. It's only recommended that this - * is used on very short debug stories, since it can end up generate a large - * quantity of text otherwise. - */ - public String buildStringOfHierarchy() { - StringBuilder sb = new StringBuilder(); - - getMainContentContainer().buildStringOfHierarchy(sb, 0, state.getCurrentPointer().resolve()); - - return sb.toString(); - } - - void callExternalFunction(String funcName, int numberOfArguments) throws Exception { - ExternalFunctionDef funcDef; - Container fallbackFunctionContainer = null; - - funcDef = externals.get(funcName); - - // Should this function break glue? Abort run if we've already seen a newline. - // Set a bool to tell it to restore the snapshot at the end of this instruction. - if (funcDef != null && !funcDef.lookaheadSafe && stateSnapshotAtLastNewline != null) { - sawLookaheadUnsafeFunctionAfterNewline = true; - return; - } - - // Try to use fallback function? - if (funcDef == null) { - if (allowExternalFunctionFallbacks) { - - fallbackFunctionContainer = knotContainerWithName(funcName); - - Assert(fallbackFunctionContainer != null, "Trying to call EXTERNAL function '" + funcName - + "' which has not been bound, and fallback ink function could not be found."); - - // Divert direct into fallback function and we're done - state.getCallStack().push(PushPopType.Function, 0, state.getOutputStream().size()); - state.setDivertedPointer(Pointer.startOf(fallbackFunctionContainer)); - return; - - } else { - Assert(false, "Trying to call EXTERNAL function '" + funcName - + "' which has not been bound (and ink fallbacks disabled)."); - } - } - - // Pop arguments - ArrayList arguments = new ArrayList<>(); - for (int i = 0; i < numberOfArguments; ++i) { - Value poppedObj = (Value) state.popEvaluationStack(); - Object valueObj = poppedObj.getValueObject(); - arguments.add(valueObj); - } - - // Reverse arguments from the order they were popped, - // so they're the right way round again. - Collections.reverse(arguments); - - // Run the function! - Object funcResult = funcDef.function.call(arguments.toArray()); - - // Convert return value (if any) to the a type that the ink engine can use - RTObject returnObj; - if (funcResult != null) { - returnObj = AbstractValue.create(funcResult); - Assert(returnObj != null, "Could not create ink value from returned Object of type " - + funcResult.getClass().getCanonicalName()); - } else { - returnObj = new Void(); - } - - state.pushEvaluationStack(returnObj); - } - - /** - * Check whether more content is available if you were to call Continue() - i.e. - * are we mid story rather than at a choice point or at the end. - * - * @return true if it's possible to call Continue() - */ - public boolean canContinue() { - return state.canContinue(); - } - - /** - * Chooses the Choice from the currentChoices list with the given index. - * Internally, this sets the current content path to that pointed to by the - * Choice, ready to continue story evaluation. - */ - public void chooseChoiceIndex(int choiceIdx) throws Exception { - List choices = getCurrentChoices(); - Assert(choiceIdx >= 0 && choiceIdx < choices.size(), "choice out of range"); - - // Replace callstack with the one from the thread at the choosing point, - // so that we can jump into the right place in the flow. - // This is important in case the flow was forked by a new thread, which - // can create multiple leading edges for the story, each of - // which has its own context. - Choice choiceToChoose = choices.get(choiceIdx); - state.getCallStack().setCurrentThread(choiceToChoose.getThreadAtGeneration()); - - choosePath(choiceToChoose.targetPath); - } - - void choosePath(Path p) throws Exception { - choosePath(p, true); - } - - void choosePath(Path p, boolean incrementingTurnIndex) throws Exception { - state.setChosenPath(p, incrementingTurnIndex); - - // Take a note of newly visited containers for read counts etc - visitChangedContainersDueToDivert(); - } - - /** - * Change the current position of the story to the given path. From here you can - * call Continue() to evaluate the next line. - * - * The path String is a dot-separated path as used ly by the engine. These - * examples should work: - * - * myKnot myKnot.myStitch - * - * Note however that this won't necessarily work: - * - * myKnot.myStitch.myLabelledChoice - * - * ...because of the way that content is nested within a weave structure. - * - * By default this will reset the callstack beforehand, which means that any - * tunnels, threads or functions you were in at the time of calling will be - * discarded. This is different from the behaviour of ChooseChoiceIndex, which - * will always keep the callstack, since the choices are known to come from the - * correct state, and known their source thread. - * - * You have the option of passing false to the resetCallstack parameter if you - * don't want this behaviour, and will leave any active threads, tunnels or - * function calls in-tact. - * - * This is potentially dangerous! If you're in the middle of a tunnel, it'll - * redirect only the inner-most tunnel, meaning that when you tunnel-return - * using '->->->', it'll return to where you were before. This may be - * what you want though. However, if you're in the middle of a function, - * ChoosePathString will throw an exception. - * - * - * @param path A dot-separted path string, as specified above. - * @param resetCallstack Whether to reset the callstack first (see summary - * description). - * @param arguments Optional set of arguments to pass, if path is to a knot - * that takes them. - */ - public void choosePathString(String path, boolean resetCallstack, Object[] arguments) throws Exception { - ifAsyncWeCant("call ChoosePathString right now"); - - if (resetCallstack) { - resetCallstack(); - } else { - // ChoosePathString is potentially dangerous since you can call it when the - // stack is - // pretty much in any state. Let's catch one of the worst offenders. - if (state.getCallStack().getCurrentElement().type == PushPopType.Function) { - String funcDetail = ""; - Container container = state.getCallStack().getCurrentElement().currentPointer.container; - if (container != null) { - funcDetail = "(" + container.getPath().toString() + ") "; - } - throw new Exception("Story was running a function " + funcDetail + "when you called ChoosePathString(" - + path + ") - this is almost certainly not not what you want! Full stack trace: \n" - + state.getCallStack().getCallStackTrace()); - } - } - - state.passArgumentsToEvaluationStack(arguments); - choosePath(new Path(path)); - } - - public void choosePathString(String path) throws Exception { - choosePathString(path, true, null); - } - - public void choosePathString(String path, boolean resetCallstack) throws Exception { - choosePathString(path, resetCallstack, null); - } - - void ifAsyncWeCant(String activityStr) throws Exception { - if (asyncContinueActive) - throw new Exception("Can't " + activityStr - + ". Story is in the middle of a ContinueAsync(). Make more ContinueAsync() calls or a single Continue() call beforehand."); - } - - SearchResult contentAtPath(Path path) throws Exception { - return getMainContentContainer().contentAtPath(path); - } - - Container knotContainerWithName(String name) { - - INamedContent namedContainer = mainContentContainer.getNamedContent().get(name); - - if (namedContainer != null) - return namedContainer instanceof Container ? (Container) namedContainer : null; - else - return null; - } - - /** - * The current flow name if using multi-flow functionality - see SwitchFlow - */ - public String getCurrentFlowName() { - return state.getCurrentFlowName(); - } - - /** - * Is the default flow currently active? By definition, will also return true if not using multi-flow - * functionality - see SwitchFlow - */ - public boolean currentFlowIsDefaultFlow() { return state.currentFlowIsDefaultFlow(); } - - /** - * Names of currently alive flows (not including the default flow) - */ - public List aliveFlowNames() { return state.aliveFlowNames(); } - - public void switchFlow(String flowName) throws Exception { - ifAsyncWeCant("switch flow"); - - if (asyncSaving) - throw new Exception("Story is already in background saving mode, can't switch flow to " + flowName); - - state.switchFlowInternal(flowName); - } - - public void removeFlow(String flowName) throws Exception { - state.removeFlowInternal(flowName); - } - - public void switchToDefaultFlow() throws Exception { - state.switchToDefaultFlowInternal(); - } - - /** - * Continue the story for one line of content, if possible. If you're not sure - * if there's more content available, for example if you want to check whether - * you're at a choice point or at the end of the story, you should call - * canContinue before calling this function. - * - * @return The line of text content. - */ - public String Continue() throws StoryException, Exception { - continueAsync(0); - return getCurrentText(); - } - - /** - * If ContinueAsync was called (with milliseconds limit > 0) then this - * property will return false if the ink evaluation isn't yet finished, and you - * need to call it again in order for the Continue to fully complete. - */ - public boolean asyncContinueComplete() { - return !asyncContinueActive; - } - - /** - * An "asnychronous" version of Continue that only partially evaluates the ink, - * with a budget of a certain time limit. It will exit ink evaluation early if - * the evaluation isn't complete within the time limit, with the - * asyncContinueComplete property being false. This is useful if ink evaluation - * takes a long time, and you want to distribute it over multiple game frames - * for smoother animation. If you pass a limit of zero, then it will fully - * evaluate the ink in the same way as calling Continue (and in fact, this - * exactly what Continue does internally). - */ - public void continueAsync(float millisecsLimitAsync) throws Exception { - if (!hasValidatedExternals) - validateExternalBindings(); - - continueInternal(millisecsLimitAsync); - } - - void continueInternal() throws Exception { - continueInternal(0); - } - - void continueInternal(float millisecsLimitAsync) throws Exception { - if (profiler != null) - profiler.preContinue(); - - boolean isAsyncTimeLimited = millisecsLimitAsync > 0; - - recursiveContinueCount++; - - // Doing either: - // - full run through non-async (so not active and don't want to be) - // - Starting async run-through - if (!asyncContinueActive) { - asyncContinueActive = isAsyncTimeLimited; - if (!canContinue()) { - throw new Exception("Can't continue - should check canContinue before calling Continue"); - } - - state.setDidSafeExit(false); - - state.resetOutput(); - - // It's possible for ink to call game to call ink to call game etc - // In this case, we only want to batch observe variable changes - // for the outermost call. - if (recursiveContinueCount == 1) - state.getVariablesState().setbatchObservingVariableChanges(true); - } - - // Start timing - Stopwatch durationStopwatch = new Stopwatch(); - durationStopwatch.start(); - - boolean outputStreamEndsInNewline = false; - sawLookaheadUnsafeFunctionAfterNewline = false; - do { - - try { - outputStreamEndsInNewline = continueSingleStep(); - } catch (StoryException e) { - addError(e.getMessage(), false, e.useEndLineNumber); - break; - } - - if (outputStreamEndsInNewline) - break; - - // Run out of async time? - if (asyncContinueActive && durationStopwatch.getElapsedMilliseconds() > millisecsLimitAsync) { - break; - } - - } while (canContinue()); - - durationStopwatch.stop(); - - // 4 outcomes: - // - got newline (so finished this line of text) - // - can't continue (e.g. choices or ending) - // - ran out of time during evaluation - // - error - // - // Successfully finished evaluation in time (or in error) - if (outputStreamEndsInNewline || !canContinue()) { - // Need to rewind, due to evaluating further than we should? - if (stateSnapshotAtLastNewline != null) { - restoreStateSnapshot(); - } - - // Finished a section of content / reached a choice point? - if (!canContinue()) { - if (state.getCallStack().canPopThread()) - addError("Thread available to pop, threads should always be flat by the end of evaluation?"); - - if (state.getGeneratedChoices().size() == 0 && !state.isDidSafeExit() - && temporaryEvaluationContainer == null) { - if (state.getCallStack().canPop(PushPopType.Tunnel)) - addError("unexpectedly reached end of content. Do you need a '->->' to return from a tunnel?"); - else if (state.getCallStack().canPop(PushPopType.Function)) - addError("unexpectedly reached end of content. Do you need a '~ return'?"); - else if (!state.getCallStack().canPop()) - addError("ran out of content. Do you need a '-> DONE' or '-> END'?"); - else - addError("unexpectedly reached end of content for unknown reason. Please debug compiler!"); - } - } - state.setDidSafeExit(false); - sawLookaheadUnsafeFunctionAfterNewline = false; - - if (recursiveContinueCount == 1) - state.getVariablesState().setbatchObservingVariableChanges(false); - asyncContinueActive = false; - } - - recursiveContinueCount--; - - if (profiler != null) - profiler.postContinue(); - - // Report any errors that occured during evaluation. - // This may either have been StoryExceptions that were thrown - // and caught during evaluation, or directly added with AddError. - if (state.hasError() || state.hasWarning()) { - if (onError != null) { - if (state.hasError()) { - for (String err : state.getCurrentErrors()) { - onError.error(err, ErrorType.Error); - } - } - if (state.hasWarning()) { - for (String err : state.getCurrentWarnings()) { - onError.error(err, ErrorType.Warning); - } - } - - resetErrors(); - } - // Throw an exception since there's no error handler - else { - StringBuilder sb = new StringBuilder(); - sb.append("Ink had "); - if (state.hasError()) { - sb.append(state.getCurrentErrors().size()); - sb.append(state.getCurrentErrors().size() == 1 ? " error" : " errors"); - if (state.hasWarning()) - sb.append(" and "); - } - if (state.hasWarning()) { - sb.append(state.getCurrentWarnings().size()); - sb.append(state.getCurrentWarnings().size() == 1 ? " warning" : " warnings"); - } - sb.append( - ". It is strongly suggested that you assign an error handler to story.onError. The first issue was: "); - sb.append(state.hasError() ? state.getCurrentErrors().get(0) : state.getCurrentWarnings().get(0)); - - // If you get this exception, please assign an error handler to your story. - // If you're using Unity, you can do something like this when you create - // your story: - // - // var story = new Ink.Runtime.Story(jsonTxt); - // story.onError = (errorMessage, errorType) => { - // if( errorType == ErrorType.Warning ) - // Debug.LogWarning(errorMessage); - // else - // Debug.LogError(errorMessage); - // }; - // - // - throw new StoryException(sb.toString()); - } - } - } - - boolean continueSingleStep() throws Exception { - if (profiler != null) - profiler.preStep(); - - // Run main step function (walks through content) - step(); - - if (profiler != null) - profiler.postStep(); - - // Run out of content and we have a default invisible choice that we can follow? - if (!canContinue() && !state.getCallStack().elementIsEvaluateFromGame()) { - - tryFollowDefaultInvisibleChoice(); - } - - if (profiler != null) - profiler.preSnapshot(); - - // Don't save/rewind during string evaluation, which is e.g. used for choices - if (!state.inStringEvaluation()) { - - // We previously found a newline, but were we just double checking that - // it wouldn't immediately be removed by glue? - if (stateSnapshotAtLastNewline != null) { - - // Has proper text or a tag been added? Then we know that the newline - // that was previously added is definitely the end of the line. - OutputStateChange change = calculateNewlineOutputStateChange( - stateSnapshotAtLastNewline.getCurrentText(), state.getCurrentText(), - stateSnapshotAtLastNewline.getCurrentTags().size(), state.getCurrentTags().size()); - - // The last time we saw a newline, it was definitely the end of the line, so we - // want to rewind to that point. - if (change == OutputStateChange.ExtendedBeyondNewline || sawLookaheadUnsafeFunctionAfterNewline) { - restoreStateSnapshot(); - - // Hit a newline for sure, we're done - return true; - } - - // Newline that previously existed is no longer valid - e.g. - // glue was encounted that caused it to be removed. - else if (change == OutputStateChange.NewlineRemoved) { - stateSnapshotAtLastNewline = null; - discardSnapshot(); - } - - } - - // Current content ends in a newline - approaching end of our evaluation - if (state.outputStreamEndsInNewline()) { - - // If we can continue evaluation for a bit: - // Create a snapshot in case we need to rewind. - // We're going to continue stepping in case we see glue or some - // non-text content such as choices. - if (canContinue()) { - - // Don't bother to record the state beyond the current newline. - // e.g.: - // Hello world\n // record state at the end of here - // ~ complexCalculation() // don't actually need this unless it generates text - if (stateSnapshotAtLastNewline == null) - stateSnapshot(); - } - - // Can't continue, so we're about to exit - make sure we - // don't have an old state hanging around. - else { - discardSnapshot(); - } - - } - - } - - if (profiler != null) - profiler.postSnapshot(); - - // outputStreamEndsInNewline = false - return false; - - } - - /** - * Continue the story until the next choice point or until it runs out of - * content. This is as opposed to the Continue() method which only evaluates one - * line of output at a time. - * - * @return The resulting text evaluated by the ink engine, concatenated - * together. - */ - public String continueMaximally() throws StoryException, Exception { - ifAsyncWeCant("ContinueMaximally"); - - StringBuilder sb = new StringBuilder(); - - while (canContinue()) { - sb.append(Continue()); - } - - return sb.toString(); - } - - DebugMetadata currentDebugMetadata() { - DebugMetadata dm; - - // Try to get from the current path first - final Pointer pointer = new Pointer(state.getCurrentPointer()); - if (!pointer.isNull()) { - dm = pointer.resolve().getDebugMetadata(); - if (dm != null) { - return dm; - } - } - - // Move up callstack if possible - for (int i = state.getCallStack().getElements().size() - 1; i >= 0; --i) { - pointer.assign(state.getCallStack().getElements().get(i).currentPointer); - if (!pointer.isNull() && pointer.resolve() != null) { - dm = pointer.resolve().getDebugMetadata(); - if (dm != null) { - return dm; - } - } - } - - // Current/previous path may not be valid if we've just had an error, - // or if we've simply run out of content. - // As a last resort, try to grab something from the output stream - for (int i = state.getOutputStream().size() - 1; i >= 0; --i) { - RTObject outputObj = state.getOutputStream().get(i); - dm = outputObj.getDebugMetadata(); - if (dm != null) { - return dm; - } - } - - return null; - } - - int currentLineNumber() throws Exception { - DebugMetadata dm = currentDebugMetadata(); - if (dm != null) { - return dm.startLineNumber; - } - return 0; - } - - void error(String message) throws Exception { - error(message, false); - } - - // Throw an exception that gets caught and causes AddError to be called, - // then exits the flow. - void error(String message, boolean useEndLineNumber) throws Exception { - StoryException e = new StoryException(message); - e.useEndLineNumber = useEndLineNumber; - throw e; - } - - // Evaluate a "hot compiled" piece of ink content, as used by the REPL-like - // CommandLinePlayer. - RTObject evaluateExpression(Container exprContainer) throws StoryException, Exception { - int startCallStackHeight = state.getCallStack().getElements().size(); - - state.getCallStack().push(PushPopType.Tunnel); - - temporaryEvaluationContainer = exprContainer; - - state.goToStart(); - - int evalStackHeight = state.getEvaluationStack().size(); - - Continue(); - - temporaryEvaluationContainer = null; - - // Should have fallen off the end of the Container, which should - // have auto-popped, but just in case we didn't for some reason, - // manually pop to restore the state (including currentPath). - if (state.getCallStack().getElements().size() > startCallStackHeight) { - state.popCallstack(); - } - - int endStackHeight = state.getEvaluationStack().size(); - if (endStackHeight > evalStackHeight) { - return state.popEvaluationStack(); - } else { - return null; - } - - } - - /** - * The list of Choice Objects available at the current point in the Story. This - * list will be populated as the Story is stepped through with the Continue() - * method. Once canContinue becomes false, this list will be populated, and is - * usually (but not always) on the final Continue() step. - */ - public List getCurrentChoices() { - - // Don't include invisible choices for external usage. - List choices = new ArrayList<>(); - for (Choice c : state.getCurrentChoices()) { - if (!c.isInvisibleDefault) { - c.setIndex(choices.size()); - choices.add(c); - } - } - - return choices; - } - - /** - * Gets a list of tags as defined with '#' in source that were seen during the - * latest Continue() call. - * - * @throws Exception - */ - public List getCurrentTags() throws Exception { - ifAsyncWeCant("call currentTags since it's a work in progress"); - return state.getCurrentTags(); - } - - /** - * Any warnings generated during evaluation of the Story. - */ - public List getCurrentWarnings() { - return state.getCurrentWarnings(); - } - - /** - * Any errors generated during evaluation of the Story. - */ - public List getCurrentErrors() { - return state.getCurrentErrors(); - } - - /** - * The latest line of text to be generated from a Continue() call. - * - * @throws Exception - */ - public String getCurrentText() throws Exception { - ifAsyncWeCant("call currentText since it's a work in progress"); - return state.getCurrentText(); - } - - /** - * The entire current state of the story including (but not limited to): - * - * * Global variables * Temporary variables * Read/visit and turn counts * The - * callstack and evaluation stacks * The current threads - * - */ - public StoryState getState() { - return state; - } - - /** - * The VariablesState Object contains all the global variables in the story. - * However, note that there's more to the state of a Story than just the global - * variables. This is a convenience accessor to the full state Object. - */ - public VariablesState getVariablesState() { - return state.getVariablesState(); - } - - public ListDefinitionsOrigin getListDefinitions() { - return listDefinitions; - } - - /** - * Whether the currentErrors list contains any errors. THIS MAY BE REMOVED - you - * should be setting an error handler directly using Story.onError. - */ - public boolean hasError() { - return state.hasError(); - } - - /** - * Whether the currentWarnings list contains any warnings. - */ - public boolean hasWarning() { - return state.hasWarning(); - } - - boolean incrementContentPointer() { - boolean successfulIncrement = true; - - Pointer pointer = new Pointer(state.getCallStack().getCurrentElement().currentPointer); - pointer.index++; - - // Each time we step off the end, we fall out to the next container, all - // the - // while we're in indexed rather than named content - while (pointer.index >= pointer.container.getContent().size()) { - - successfulIncrement = false; - - Container nextAncestor = pointer.container.getParent() instanceof Container - ? (Container) pointer.container.getParent() - : null; - - if (nextAncestor == null) { - break; - } - - int indexInAncestor = nextAncestor.getContent().indexOf(pointer.container); - if (indexInAncestor == -1) { - break; - } - - pointer = new Pointer(nextAncestor, indexInAncestor); - - // Increment to next content in outer container - pointer.index++; - - successfulIncrement = true; - } - - if (!successfulIncrement) - pointer.assign(Pointer.Null); - - state.getCallStack().getCurrentElement().currentPointer.assign(pointer); - - return successfulIncrement; - } - - // Does the expression result represented by this Object evaluate to true? - // e.g. is it a Number that's not equal to 1? - boolean isTruthy(RTObject obj) throws Exception { - boolean truthy = false; - if (obj instanceof Value) { - Value val = (Value) obj; - - if (val instanceof DivertTargetValue) { - DivertTargetValue divTarget = (DivertTargetValue) val; - error("Shouldn't use a divert target (to " + divTarget.getTargetPath() - + ") as a conditional value. Did you intend a function call 'likeThis()' or a read count check 'likeThis'? (no arrows)"); - return false; - } - - return val.isTruthy(); - } - return truthy; - } - - /** - * When the named global variable changes it's value, the observer will be - * called to notify it of the change. Note that if the value changes multiple - * times within the ink, the observer will only be called once, at the end of - * the ink's evaluation. If, during the evaluation, it changes and then changes - * back again to its original value, it will still be called. Note that the - * observer will also be fired if the value of the variable is changed - * externally to the ink, by directly setting a value in story.variablesState. - * - * @param variableName The name of the global variable to observe. - * @param observer A delegate function to call when the variable changes. - * @throws Exception - */ - public void observeVariable(String variableName, VariableObserver observer) throws Exception { - ifAsyncWeCant("observe a new variable"); - - if (variableObservers == null) - variableObservers = new HashMap<>(); - - if (!state.getVariablesState().globalVariableExistsWithName(variableName)) - throw new Exception( - "Cannot observe variable '" + variableName + "' because it wasn't declared in the ink story."); - - if (variableObservers.containsKey(variableName)) { - variableObservers.get(variableName).add(observer); - } else { - List l = new ArrayList<>(); - l.add(observer); - variableObservers.put(variableName, l); - } - } - - /** - * Convenience function to allow multiple variables to be observed with the same - * observer delegate function. See the singular ObserveVariable for details. The - * observer will get one call for every variable that has changed. - * - * @param variableNames The set of variables to observe. - * @param observer The delegate function to call when any of the named - * variables change. - * @throws Exception - * @throws StoryException - */ - public void observeVariables(List variableNames, VariableObserver observer) - throws StoryException, Exception { - for (String varName : variableNames) { - observeVariable(varName, observer); - } - } - - /** - * Removes the variable observer, to stop getting variable change notifications. - * If you pass a specific variable name, it will stop observing that particular - * one. If you pass null (or leave it blank, since it's optional), then the - * observer will be removed from all variables that it's subscribed to. - * - * @param observer The observer to stop observing. - * @param specificVariableName (Optional) Specific variable name to stop - * observing. - * @throws Exception - */ - public void removeVariableObserver(VariableObserver observer, String specificVariableName) throws Exception { - ifAsyncWeCant("remove a variable observer"); - - if (variableObservers == null) - return; - - // Remove observer for this specific variable - if (specificVariableName != null) { - if (variableObservers.containsKey(specificVariableName)) { - variableObservers.get(specificVariableName).remove(observer); - if (variableObservers.get(specificVariableName).size() == 0) { - variableObservers.remove(specificVariableName); - } - } - } else { - // Remove observer for all variables - for (Map.Entry> obs : variableObservers.entrySet()) { - obs.getValue().remove(observer); - if (obs.getValue().size() == 0) { - variableObservers.remove(obs.getKey()); - } - } - } - } - - public void removeVariableObserver(VariableObserver observer) throws Exception { - removeVariableObserver(observer, null); - } - - @Override - public void variableStateDidChangeEvent(String variableName, RTObject newValueObj) throws Exception { - if (variableObservers == null) - return; - - List observers = variableObservers.get(variableName); - - if (observers != null) { - if (!(newValueObj instanceof Value)) { - throw new Exception("Tried to get the value of a variable that isn't a standard type"); - } - - Value val = (Value) newValueObj; - - for (VariableObserver o : observers) { - o.call(variableName, val.getValueObject()); - } - } - } - - public Container getMainContentContainer() { - if (temporaryEvaluationContainer != null) { - return temporaryEvaluationContainer; - } else { - return mainContentContainer; - } - } - - String buildStringOfContainer(Container container) { - StringBuilder sb = new StringBuilder(); - - container.buildStringOfHierarchy(sb, 0, state.getCurrentPointer().resolve()); - - return sb.toString(); - } - - private void nextContent() throws Exception { - // Setting previousContentObject is critical for - // VisitChangedContainersDueToDivert - state.setPreviousPointer(state.getCurrentPointer()); - - // Divert step? - if (!state.getDivertedPointer().isNull()) { - - state.setCurrentPointer(state.getDivertedPointer()); - state.setDivertedPointer(Pointer.Null); - - // Internally uses state.previousContentObject and - // state.currentContentObject - visitChangedContainersDueToDivert(); - - // Diverted location has valid content? - if (!state.getCurrentPointer().isNull()) { - return; - } - - // Otherwise, if diverted location doesn't have valid content, - // drop down and attempt to increment. - // This can happen if the diverted path is intentionally jumping - // to the end of a container - e.g. a Conditional that's re-joining - } - - boolean successfulPointerIncrement = incrementContentPointer(); - - // Ran out of content? Try to auto-exit from a function, - // or finish evaluating the content of a thread - if (!successfulPointerIncrement) { - - boolean didPop = false; - - if (state.getCallStack().canPop(PushPopType.Function)) { - - // Pop from the call stack - state.popCallstack(PushPopType.Function); - - // This pop was due to dropping off the end of a function that - // didn't return anything, - // so in this case, we make sure that the evaluator has - // something to chomp on if it needs it - if (state.getInExpressionEvaluation()) { - state.pushEvaluationStack(new Void()); - } - - didPop = true; - } else if (state.getCallStack().canPopThread()) { - state.getCallStack().popThread(); - - didPop = true; - } else { - state.tryExitFunctionEvaluationFromGame(); - } - - // Step past the point where we last called out - if (didPop && !state.getCurrentPointer().isNull()) { - nextContent(); - } - } - } - - // Note that this is O(n), since it re-evaluates the shuffle indices - // from a consistent seed each time. - // TODO: Is this the best algorithm it can be? - int nextSequenceShuffleIndex() throws Exception { - RTObject popEvaluationStack = state.popEvaluationStack(); - - IntValue numElementsIntVal = popEvaluationStack instanceof IntValue ? (IntValue) popEvaluationStack : null; - - if (numElementsIntVal == null) { - error("expected number of elements in sequence for shuffle index"); - return 0; - } - - Container seqContainer = state.getCurrentPointer().container; - - int numElements = numElementsIntVal.value; - - IntValue seqCountVal = (IntValue) state.popEvaluationStack(); - int seqCount = seqCountVal.value; - int loopIndex = seqCount / numElements; - int iterationIndex = seqCount % numElements; - - // Generate the same shuffle based on: - // - The hash of this container, to make sure it's consistent - // each time the runtime returns to the sequence - // - How many times the runtime has looped around this full shuffle - String seqPathStr = seqContainer.getPath().toString(); - int sequenceHash = 0; - for (char c : seqPathStr.toCharArray()) { - sequenceHash += c; - } - - int randomSeed = sequenceHash + loopIndex + state.getStorySeed(); - - Random random = new Random(randomSeed); - - ArrayList unpickedIndices = new ArrayList<>(); - for (int i = 0; i < numElements; ++i) { - unpickedIndices.add(i); - } - - for (int i = 0; i <= iterationIndex; ++i) { - int chosen = random.nextInt(Integer.MAX_VALUE) % unpickedIndices.size(); - int chosenIndex = unpickedIndices.get(chosen); - unpickedIndices.remove(chosen); - - if (i == iterationIndex) { - return chosenIndex; - } - } - - throw new Exception("Should never reach here"); - } - - /** - * Checks whether contentObj is a control or flow Object rather than a piece of - * content, and performs the required command if necessary. - * - * @return true if Object was logic or flow control, false if it's normal - * content. - * @param contentObj Content Object. - */ - boolean performLogicAndFlowControl(RTObject contentObj) throws Exception { - if (contentObj == null) { - return false; - } - - // Divert - if (contentObj instanceof Divert) { - - Divert currentDivert = (Divert) contentObj; - - if (currentDivert.isConditional()) { - RTObject conditionValue = state.popEvaluationStack(); - - // False conditional? Cancel divert - if (!isTruthy(conditionValue)) - return true; - } - - if (currentDivert.hasVariableTarget()) { - String varName = currentDivert.getVariableDivertName(); - - RTObject varContents = state.getVariablesState().getVariableWithName(varName); - - if (varContents == null) { - error("Tried to divert using a target from a variable that could not be found (" + varName + ")"); - } else if (!(varContents instanceof DivertTargetValue)) { - - IntValue intContent = varContents instanceof IntValue ? (IntValue) varContents : null; - - String errorMessage = "Tried to divert to a target from a variable, but the variable (" + varName - + ") didn't contain a divert target, it "; - if (intContent != null && intContent.value == 0) { - errorMessage += "was empty/null (the value 0)."; - } else { - errorMessage += "contained '" + varContents + "'."; - } - - error(errorMessage); - } - - DivertTargetValue target = (DivertTargetValue) varContents; - state.setDivertedPointer(pointerAtPath(target.getTargetPath())); - - } else if (currentDivert.isExternal()) { - callExternalFunction(currentDivert.getTargetPathString(), currentDivert.getExternalArgs()); - return true; - } else { - state.setDivertedPointer(currentDivert.getTargetPointer()); - } - - if (currentDivert.getPushesToStack()) { - state.getCallStack().push(currentDivert.getStackPushType(), 0, state.getOutputStream().size()); - } - - if (state.getDivertedPointer().isNull() && !currentDivert.isExternal()) { - - // Human readable name available - runtime divert is part of a - // hard-written divert that to missing content - if (currentDivert != null && currentDivert.getDebugMetadata().sourceName != null) { - error("Divert target doesn't exist: " + currentDivert.getDebugMetadata().sourceName); - } else { - error("Divert resolution failed: " + currentDivert); - } - } - - return true; - } - - // Start/end an expression evaluation? Or print out the result? - else if (contentObj instanceof ControlCommand) { - ControlCommand evalCommand = (ControlCommand) contentObj; - - int choiceCount; - switch (evalCommand.getCommandType()) { - - case EvalStart: - Assert(state.getInExpressionEvaluation() == false, "Already in expression evaluation?"); - state.setInExpressionEvaluation(true); - break; - - case EvalEnd: - Assert(state.getInExpressionEvaluation() == true, "Not in expression evaluation mode"); - state.setInExpressionEvaluation(false); - break; - - case EvalOutput: - - // If the expression turned out to be empty, there may not be - // anything on the stack - if (state.getEvaluationStack().size() > 0) { - - RTObject output = state.popEvaluationStack(); - - // Functions may evaluate to Void, in which case we skip - // output - if (!(output instanceof Void)) { - // TODO: Should we really always blanket convert to - // string? - // It would be okay to have numbers in the output stream - // the - // only problem is when exporting text for viewing, it - // skips over numbers etc. - StringValue text = new StringValue(output.toString()); - - state.pushToOutputStream(text); - } - - } - break; - - case NoOp: - break; - - case Duplicate: - state.pushEvaluationStack(state.peekEvaluationStack()); - break; - - case PopEvaluatedValue: - state.popEvaluationStack(); - break; - - case PopFunction: - case PopTunnel: - - PushPopType popType = evalCommand.getCommandType() == ControlCommand.CommandType.PopFunction - ? PushPopType.Function - : PushPopType.Tunnel; - - // Tunnel onwards is allowed to specify an optional override - // divert to go to immediately after returning: ->-> target - DivertTargetValue overrideTunnelReturnTarget = null; - if (popType == PushPopType.Tunnel) { - RTObject popped = state.popEvaluationStack(); - - if (popped instanceof DivertTargetValue) { - overrideTunnelReturnTarget = (DivertTargetValue) popped; - } - - if (overrideTunnelReturnTarget == null) { - Assert(popped instanceof Void, "Expected void if ->-> doesn't override target"); - } - } - - if (state.tryExitFunctionEvaluationFromGame()) { - break; - } else if (state.getCallStack().getCurrentElement().type != popType || !state.getCallStack().canPop()) { - - HashMap names = new HashMap<>(); - names.put(PushPopType.Function, "function return statement (~ return)"); - names.put(PushPopType.Tunnel, "tunnel onwards statement (->->)"); - - String expected = names.get(state.getCallStack().getCurrentElement().type); - if (!state.getCallStack().canPop()) { - expected = "end of flow (-> END or choice)"; - } - - String errorMsg = String.format("Found %s, when expected %s", names.get(popType), expected); - - error(errorMsg); - } - - else { - state.popCallstack(); - - // Does tunnel onwards override by diverting to a new ->-> - // target? - if (overrideTunnelReturnTarget != null) - state.setDivertedPointer(pointerAtPath(overrideTunnelReturnTarget.getTargetPath())); - } - break; - - case BeginString: - state.pushToOutputStream(evalCommand); - - Assert(state.getInExpressionEvaluation() == true, - "Expected to be in an expression when evaluating a string"); - state.setInExpressionEvaluation(false); - break; - - case EndString: - - // Since we're iterating backward through the content, - // build a stack so that when we build the string, - // it's in the right order - Stack contentStackForString = new Stack<>(); - - int outputCountConsumed = 0; - for (int i = state.getOutputStream().size() - 1; i >= 0; --i) { - RTObject obj = state.getOutputStream().get(i); - - outputCountConsumed++; - - ControlCommand command = obj instanceof ControlCommand ? (ControlCommand) obj : null; - - if (command != null && command.getCommandType() == ControlCommand.CommandType.BeginString) { - break; - } - - if (obj instanceof StringValue) - contentStackForString.push(obj); - } - - // Consume the content that was produced for this string - state.popFromOutputStream(outputCountConsumed); - - // Build String out of the content we collected - StringBuilder sb = new StringBuilder(); - while (contentStackForString.size() > 0) { - RTObject c = contentStackForString.pop(); - sb.append(c.toString()); - } - - // Return to expression evaluation (from content mode) - state.setInExpressionEvaluation(true); - state.pushEvaluationStack(new StringValue(sb.toString())); - break; - - case ChoiceCount: - choiceCount = state.getGeneratedChoices().size(); - state.pushEvaluationStack(new IntValue(choiceCount)); - break; - - case Turns: - state.pushEvaluationStack(new IntValue(state.getCurrentTurnIndex() + 1)); - break; - - case TurnsSince: - case ReadCount: - RTObject target = state.popEvaluationStack(); - if (!(target instanceof DivertTargetValue)) { - String extraNote = ""; - if (target instanceof IntValue) - extraNote = ". Did you accidentally pass a read count ('knot_name') instead of a target ('-> knot_name')?"; - error("TURNS_SINCE expected a divert target (knot, stitch, label name), but saw " + target - + extraNote); - break; - } - - DivertTargetValue divertTarget = target instanceof DivertTargetValue ? (DivertTargetValue) target - : null; - - RTObject otmp = contentAtPath(divertTarget.getTargetPath()).correctObj(); - Container container = otmp instanceof Container ? (Container) otmp : null; - - int eitherCount; - - if (container != null) { - if (evalCommand.getCommandType() == ControlCommand.CommandType.TurnsSince) - eitherCount = state.turnsSinceForContainer(container); - else - eitherCount = state.visitCountForContainer(container); - } else { - if (evalCommand.getCommandType() == ControlCommand.CommandType.TurnsSince) - eitherCount = -1; // turn count, default to never/unknown - else - eitherCount = 0; // visit count, assume 0 to default to allowing entry - - warning("Failed to find container for " + evalCommand.toString() + " lookup at " - + divertTarget.getTargetPath().toString()); - } - - state.pushEvaluationStack(new IntValue(eitherCount)); - break; - - case Random: { - IntValue maxInt = null; - - RTObject o = state.popEvaluationStack(); - - if (o instanceof IntValue) - maxInt = (IntValue) o; - - IntValue minInt = null; - - o = state.popEvaluationStack(); - - if (o instanceof IntValue) - minInt = (IntValue) o; + /** + * General purpose delegate definition for bound EXTERNAL function definitions + * from ink. Note that this version isn't necessary if you have a function with + * three arguments or less. + * + * @param the result type + * @see ExternalFunction0 + * @see ExternalFunction1 + * @see ExternalFunction2 + * @see ExternalFunction3 + */ + public interface ExternalFunction { + R call(Object... args) throws Exception; + } + + /** + * EXTERNAL function delegate with zero arguments. + * + * @param the result type + */ + public abstract static class ExternalFunction0 implements ExternalFunction { + @Override + public final R call(Object... args) throws Exception { + if (args.length != 0) { + throw new IllegalArgumentException("Expecting 0 arguments."); + } + return call(); + } + + protected abstract R call() throws Exception; + } + + /** + * EXTERNAL function delegate with one argument. + * + * @param the argument type + * @param the result type + */ + public abstract static class ExternalFunction1 implements ExternalFunction { + @Override + public final R call(Object... args) throws Exception { + if (args.length != 1) { + throw new IllegalArgumentException("Expecting 1 argument."); + } + return call(coerceArg(args[0])); + } + + protected abstract R call(T t) throws Exception; + + @SuppressWarnings("unchecked") + protected T coerceArg(Object arg) throws Exception { + return (T) arg; + } + } + + /** + * EXTERNAL function delegate with two arguments. + * + * @param the first argument type + * @param the second argument type + * @param the result type + */ + public abstract static class ExternalFunction2 implements ExternalFunction { + @Override + public final R call(Object... args) throws Exception { + if (args.length != 2) { + throw new IllegalArgumentException("Expecting 2 arguments."); + } + return call(coerceArg0(args[0]), coerceArg1(args[1])); + } + + protected abstract R call(T1 t1, T2 t2) throws Exception; + + @SuppressWarnings("unchecked") + protected T1 coerceArg0(Object arg) throws Exception { + return (T1) arg; + } + + @SuppressWarnings("unchecked") + protected T2 coerceArg1(Object arg) throws Exception { + return (T2) arg; + } + } + + /** + * EXTERNAL function delegate with three arguments. + * + * @param the first argument type + * @param the second argument type + * @param the third argument type + * @param the result type + */ + public abstract static class ExternalFunction3 implements ExternalFunction { + @Override + public final R call(Object... args) throws Exception { + if (args.length != 3) { + throw new IllegalArgumentException("Expecting 3 arguments."); + } + return call(coerceArg0(args[0]), coerceArg1(args[1]), coerceArg2(args[2])); + } + + protected abstract R call(T1 t1, T2 t2, T3 t3) throws Exception; + + @SuppressWarnings("unchecked") + protected T1 coerceArg0(Object arg) throws Exception { + return (T1) arg; + } + + @SuppressWarnings("unchecked") + protected T2 coerceArg1(Object arg) throws Exception { + return (T2) arg; + } + + @SuppressWarnings("unchecked") + protected T3 coerceArg2(Object arg) throws Exception { + return (T3) arg; + } + } + + class ExternalFunctionDef { + public ExternalFunction function; + public boolean lookaheadSafe; + } + + // Version numbers are for engine itself and story file, rather + // than the story state save format + // -- old engine, new format: always fail + // -- new engine, old format: possibly cope, based on this number + // When incrementing the version number above, the question you + // should ask yourself is: + // -- Will the engine be able to load an old story file from + // before I made these changes to the engine? + // If possible, you should support it, though it's not as + // critical as loading old save games, since it's an + // in-development problem only. + + /** + * Delegate definition for variable observation - see ObserveVariable. + */ + public interface VariableObserver { + void call(String variableName, Object newValue); + } + + /** + * The current version of the ink story file format. + */ + public static final int inkVersionCurrent = 21; + + /** + * The minimum legacy version of ink that can be loaded by the current version + * of the code. + */ + public static final int inkVersionMinimumCompatible = 18; + + private Container mainContentContainer; + private ListDefinitionsOrigin listDefinitions; + + /** + * An ink file can provide a fallback functions for when when an EXTERNAL has + * been left unbound by the client, and the fallback function will be called + * instead. Useful when testing a story in playmode, when it's not possible to + * write a client-side C# external function, but you don't want it to fail to + * run. + */ + private boolean allowExternalFunctionFallbacks; + + private HashMap externals; + + private boolean hasValidatedExternals; + + private StoryState state; + + private Container temporaryEvaluationContainer; + + private HashMap> variableObservers; + + private List prevContainers = new ArrayList<>(); + + private Profiler profiler; + + private boolean asyncContinueActive; + private StoryState stateSnapshotAtLastNewline = null; + + private int recursiveContinueCount = 0; + + private boolean asyncSaving; + + private boolean sawLookaheadUnsafeFunctionAfterNewline = false; + + public Error.ErrorHandler onError = null; + + // Warning: When creating a Story using this constructor, you need to + // call ResetState on it before use. Intended for compiler use only. + // For normal use, use the constructor that takes a json string. + public Story(Container contentContainer, List lists) { + mainContentContainer = contentContainer; + + if (lists != null) { + listDefinitions = new ListDefinitionsOrigin(lists); + } + + externals = new HashMap<>(); + } + + public Story(Container contentContainer) { + this(contentContainer, null); + } + + /** + * Construct a Story Object using a JSON String compiled through inklecate. + */ + public Story(String jsonString) throws Exception { + this((Container) null); + HashMap rootObject = SimpleJson.textToDictionary(jsonString); + + Object versionObj = rootObject.get("inkVersion"); + if (versionObj == null) + throw new Exception("ink version number not found. Are you sure it's a valid .ink.json file?"); + + int formatFromFile = versionObj instanceof String ? Integer.parseInt((String) versionObj) : (int) versionObj; + + if (formatFromFile > inkVersionCurrent) { + throw new Exception("Version of ink used to build story was newer than the current version of the engine"); + } else if (formatFromFile < inkVersionMinimumCompatible) { + throw new Exception( + "Version of ink used to build story is too old to be loaded by this version of the engine"); + } else if (formatFromFile != inkVersionCurrent) { + System.out.println( + "WARNING: Version of ink used to build story doesn't match current version of engine. " + + "Non-critical, but recommend synchronising."); + } + + Object rootToken = rootObject.get("root"); + if (rootToken == null) + throw new Exception("Root node for ink not found. Are you sure it's a valid .ink.json file?"); + + Object listDefsObj = rootObject.get("listDefs"); + if (listDefsObj != null) { + listDefinitions = Json.jTokenToListDefinitions(listDefsObj); + } + + RTObject runtimeObject = Json.jTokenToRuntimeObject(rootToken); + mainContentContainer = runtimeObject instanceof Container ? (Container) runtimeObject : null; + + resetState(); + } + + void addError(String message) throws Exception { + addError(message, false, false); + } + + void warning(String message) throws Exception { + addError(message, true, false); + } + + void addError(String message, boolean isWarning, boolean useEndLineNumber) throws Exception { + DebugMetadata dm = currentDebugMetadata(); + + String errorTypeStr = isWarning ? "WARNING" : "ERROR"; + + if (dm != null) { + int lineNum = useEndLineNumber ? dm.endLineNumber : dm.startLineNumber; + message = String.format("RUNTIME %s: '%s' line %d: %s", errorTypeStr, dm.fileName, lineNum, message); + } else if (!state.getCurrentPointer().isNull()) { + message = String.format("RUNTIME %s: (%s): %s", errorTypeStr, + state.getCurrentPointer().getPath().toString(), message); + } else { + message = "RUNTIME " + errorTypeStr + ": " + message; + } + + state.addError(message, isWarning); + + // In a broken state don't need to know about any other errors. + if (!isWarning) + state.forceEnd(); + } + + /** + * Start recording ink profiling information during calls to Continue on Story. + * Return a Profiler instance that you can request a report from when you're + * finished. + * + * @throws Exception + */ + public Profiler startProfiling() throws Exception { + ifAsyncWeCant("start profiling"); + profiler = new Profiler(); + + return profiler; + } + + /** + * Stop recording ink profiling information during calls to Continue on Story. + * To generate a report from the profiler, call + */ + public void endProfiling() { + profiler = null; + } + + void Assert(boolean condition, Object... formatParams) throws Exception { + Assert(condition, null, formatParams); + } + + void Assert(boolean condition, String message, Object... formatParams) throws Exception { + if (!condition) { + if (message == null) { + message = "Story assert"; + } + if (formatParams != null && formatParams.length > 0) { + message = String.format(message, formatParams); + } + + throw new Exception(message + " " + currentDebugMetadata()); + } + } + + /** + * Binds a Java function to an ink EXTERNAL function. + * + * @param funcName EXTERNAL ink function name to bind to. + * @param func The Java function to bind. + * @param lookaheadSafe The ink engine often evaluates further than you might + * expect beyond the current line just in case it sees glue + * that will cause the two lines to become one. In this + * case it's possible that a function can appear to be + * called twice instead of just once, and earlier than you + * expect. If it's safe for your function to be called in + * this way (since the result and side effect of the + * function will not change), then you can pass 'true'. + * Usually, you want to pass 'false', especially if you + * want some action to be performed in game code when this + * function is called. + */ + public void bindExternalFunction(String funcName, ExternalFunction func, boolean lookaheadSafe) + throws Exception { + ifAsyncWeCant("bind an external function"); + Assert(!externals.containsKey(funcName), "Function '" + funcName + "' has already been bound."); + ExternalFunctionDef externalFunctionDef = new ExternalFunctionDef(); + externalFunctionDef.function = func; + externalFunctionDef.lookaheadSafe = lookaheadSafe; + + externals.put(funcName, externalFunctionDef); + } + + public void bindExternalFunction(String funcName, ExternalFunction func) throws Exception { + bindExternalFunction(funcName, func, true); + } + + @SuppressWarnings("unchecked") + public T tryCoerce(Object value, Class type) throws Exception { + + if (value == null) + return null; + + if (type.isAssignableFrom(value.getClass())) + return (T) value; + + if (value instanceof Float && type == Integer.class) { + Integer intVal = (int) Math.round((Float) value); + return (T) intVal; + } + + if (value instanceof Integer && type == Float.class) { + Float floatVal = Float.valueOf((Integer) value); + return (T) floatVal; + } + + if (value instanceof Integer && type == Boolean.class) { + int intVal = (Integer) value; + return (T) (intVal == 0 ? Boolean.FALSE : Boolean.TRUE); + } + + if (value instanceof Boolean && type == Integer.class) { + boolean val = (Boolean) value; + return (T) (val ? (Integer) 1 : (Integer) 0); + } + + if (type == String.class) { + return (T) value.toString(); + } + + Assert(false, "Failed to cast " + value.getClass().getCanonicalName() + " to " + type.getCanonicalName()); + + return null; + } + + /** + * Get any global tags associated with the story. These are defined as hash tags + * defined at the very top of the story. + * + * @throws Exception + */ + public List getGlobalTags() throws Exception { + return tagsAtStartOfFlowContainerWithPathString(""); + } + + /** + * Gets any tags associated with a particular knot or knot.stitch. These are + * defined as hash tags defined at the very top of a knot or stitch. + * + * @param path The path of the knot or stitch, in the form "knot" or + * "knot.stitch". + * @throws Exception + */ + public List tagsForContentAtPath(String path) throws Exception { + return tagsAtStartOfFlowContainerWithPathString(path); + } + + List tagsAtStartOfFlowContainerWithPathString(String pathString) throws Exception { + Path path = new Path(pathString); + + // Expected to be global story, knot or stitch + Container flowContainer = contentAtPath(path).getContainer(); + + while (true) { + RTObject firstContent = flowContainer.getContent().get(0); + if (firstContent instanceof Container) + flowContainer = (Container) firstContent; + else + break; + } + + // Any initial tag objects count as the "main tags" associated with that + // story/knot/stitch + boolean inTag = false; + List tags = null; + for (RTObject c : flowContainer.getContent()) { + + if (c instanceof ControlCommand) { + ControlCommand command = (ControlCommand) c; + + if (command.getCommandType() == ControlCommand.CommandType.BeginTag) { + inTag = true; + } else if (command.getCommandType() == ControlCommand.CommandType.EndTag) { + inTag = false; + } + } else if (inTag) { + if (c instanceof StringValue) { + StringValue str = (StringValue) c; + if (tags == null) tags = new ArrayList<>(); + tags.add(str.value); + } else { + error("Tag contained non-text content. Only plain text is allowed when using globalTags or " + + "TagsAtContentPath. If you want to evaluate dynamic content, you need to use story" + + ".Continue()."); + } + } + + // Any other content - we're done + // We only recognise initial text-only tags + else { + break; + } + } + + return tags; + } + + /** + * Useful when debugging a (very short) story, to visualise the state of the + * story. Add this call as a watch and open the extended text. A left-arrow mark + * will denote the current point of the story. It's only recommended that this + * is used on very short debug stories, since it can end up generate a large + * quantity of text otherwise. + */ + public String buildStringOfHierarchy() { + StringBuilder sb = new StringBuilder(); + + getMainContentContainer().buildStringOfHierarchy(sb, 0, state.getCurrentPointer().resolve()); + + return sb.toString(); + } + + public ExternalFunction getExternalFunction(String functionName) { + ExternalFunctionDef externalFunctionDef = externals.get(functionName); + + if (externalFunctionDef != null) { + return externalFunctionDef.function; + } + + return null; + } + + void callExternalFunction(String funcName, int numberOfArguments) throws Exception { + ExternalFunctionDef funcDef; + Container fallbackFunctionContainer = null; + + funcDef = externals.get(funcName); + + // Should this function break glue? Abort run if we've already seen a newline. + // Set a bool to tell it to restore the snapshot at the end of this instruction. + if (funcDef != null && !funcDef.lookaheadSafe && stateSnapshotAtLastNewline != null) { + sawLookaheadUnsafeFunctionAfterNewline = true; + return; + } + + // Try to use fallback function? + if (funcDef == null) { + if (allowExternalFunctionFallbacks) { + + fallbackFunctionContainer = knotContainerWithName(funcName); + + Assert(fallbackFunctionContainer != null, "Trying to call EXTERNAL function '" + funcName + + "' which has not been bound, and fallback ink function could not be found."); + + // Divert direct into fallback function and we're done + state.getCallStack().push(PushPopType.Function, 0, state.getOutputStream().size()); + state.setDivertedPointer(Pointer.startOf(fallbackFunctionContainer)); + return; + + } else { + Assert(false, "Trying to call EXTERNAL function '" + funcName + + "' which has not been bound (and ink fallbacks disabled)."); + } + } + + // Pop arguments + ArrayList arguments = new ArrayList<>(); + for (int i = 0; i < numberOfArguments; ++i) { + Value poppedObj = (Value) state.popEvaluationStack(); + Object valueObj = poppedObj.getValueObject(); + arguments.add(valueObj); + } + + // Reverse arguments from the order they were popped, + // so they're the right way round again. + Collections.reverse(arguments); + + // Run the function! + Object funcResult = funcDef.function.call(arguments.toArray()); + + // Convert return value (if any) to the a type that the ink engine can use + RTObject returnObj; + if (funcResult != null) { + returnObj = AbstractValue.create(funcResult); + Assert(returnObj != null, "Could not create ink value from returned Object of type " + + funcResult.getClass().getCanonicalName()); + } else { + returnObj = new Void(); + } + + state.pushEvaluationStack(returnObj); + } + + /** + * Check whether more content is available if you were to call Continue() - i.e. + * are we mid story rather than at a choice point or at the end. + * + * @return true if it's possible to call Continue() + */ + public boolean canContinue() { + return state.canContinue(); + } + + /** + * Chooses the Choice from the currentChoices list with the given index. + * Internally, this sets the current content path to that pointed to by the + * Choice, ready to continue story evaluation. + */ + public void chooseChoiceIndex(int choiceIdx) throws Exception { + List choices = getCurrentChoices(); + Assert(choiceIdx >= 0 && choiceIdx < choices.size(), "choice out of range"); + + // Replace callstack with the one from the thread at the choosing point, + // so that we can jump into the right place in the flow. + // This is important in case the flow was forked by a new thread, which + // can create multiple leading edges for the story, each of + // which has its own context. + Choice choiceToChoose = choices.get(choiceIdx); + state.getCallStack().setCurrentThread(choiceToChoose.getThreadAtGeneration()); + + choosePath(choiceToChoose.targetPath); + } + + void choosePath(Path p) throws Exception { + choosePath(p, true); + } + + void choosePath(Path p, boolean incrementingTurnIndex) throws Exception { + state.setChosenPath(p, incrementingTurnIndex); + + // Take a note of newly visited containers for read counts etc + visitChangedContainersDueToDivert(); + } + + /** + * Change the current position of the story to the given path. From here you can + * call Continue() to evaluate the next line. + *

+ * The path String is a dot-separated path as used ly by the engine. These + * examples should work: + *

+ * myKnot myKnot.myStitch + *

+ * Note however that this won't necessarily work: + *

+ * myKnot.myStitch.myLabelledChoice + *

+ * ...because of the way that content is nested within a weave structure. + *

+ * By default this will reset the callstack beforehand, which means that any + * tunnels, threads or functions you were in at the time of calling will be + * discarded. This is different from the behaviour of ChooseChoiceIndex, which + * will always keep the callstack, since the choices are known to come from the + * correct state, and known their source thread. + *

+ * You have the option of passing false to the resetCallstack parameter if you + * don't want this behaviour, and will leave any active threads, tunnels or + * function calls in-tact. + *

+ * This is potentially dangerous! If you're in the middle of a tunnel, it'll + * redirect only the inner-most tunnel, meaning that when you tunnel-return + * using '->->->', it'll return to where you were before. This may be + * what you want though. However, if you're in the middle of a function, + * ChoosePathString will throw an exception. + * + * @param path A dot-separted path string, as specified above. + * @param resetCallstack Whether to reset the callstack first (see summary + * description). + * @param arguments Optional set of arguments to pass, if path is to a knot + * that takes them. + */ + public void choosePathString(String path, boolean resetCallstack, Object[] arguments) throws Exception { + ifAsyncWeCant("call ChoosePathString right now"); + + if (resetCallstack) { + resetCallstack(); + } else { + // ChoosePathString is potentially dangerous since you can call it when the + // stack is + // pretty much in any state. Let's catch one of the worst offenders. + if (state.getCallStack().getCurrentElement().type == PushPopType.Function) { + String funcDetail = ""; + Container container = state.getCallStack().getCurrentElement().currentPointer.container; + if (container != null) { + funcDetail = "(" + container.getPath().toString() + ") "; + } + throw new Exception("Story was running a function " + funcDetail + "when you called ChoosePathString(" + + path + ") - this is almost certainly not not what you want! Full stack trace: \n" + + state.getCallStack().getCallStackTrace()); + } + } + + state.passArgumentsToEvaluationStack(arguments); + choosePath(new Path(path)); + } + + public void choosePathString(String path) throws Exception { + choosePathString(path, true, null); + } + + public void choosePathString(String path, boolean resetCallstack) throws Exception { + choosePathString(path, resetCallstack, null); + } + + void ifAsyncWeCant(String activityStr) throws Exception { + if (asyncContinueActive) + throw new Exception("Can't " + activityStr + + ". Story is in the middle of a ContinueAsync(). Make more ContinueAsync() calls or a single " + + "Continue() call beforehand."); + } + + SearchResult contentAtPath(Path path) throws Exception { + return getMainContentContainer().contentAtPath(path); + } + + Container knotContainerWithName(String name) { + + INamedContent namedContainer = mainContentContainer.getNamedContent().get(name); + + if (namedContainer != null) + return namedContainer instanceof Container ? (Container) namedContainer : null; + else + return null; + } + + /** + * The current flow name if using multi-flow functionality - see SwitchFlow + */ + public String getCurrentFlowName() { + return state.getCurrentFlowName(); + } + + /** + * Is the default flow currently active? By definition, will also return true if not using multi-flow + * functionality - see SwitchFlow + */ + public boolean currentFlowIsDefaultFlow() { + return state.currentFlowIsDefaultFlow(); + } + + /** + * Names of currently alive flows (not including the default flow) + */ + public List aliveFlowNames() { + return state.aliveFlowNames(); + } + + public void switchFlow(String flowName) throws Exception { + ifAsyncWeCant("switch flow"); + + if (asyncSaving) + throw new Exception("Story is already in background saving mode, can't switch flow to " + flowName); + + state.switchFlowInternal(flowName); + } + + public void removeFlow(String flowName) throws Exception { + state.removeFlowInternal(flowName); + } + + public void switchToDefaultFlow() throws Exception { + state.switchToDefaultFlowInternal(); + } + + /** + * Continue the story for one line of content, if possible. If you're not sure + * if there's more content available, for example if you want to check whether + * you're at a choice point or at the end of the story, you should call + * canContinue before calling this function. + * + * @return The line of text content. + */ + public String Continue() throws StoryException, Exception { + continueAsync(0); + return getCurrentText(); + } + + /** + * If ContinueAsync was called (with milliseconds limit > 0) then this + * property will return false if the ink evaluation isn't yet finished, and you + * need to call it again in order for the Continue to fully complete. + */ + public boolean asyncContinueComplete() { + return !asyncContinueActive; + } + + /** + * An "asnychronous" version of Continue that only partially evaluates the ink, + * with a budget of a certain time limit. It will exit ink evaluation early if + * the evaluation isn't complete within the time limit, with the + * asyncContinueComplete property being false. This is useful if ink evaluation + * takes a long time, and you want to distribute it over multiple game frames + * for smoother animation. If you pass a limit of zero, then it will fully + * evaluate the ink in the same way as calling Continue (and in fact, this + * exactly what Continue does internally). + */ + public void continueAsync(float millisecsLimitAsync) throws Exception { + if (!hasValidatedExternals) + validateExternalBindings(); + + continueInternal(millisecsLimitAsync); + } + + void continueInternal() throws Exception { + continueInternal(0); + } + + void continueInternal(float millisecsLimitAsync) throws Exception { + if (profiler != null) + profiler.preContinue(); + + boolean isAsyncTimeLimited = millisecsLimitAsync > 0; + + recursiveContinueCount++; + + // Doing either: + // - full run through non-async (so not active and don't want to be) + // - Starting async run-through + if (!asyncContinueActive) { + asyncContinueActive = isAsyncTimeLimited; + if (!canContinue()) { + throw new Exception("Can't continue - should check canContinue before calling Continue"); + } + + state.setDidSafeExit(false); + + state.resetOutput(); + + // It's possible for ink to call game to call ink to call game etc + // In this case, we only want to batch observe variable changes + // for the outermost call. + if (recursiveContinueCount == 1) + state.getVariablesState().setbatchObservingVariableChanges(true); + } + + // Start timing + Stopwatch durationStopwatch = new Stopwatch(); + durationStopwatch.start(); + + boolean outputStreamEndsInNewline = false; + sawLookaheadUnsafeFunctionAfterNewline = false; + do { + + try { + outputStreamEndsInNewline = continueSingleStep(); + } catch (StoryException e) { + addError(e.getMessage(), false, e.useEndLineNumber); + break; + } + + if (outputStreamEndsInNewline) + break; + + // Run out of async time? + if (asyncContinueActive && durationStopwatch.getElapsedMilliseconds() > millisecsLimitAsync) { + break; + } + + } while (canContinue()); + + durationStopwatch.stop(); + + // 4 outcomes: + // - got newline (so finished this line of text) + // - can't continue (e.g. choices or ending) + // - ran out of time during evaluation + // - error + // + // Successfully finished evaluation in time (or in error) + if (outputStreamEndsInNewline || !canContinue()) { + // Need to rewind, due to evaluating further than we should? + if (stateSnapshotAtLastNewline != null) { + restoreStateSnapshot(); + } + + // Finished a section of content / reached a choice point? + if (!canContinue()) { + if (state.getCallStack().canPopThread()) + addError("Thread available to pop, threads should always be flat by the end of evaluation?"); + + if (state.getGeneratedChoices().size() == 0 && !state.isDidSafeExit() + && temporaryEvaluationContainer == null) { + if (state.getCallStack().canPop(PushPopType.Tunnel)) + addError("unexpectedly reached end of content. Do you need a '->->' to return from a tunnel?"); + else if (state.getCallStack().canPop(PushPopType.Function)) + addError("unexpectedly reached end of content. Do you need a '~ return'?"); + else if (!state.getCallStack().canPop()) + addError("ran out of content. Do you need a '-> DONE' or '-> END'?"); + else + addError("unexpectedly reached end of content for unknown reason. Please debug compiler!"); + } + } + state.setDidSafeExit(false); + sawLookaheadUnsafeFunctionAfterNewline = false; + + if (recursiveContinueCount == 1) + state.getVariablesState().setbatchObservingVariableChanges(false); + asyncContinueActive = false; + } + + recursiveContinueCount--; + + if (profiler != null) + profiler.postContinue(); + + // Report any errors that occured during evaluation. + // This may either have been StoryExceptions that were thrown + // and caught during evaluation, or directly added with AddError. + if (state.hasError() || state.hasWarning()) { + if (onError != null) { + if (state.hasError()) { + for (String err : state.getCurrentErrors()) { + onError.error(err, ErrorType.Error); + } + } + if (state.hasWarning()) { + for (String err : state.getCurrentWarnings()) { + onError.error(err, ErrorType.Warning); + } + } + + resetErrors(); + } + // Throw an exception since there's no error handler + else { + StringBuilder sb = new StringBuilder(); + sb.append("Ink had "); + if (state.hasError()) { + sb.append(state.getCurrentErrors().size()); + sb.append(state.getCurrentErrors().size() == 1 ? " error" : " errors"); + if (state.hasWarning()) + sb.append(" and "); + } + if (state.hasWarning()) { + sb.append(state.getCurrentWarnings().size()); + sb.append(state.getCurrentWarnings().size() == 1 ? " warning" : " warnings"); + } + sb.append( + ". It is strongly suggested that you assign an error handler to story.onError. The first " + + "issue was: "); + sb.append(state.hasError() ? state.getCurrentErrors().get(0) : state.getCurrentWarnings().get(0)); + + // If you get this exception, please assign an error handler to your story. + // If you're using Unity, you can do something like this when you create + // your story: + // + // var story = new Ink.Runtime.Story(jsonTxt); + // story.onError = (errorMessage, errorType) => { + // if( errorType == ErrorType.Warning ) + // Debug.LogWarning(errorMessage); + // else + // Debug.LogError(errorMessage); + // }; + // + // + throw new StoryException(sb.toString()); + } + } + } + + boolean continueSingleStep() throws Exception { + if (profiler != null) + profiler.preStep(); + + // Run main step function (walks through content) + step(); + + if (profiler != null) + profiler.postStep(); + + // Run out of content and we have a default invisible choice that we can follow? + if (!canContinue() && !state.getCallStack().elementIsEvaluateFromGame()) { + + tryFollowDefaultInvisibleChoice(); + } + + if (profiler != null) + profiler.preSnapshot(); + + // Don't save/rewind during string evaluation, which is e.g. used for choices + if (!state.inStringEvaluation()) { + + // We previously found a newline, but were we just double checking that + // it wouldn't immediately be removed by glue? + if (stateSnapshotAtLastNewline != null) { + + // Has proper text or a tag been added? Then we know that the newline + // that was previously added is definitely the end of the line. + OutputStateChange change = calculateNewlineOutputStateChange( + stateSnapshotAtLastNewline.getCurrentText(), state.getCurrentText(), + stateSnapshotAtLastNewline.getCurrentTags().size(), state.getCurrentTags().size()); + + // The last time we saw a newline, it was definitely the end of the line, so we + // want to rewind to that point. + if (change == OutputStateChange.ExtendedBeyondNewline || sawLookaheadUnsafeFunctionAfterNewline) { + restoreStateSnapshot(); + + // Hit a newline for sure, we're done + return true; + } + + // Newline that previously existed is no longer valid - e.g. + // glue was encounted that caused it to be removed. + else if (change == OutputStateChange.NewlineRemoved) { + stateSnapshotAtLastNewline = null; + discardSnapshot(); + } + + } + + // Current content ends in a newline - approaching end of our evaluation + if (state.outputStreamEndsInNewline()) { + + // If we can continue evaluation for a bit: + // Create a snapshot in case we need to rewind. + // We're going to continue stepping in case we see glue or some + // non-text content such as choices. + if (canContinue()) { + + // Don't bother to record the state beyond the current newline. + // e.g.: + // Hello world\n // record state at the end of here + // ~ complexCalculation() // don't actually need this unless it generates text + if (stateSnapshotAtLastNewline == null) + stateSnapshot(); + } + + // Can't continue, so we're about to exit - make sure we + // don't have an old state hanging around. + else { + discardSnapshot(); + } + + } + + } + + if (profiler != null) + profiler.postSnapshot(); + + // outputStreamEndsInNewline = false + return false; + + } + + /** + * Continue the story until the next choice point or until it runs out of + * content. This is as opposed to the Continue() method which only evaluates one + * line of output at a time. + * + * @return The resulting text evaluated by the ink engine, concatenated + * together. + */ + public String continueMaximally() throws StoryException, Exception { + ifAsyncWeCant("ContinueMaximally"); + + StringBuilder sb = new StringBuilder(); + + while (canContinue()) { + sb.append(Continue()); + } + + return sb.toString(); + } + + DebugMetadata currentDebugMetadata() { + DebugMetadata dm; + + // Try to get from the current path first + final Pointer pointer = new Pointer(state.getCurrentPointer()); + if (!pointer.isNull()) { + dm = pointer.resolve().getDebugMetadata(); + if (dm != null) { + return dm; + } + } + + // Move up callstack if possible + for (int i = state.getCallStack().getElements().size() - 1; i >= 0; --i) { + pointer.assign(state.getCallStack().getElements().get(i).currentPointer); + if (!pointer.isNull() && pointer.resolve() != null) { + dm = pointer.resolve().getDebugMetadata(); + if (dm != null) { + return dm; + } + } + } + + // Current/previous path may not be valid if we've just had an error, + // or if we've simply run out of content. + // As a last resort, try to grab something from the output stream + for (int i = state.getOutputStream().size() - 1; i >= 0; --i) { + RTObject outputObj = state.getOutputStream().get(i); + dm = outputObj.getDebugMetadata(); + if (dm != null) { + return dm; + } + } + + return null; + } + + int currentLineNumber() throws Exception { + DebugMetadata dm = currentDebugMetadata(); + if (dm != null) { + return dm.startLineNumber; + } + return 0; + } + + void error(String message) throws Exception { + error(message, false); + } + + // Throw an exception that gets caught and causes AddError to be called, + // then exits the flow. + void error(String message, boolean useEndLineNumber) throws Exception { + StoryException e = new StoryException(message); + e.useEndLineNumber = useEndLineNumber; + throw e; + } + + // Evaluate a "hot compiled" piece of ink content, as used by the REPL-like + // CommandLinePlayer. + RTObject evaluateExpression(Container exprContainer) throws StoryException, Exception { + int startCallStackHeight = state.getCallStack().getElements().size(); + + state.getCallStack().push(PushPopType.Tunnel); + + temporaryEvaluationContainer = exprContainer; + + state.goToStart(); + + int evalStackHeight = state.getEvaluationStack().size(); + + Continue(); + + temporaryEvaluationContainer = null; + + // Should have fallen off the end of the Container, which should + // have auto-popped, but just in case we didn't for some reason, + // manually pop to restore the state (including currentPath). + if (state.getCallStack().getElements().size() > startCallStackHeight) { + state.popCallstack(); + } + + int endStackHeight = state.getEvaluationStack().size(); + if (endStackHeight > evalStackHeight) { + return state.popEvaluationStack(); + } else { + return null; + } + + } + + /** + * The list of Choice Objects available at the current point in the Story. This + * list will be populated as the Story is stepped through with the Continue() + * method. Once canContinue becomes false, this list will be populated, and is + * usually (but not always) on the final Continue() step. + */ + public List getCurrentChoices() { + + // Don't include invisible choices for external usage. + List choices = new ArrayList<>(); + for (Choice c : state.getCurrentChoices()) { + if (!c.isInvisibleDefault) { + c.setIndex(choices.size()); + choices.add(c); + } + } + + return choices; + } + + /** + * Gets a list of tags as defined with '#' in source that were seen during the + * latest Continue() call. + * + * @throws Exception + */ + public List getCurrentTags() throws Exception { + ifAsyncWeCant("call currentTags since it's a work in progress"); + return state.getCurrentTags(); + } + + /** + * Any warnings generated during evaluation of the Story. + */ + public List getCurrentWarnings() { + return state.getCurrentWarnings(); + } + + /** + * Any errors generated during evaluation of the Story. + */ + public List getCurrentErrors() { + return state.getCurrentErrors(); + } + + /** + * The latest line of text to be generated from a Continue() call. + * + * @throws Exception + */ + public String getCurrentText() throws Exception { + ifAsyncWeCant("call currentText since it's a work in progress"); + return state.getCurrentText(); + } + + /** + * The entire current state of the story including (but not limited to): + *

+ * * Global variables * Temporary variables * Read/visit and turn counts * The + * callstack and evaluation stacks * The current threads + */ + public StoryState getState() { + return state; + } + + /** + * The VariablesState Object contains all the global variables in the story. + * However, note that there's more to the state of a Story than just the global + * variables. This is a convenience accessor to the full state Object. + */ + public VariablesState getVariablesState() { + return state.getVariablesState(); + } + + public ListDefinitionsOrigin getListDefinitions() { + return listDefinitions; + } + + /** + * Whether the currentErrors list contains any errors. THIS MAY BE REMOVED - you + * should be setting an error handler directly using Story.onError. + */ + public boolean hasError() { + return state.hasError(); + } + + /** + * Whether the currentWarnings list contains any warnings. + */ + public boolean hasWarning() { + return state.hasWarning(); + } + + boolean incrementContentPointer() { + boolean successfulIncrement = true; + + Pointer pointer = new Pointer(state.getCallStack().getCurrentElement().currentPointer); + pointer.index++; + + // Each time we step off the end, we fall out to the next container, all + // the + // while we're in indexed rather than named content + while (pointer.index >= pointer.container.getContent().size()) { + + successfulIncrement = false; + + Container nextAncestor = pointer.container.getParent() instanceof Container + ? (Container) pointer.container.getParent() + : null; + + if (nextAncestor == null) { + break; + } + + int indexInAncestor = nextAncestor.getContent().indexOf(pointer.container); + if (indexInAncestor == -1) { + break; + } + + pointer = new Pointer(nextAncestor, indexInAncestor); + + // Increment to next content in outer container + pointer.index++; + + successfulIncrement = true; + } + + if (!successfulIncrement) + pointer.assign(Pointer.Null); + + state.getCallStack().getCurrentElement().currentPointer.assign(pointer); + + return successfulIncrement; + } + + // Does the expression result represented by this Object evaluate to true? + // e.g. is it a Number that's not equal to 1? + boolean isTruthy(RTObject obj) throws Exception { + boolean truthy = false; + if (obj instanceof Value) { + Value val = (Value) obj; + + if (val instanceof DivertTargetValue) { + DivertTargetValue divTarget = (DivertTargetValue) val; + error("Shouldn't use a divert target (to " + divTarget.getTargetPath() + + ") as a conditional value. Did you intend a function call 'likeThis()' or a read count " + + "check 'likeThis'? (no arrows)"); + return false; + } + + return val.isTruthy(); + } + return truthy; + } + + /** + * When the named global variable changes it's value, the observer will be + * called to notify it of the change. Note that if the value changes multiple + * times within the ink, the observer will only be called once, at the end of + * the ink's evaluation. If, during the evaluation, it changes and then changes + * back again to its original value, it will still be called. Note that the + * observer will also be fired if the value of the variable is changed + * externally to the ink, by directly setting a value in story.variablesState. + * + * @param variableName The name of the global variable to observe. + * @param observer A delegate function to call when the variable changes. + * @throws Exception + */ + public void observeVariable(String variableName, VariableObserver observer) throws Exception { + ifAsyncWeCant("observe a new variable"); + + if (variableObservers == null) + variableObservers = new HashMap<>(); + + if (!state.getVariablesState().globalVariableExistsWithName(variableName)) + throw new Exception( + "Cannot observe variable '" + variableName + "' because it wasn't declared in the ink story."); + + if (variableObservers.containsKey(variableName)) { + variableObservers.get(variableName).add(observer); + } else { + List l = new ArrayList<>(); + l.add(observer); + variableObservers.put(variableName, l); + } + } + + /** + * Convenience function to allow multiple variables to be observed with the same + * observer delegate function. See the singular ObserveVariable for details. The + * observer will get one call for every variable that has changed. + * + * @param variableNames The set of variables to observe. + * @param observer The delegate function to call when any of the named + * variables change. + * @throws Exception + * @throws StoryException + */ + public void observeVariables(List variableNames, VariableObserver observer) + throws StoryException, Exception { + for (String varName : variableNames) { + observeVariable(varName, observer); + } + } + + /** + * Removes the variable observer, to stop getting variable change notifications. + * If you pass a specific variable name, it will stop observing that particular + * one. If you pass null (or leave it blank, since it's optional), then the + * observer will be removed from all variables that it's subscribed to. + * + * @param observer The observer to stop observing. + * @param specificVariableName (Optional) Specific variable name to stop + * observing. + * @throws Exception + */ + public void removeVariableObserver(VariableObserver observer, String specificVariableName) throws Exception { + ifAsyncWeCant("remove a variable observer"); + + if (variableObservers == null) + return; + + // Remove observer for this specific variable + if (specificVariableName != null) { + if (variableObservers.containsKey(specificVariableName)) { + variableObservers.get(specificVariableName).remove(observer); + if (variableObservers.get(specificVariableName).size() == 0) { + variableObservers.remove(specificVariableName); + } + } + } else { + // Remove observer for all variables + for (Map.Entry> obs : variableObservers.entrySet()) { + obs.getValue().remove(observer); + if (obs.getValue().size() == 0) { + variableObservers.remove(obs.getKey()); + } + } + } + } + + public void removeVariableObserver(VariableObserver observer) throws Exception { + removeVariableObserver(observer, null); + } + + @Override + public void variableStateDidChangeEvent(String variableName, RTObject newValueObj) throws Exception { + if (variableObservers == null) + return; + + List observers = variableObservers.get(variableName); + + if (observers != null) { + if (!(newValueObj instanceof Value)) { + throw new Exception("Tried to get the value of a variable that isn't a standard type"); + } + + Value val = (Value) newValueObj; + + for (VariableObserver o : observers) { + o.call(variableName, val.getValueObject()); + } + } + } + + public Container getMainContentContainer() { + if (temporaryEvaluationContainer != null) { + return temporaryEvaluationContainer; + } else { + return mainContentContainer; + } + } + + String buildStringOfContainer(Container container) { + StringBuilder sb = new StringBuilder(); + + container.buildStringOfHierarchy(sb, 0, state.getCurrentPointer().resolve()); + + return sb.toString(); + } + + private void nextContent() throws Exception { + // Setting previousContentObject is critical for + // VisitChangedContainersDueToDivert + state.setPreviousPointer(state.getCurrentPointer()); + + // Divert step? + if (!state.getDivertedPointer().isNull()) { + + state.setCurrentPointer(state.getDivertedPointer()); + state.setDivertedPointer(Pointer.Null); + + // Internally uses state.previousContentObject and + // state.currentContentObject + visitChangedContainersDueToDivert(); + + // Diverted location has valid content? + if (!state.getCurrentPointer().isNull()) { + return; + } + + // Otherwise, if diverted location doesn't have valid content, + // drop down and attempt to increment. + // This can happen if the diverted path is intentionally jumping + // to the end of a container - e.g. a Conditional that's re-joining + } + + boolean successfulPointerIncrement = incrementContentPointer(); + + // Ran out of content? Try to auto-exit from a function, + // or finish evaluating the content of a thread + if (!successfulPointerIncrement) { + + boolean didPop = false; + + if (state.getCallStack().canPop(PushPopType.Function)) { + + // Pop from the call stack + state.popCallstack(PushPopType.Function); + + // This pop was due to dropping off the end of a function that + // didn't return anything, + // so in this case, we make sure that the evaluator has + // something to chomp on if it needs it + if (state.getInExpressionEvaluation()) { + state.pushEvaluationStack(new Void()); + } + + didPop = true; + } else if (state.getCallStack().canPopThread()) { + state.getCallStack().popThread(); + + didPop = true; + } else { + state.tryExitFunctionEvaluationFromGame(); + } + + // Step past the point where we last called out + if (didPop && !state.getCurrentPointer().isNull()) { + nextContent(); + } + } + } + + // Note that this is O(n), since it re-evaluates the shuffle indices + // from a consistent seed each time. + // TODO: Is this the best algorithm it can be? + int nextSequenceShuffleIndex() throws Exception { + RTObject popEvaluationStack = state.popEvaluationStack(); + + IntValue numElementsIntVal = popEvaluationStack instanceof IntValue ? (IntValue) popEvaluationStack : null; + + if (numElementsIntVal == null) { + error("expected number of elements in sequence for shuffle index"); + return 0; + } + + Container seqContainer = state.getCurrentPointer().container; + + int numElements = numElementsIntVal.value; + + IntValue seqCountVal = (IntValue) state.popEvaluationStack(); + int seqCount = seqCountVal.value; + int loopIndex = seqCount / numElements; + int iterationIndex = seqCount % numElements; + + // Generate the same shuffle based on: + // - The hash of this container, to make sure it's consistent + // each time the runtime returns to the sequence + // - How many times the runtime has looped around this full shuffle + String seqPathStr = seqContainer.getPath().toString(); + int sequenceHash = 0; + for (char c : seqPathStr.toCharArray()) { + sequenceHash += c; + } + + int randomSeed = sequenceHash + loopIndex + state.getStorySeed(); + + Random random = new Random(randomSeed); + + ArrayList unpickedIndices = new ArrayList<>(); + for (int i = 0; i < numElements; ++i) { + unpickedIndices.add(i); + } + + for (int i = 0; i <= iterationIndex; ++i) { + int chosen = random.nextInt(Integer.MAX_VALUE) % unpickedIndices.size(); + int chosenIndex = unpickedIndices.get(chosen); + unpickedIndices.remove(chosen); + + if (i == iterationIndex) { + return chosenIndex; + } + } + + throw new Exception("Should never reach here"); + } + + /** + * Checks whether contentObj is a control or flow Object rather than a piece of + * content, and performs the required command if necessary. + * + * @param contentObj Content Object. + * @return true if Object was logic or flow control, false if it's normal + * content. + */ + boolean performLogicAndFlowControl(RTObject contentObj) throws Exception { + if (contentObj == null) { + return false; + } + + // Divert + if (contentObj instanceof Divert) { + + Divert currentDivert = (Divert) contentObj; + + if (currentDivert.isConditional()) { + RTObject conditionValue = state.popEvaluationStack(); + + // False conditional? Cancel divert + if (!isTruthy(conditionValue)) + return true; + } + + if (currentDivert.hasVariableTarget()) { + String varName = currentDivert.getVariableDivertName(); + + RTObject varContents = state.getVariablesState().getVariableWithName(varName); + + if (varContents == null) { + error("Tried to divert using a target from a variable that could not be found (" + varName + ")"); + } else if (!(varContents instanceof DivertTargetValue)) { + + IntValue intContent = varContents instanceof IntValue ? (IntValue) varContents : null; + + String errorMessage = "Tried to divert to a target from a variable, but the variable (" + varName + + ") didn't contain a divert target, it "; + if (intContent != null && intContent.value == 0) { + errorMessage += "was empty/null (the value 0)."; + } else { + errorMessage += "contained '" + varContents + "'."; + } + + error(errorMessage); + } + + DivertTargetValue target = (DivertTargetValue) varContents; + state.setDivertedPointer(pointerAtPath(target.getTargetPath())); + + } else if (currentDivert.isExternal()) { + callExternalFunction(currentDivert.getTargetPathString(), currentDivert.getExternalArgs()); + return true; + } else { + state.setDivertedPointer(currentDivert.getTargetPointer()); + } + + if (currentDivert.getPushesToStack()) { + state.getCallStack().push(currentDivert.getStackPushType(), 0, state.getOutputStream().size()); + } + + if (state.getDivertedPointer().isNull() && !currentDivert.isExternal()) { + + // Human readable name available - runtime divert is part of a + // hard-written divert that to missing content + if (currentDivert.getDebugMetadata().sourceName != null) { + error("Divert target doesn't exist: " + currentDivert.getDebugMetadata().sourceName); + } else { + error("Divert resolution failed: " + currentDivert); + } + } + + return true; + } + + // Start/end an expression evaluation? Or print out the result? + else if (contentObj instanceof ControlCommand) { + ControlCommand evalCommand = (ControlCommand) contentObj; + + int choiceCount; + switch (evalCommand.getCommandType()) { + + case EvalStart: + Assert(!state.getInExpressionEvaluation(), "Already in expression evaluation?"); + state.setInExpressionEvaluation(true); + break; + + case EvalEnd: + Assert(state.getInExpressionEvaluation(), "Not in expression evaluation mode"); + state.setInExpressionEvaluation(false); + break; + + case EvalOutput: + + // If the expression turned out to be empty, there may not be + // anything on the stack + if (state.getEvaluationStack().size() > 0) { + + RTObject output = state.popEvaluationStack(); + + // Functions may evaluate to Void, in which case we skip + // output + if (!(output instanceof Void)) { + // TODO: Should we really always blanket convert to + // string? + // It would be okay to have numbers in the output stream + // the + // only problem is when exporting text for viewing, it + // skips over numbers etc. + StringValue text = new StringValue(output.toString()); + + state.pushToOutputStream(text); + } + + } + break; + + case NoOp: + break; + + case Duplicate: + state.pushEvaluationStack(state.peekEvaluationStack()); + break; + + case PopEvaluatedValue: + state.popEvaluationStack(); + break; + + case PopFunction: + case PopTunnel: + + PushPopType popType = evalCommand.getCommandType() == ControlCommand.CommandType.PopFunction + ? PushPopType.Function + : PushPopType.Tunnel; + + // Tunnel onwards is allowed to specify an optional override + // divert to go to immediately after returning: ->-> target + DivertTargetValue overrideTunnelReturnTarget = null; + if (popType == PushPopType.Tunnel) { + RTObject popped = state.popEvaluationStack(); + + if (popped instanceof DivertTargetValue) { + overrideTunnelReturnTarget = (DivertTargetValue) popped; + } + + if (overrideTunnelReturnTarget == null) { + Assert(popped instanceof Void, "Expected void if ->-> doesn't override target"); + } + } + + if (state.tryExitFunctionEvaluationFromGame()) { + break; + } else if (state.getCallStack().getCurrentElement().type != popType || !state.getCallStack() + .canPop()) { + + HashMap names = new HashMap<>(); + names.put(PushPopType.Function, "function return statement (~ return)"); + names.put(PushPopType.Tunnel, "tunnel onwards statement (->->)"); + + String expected = names.get(state.getCallStack().getCurrentElement().type); + if (!state.getCallStack().canPop()) { + expected = "end of flow (-> END or choice)"; + } + + String errorMsg = String.format("Found %s, when expected %s", names.get(popType), expected); + + error(errorMsg); + } else { + state.popCallstack(); + + // Does tunnel onwards override by diverting to a new ->-> + // target? + if (overrideTunnelReturnTarget != null) + state.setDivertedPointer(pointerAtPath(overrideTunnelReturnTarget.getTargetPath())); + } + break; + + case BeginString: + state.pushToOutputStream(evalCommand); + + Assert(state.getInExpressionEvaluation(), + "Expected to be in an expression when evaluating a string"); + state.setInExpressionEvaluation(false); + break; + // Leave it to story.currentText and story.currentTags to sort out the text from the tags + // This is mostly because we can't always rely on the existence of EndTag, and we don't want + // to try and flatten dynamic tags to strings every time \n is pushed to output + case BeginTag: + state.pushToOutputStream(evalCommand); + break; + case EndTag: { + + // EndTag has 2 modes: + // - When in string evaluation (for choices) + // - Normal + // + // The only way you could have an EndTag in the middle of + // string evaluation is if we're currently generating text for a + // choice, such as: + // + // + choice # tag + // + // In the above case, the ink will be run twice: + // - First, to generate the choice text. String evaluation + // will be on, and the final string will be pushed to the + // evaluation stack, ready to be popped to make a Choice + // object. + // - Second, when ink generates text after choosing the choice. + // On this ocassion, it's not in string evaluation mode. + // + // On the writing side, we disallow manually putting tags within + // strings like this: + // + // {"hello # world"} + // + // So we know that the tag must be being generated as part of + // choice content. Therefore, when the tag has been generated, + // we push it onto the evaluation stack in the exact same way + // as the string for the choice content. + if (state.inStringEvaluation()) { + + Stack contentStackForTag = new Stack<>(); + int outputCountConsumed = 0; + + for (int i = state.getOutputStream().size() - 1; i >= 0; --i) { + RTObject obj = state.getOutputStream().get(i); + + outputCountConsumed++; + + if (obj instanceof ControlCommand) { + ControlCommand command = (ControlCommand) obj; + if (command.getCommandType() == ControlCommand.CommandType.BeginTag) { + break; + } else { + error("Unexpected ControlCommand while extracting tag from choice"); + break; + } + } + + if (obj instanceof StringValue) + contentStackForTag.push((StringValue) obj); + } + + // Consume the content that was produced for this string + state.popFromOutputStream(outputCountConsumed); + + StringBuilder sb = new StringBuilder(); + for (StringValue strVal : contentStackForTag) { + sb.append(strVal.value); + } + + Tag choiceTag = new Tag(state.cleanOutputWhitespace(sb.toString())); + // Pushing to the evaluation stack means it gets picked up + // when a Choice is generated from the next Choice Point. + state.pushEvaluationStack(choiceTag); + } + + // Otherwise! Simply push EndTag, so that in the output stream we + // have a structure of: [BeginTag, "the tag content", EndTag] + else { + state.pushToOutputStream(evalCommand); + } + break; + } + // Dynamic strings and tags are built in the same way + case EndString: { + + // Since we're iterating backward through the content, + // build a stack so that when we build the string, + // it's in the right order + Stack contentStackForString = new Stack<>(); + Stack contentToRetain = new Stack<>(); + + int outputCountConsumed = 0; + for (int i = state.getOutputStream().size() - 1; i >= 0; --i) { + RTObject obj = state.getOutputStream().get(i); + + outputCountConsumed++; + + ControlCommand command = obj instanceof ControlCommand ? (ControlCommand) obj : null; + + if (command != null && command.getCommandType() == ControlCommand.CommandType.BeginString) { + break; + } + + if (obj instanceof Tag) + contentToRetain.push(obj); + + if (obj instanceof StringValue) + contentStackForString.push(obj); + } + + // Consume the content that was produced for this string + state.popFromOutputStream(outputCountConsumed); + + // Rescue the tags that we want actually to keep on the output stack + // rather than consume as part of the string we're building. + // At the time of writing, this only applies to Tag objects generated + // by choices, which are pushed to the stack during string generation. + for (RTObject rescuedTag : contentToRetain) + state.pushToOutputStream(rescuedTag); + + // Build string out of the content we collected + StringBuilder sb = new StringBuilder(); + + while (contentStackForString.size() > 0) { + RTObject c = contentStackForString.pop(); + + sb.append(c.toString()); + } + + // Return to expression evaluation (from content mode) + state.setInExpressionEvaluation(true); + state.pushEvaluationStack(new StringValue(sb.toString())); + break; + } + case ChoiceCount: + choiceCount = state.getGeneratedChoices().size(); + state.pushEvaluationStack(new IntValue(choiceCount)); + break; + + case Turns: + state.pushEvaluationStack(new IntValue(state.getCurrentTurnIndex() + 1)); + break; + + case TurnsSince: + case ReadCount: + RTObject target = state.popEvaluationStack(); + if (!(target instanceof DivertTargetValue)) { + String extraNote = ""; + if (target instanceof IntValue) + extraNote = ". Did you accidentally pass a read count ('knot_name') instead of a target " + + "('-> knot_name')?"; + error("TURNS_SINCE expected a divert target (knot, stitch, label name), but saw " + target + + extraNote); + break; + } + + DivertTargetValue divertTarget = target instanceof DivertTargetValue ? (DivertTargetValue) target + : null; + + RTObject otmp = contentAtPath(divertTarget.getTargetPath()).correctObj(); + Container container = otmp instanceof Container ? (Container) otmp : null; + + int eitherCount; + + if (container != null) { + if (evalCommand.getCommandType() == ControlCommand.CommandType.TurnsSince) + eitherCount = state.turnsSinceForContainer(container); + else + eitherCount = state.visitCountForContainer(container); + } else { + if (evalCommand.getCommandType() == ControlCommand.CommandType.TurnsSince) + eitherCount = -1; // turn count, default to never/unknown + else + eitherCount = 0; // visit count, assume 0 to default to allowing entry + + warning("Failed to find container for " + evalCommand.toString() + " lookup at " + + divertTarget.getTargetPath().toString()); + } + + state.pushEvaluationStack(new IntValue(eitherCount)); + break; + + case Random: { + IntValue maxInt = null; + + RTObject o = state.popEvaluationStack(); + + if (o instanceof IntValue) + maxInt = (IntValue) o; - if (minInt == null) - error("Invalid value for minimum parameter of RANDOM(min, max)"); + IntValue minInt = null; - if (maxInt == null) - error("Invalid value for maximum parameter of RANDOM(min, max)"); + o = state.popEvaluationStack(); - // +1 because it's inclusive of min and max, for e.g. - // RANDOM(1,6) for a dice roll. - int randomRange = maxInt.value - minInt.value + 1; - if (randomRange <= 0) - error("RANDOM was called with minimum as " + minInt.value + " and maximum as " + maxInt.value - + ". The maximum must be larger"); + if (o instanceof IntValue) + minInt = (IntValue) o; - int resultSeed = state.getStorySeed() + state.getPreviousRandom(); - Random random = new Random(resultSeed); + if (minInt == null) + error("Invalid value for minimum parameter of RANDOM(min, max)"); + + if (maxInt == null) + error("Invalid value for maximum parameter of RANDOM(min, max)"); - int nextRandom = random.nextInt(Integer.MAX_VALUE); - int chosenValue = (nextRandom % randomRange) + minInt.value; - state.pushEvaluationStack(new IntValue(chosenValue)); + // +1 because it's inclusive of min and max, for e.g. + // RANDOM(1,6) for a dice roll. + int randomRange = maxInt.value - minInt.value + 1; + if (randomRange <= 0) + error("RANDOM was called with minimum as " + minInt.value + " and maximum as " + maxInt.value + + ". The maximum must be larger"); - // Next random number (rather than keeping the Random object - // around) - state.setPreviousRandom(state.getPreviousRandom() + 1); - break; - } + int resultSeed = state.getStorySeed() + state.getPreviousRandom(); + Random random = new Random(resultSeed); - case SeedRandom: { - IntValue seed = null; + int nextRandom = random.nextInt(Integer.MAX_VALUE); + int chosenValue = (nextRandom % randomRange) + minInt.value; + state.pushEvaluationStack(new IntValue(chosenValue)); - RTObject o = state.popEvaluationStack(); + // Next random number (rather than keeping the Random object + // around) + state.setPreviousRandom(state.getPreviousRandom() + 1); + break; + } - if (o instanceof IntValue) - seed = (IntValue) o; + case SeedRandom: { + IntValue seed = null; - if (seed == null) - error("Invalid value passed to SEED_RANDOM"); + RTObject o = state.popEvaluationStack(); - // Story seed affects both RANDOM and shuffle behaviour - state.setStorySeed(seed.value); - state.setPreviousRandom(0); + if (o instanceof IntValue) + seed = (IntValue) o; - // SEED_RANDOM returns nothing. - state.pushEvaluationStack(new Void()); - break; - } - case VisitIndex: - int count = state.visitCountForContainer(state.getCurrentPointer().container) - 1; // index - // not - // count - state.pushEvaluationStack(new IntValue(count)); - break; + if (seed == null) + error("Invalid value passed to SEED_RANDOM"); - case SequenceShuffleIndex: - int shuffleIndex = nextSequenceShuffleIndex(); - state.pushEvaluationStack(new IntValue(shuffleIndex)); - break; + // Story seed affects both RANDOM and shuffle behaviour + state.setStorySeed(seed.value); + state.setPreviousRandom(0); - case StartThread: - // Handled in main step function - break; + // SEED_RANDOM returns nothing. + state.pushEvaluationStack(new Void()); + break; + } + case VisitIndex: + int count = state.visitCountForContainer(state.getCurrentPointer().container) - 1; // index + // not + // count + state.pushEvaluationStack(new IntValue(count)); + break; - case Done: + case SequenceShuffleIndex: + int shuffleIndex = nextSequenceShuffleIndex(); + state.pushEvaluationStack(new IntValue(shuffleIndex)); + break; - // We may exist in the context of the initial - // act of creating the thread, or in the context of - // evaluating the content. - if (state.getCallStack().canPopThread()) { - state.getCallStack().popThread(); - } + case StartThread: + // Handled in main step function + break; - // In normal flow - allow safe exit without warning - else { - state.setDidSafeExit(true); + case Done: - // Stop flow in current thread - state.setCurrentPointer(Pointer.Null); - } + // We may exist in the context of the initial + // act of creating the thread, or in the context of + // evaluating the content. + if (state.getCallStack().canPopThread()) { + state.getCallStack().popThread(); + } - break; + // In normal flow - allow safe exit without warning + else { + state.setDidSafeExit(true); - // Force flow to end completely - case End: - state.forceEnd(); - break; + // Stop flow in current thread + state.setCurrentPointer(Pointer.Null); + } - case ListFromInt: { - IntValue intVal = null; + break; - RTObject o = state.popEvaluationStack(); + // Force flow to end completely + case End: + state.forceEnd(); + break; - if (o instanceof IntValue) - intVal = (IntValue) o; + case ListFromInt: { + IntValue intVal = null; - StringValue listNameVal = null; + RTObject o = state.popEvaluationStack(); - o = state.popEvaluationStack(); + if (o instanceof IntValue) + intVal = (IntValue) o; - if (o instanceof StringValue) - listNameVal = (StringValue) o; + StringValue listNameVal = null; - if (intVal == null) { - throw new StoryException("Passed non-integer when creating a list element from a numerical value."); - } + o = state.popEvaluationStack(); - ListValue generatedListValue = null; + if (o instanceof StringValue) + listNameVal = (StringValue) o; - ListDefinition foundListDef = listDefinitions.getListDefinition(listNameVal.value); + if (intVal == null) { + throw new StoryException( + "Passed non-integer when creating a list element from a numerical value."); + } - if (foundListDef != null) { - InkListItem foundItem; + ListValue generatedListValue = null; - foundItem = foundListDef.getItemWithValue(intVal.value); + ListDefinition foundListDef = listDefinitions.getListDefinition(listNameVal.value); - if (foundItem != null) { - generatedListValue = new ListValue(foundItem, intVal.value); - } - } else { - throw new StoryException("Failed to find List called " + listNameVal.value); - } + if (foundListDef != null) { + InkListItem foundItem; - if (generatedListValue == null) - generatedListValue = new ListValue(); + foundItem = foundListDef.getItemWithValue(intVal.value); - state.pushEvaluationStack(generatedListValue); - break; - } + if (foundItem != null) { + generatedListValue = new ListValue(foundItem, intVal.value); + } + } else { + throw new StoryException("Failed to find List called " + listNameVal.value); + } - case ListRange: { - RTObject p = state.popEvaluationStack(); - Value max = p instanceof Value ? (Value) p : null; + if (generatedListValue == null) + generatedListValue = new ListValue(); - p = state.popEvaluationStack(); - Value min = p instanceof Value ? (Value) p : null; + state.pushEvaluationStack(generatedListValue); + break; + } - p = state.popEvaluationStack(); - ListValue targetList = p instanceof ListValue ? (ListValue) p : null; + case ListRange: { + RTObject p = state.popEvaluationStack(); + Value max = p instanceof Value ? (Value) p : null; - if (targetList == null || min == null || max == null) - throw new StoryException("Expected List, minimum and maximum for LIST_RANGE"); + p = state.popEvaluationStack(); + Value min = p instanceof Value ? (Value) p : null; - InkList result = targetList.value.listWithSubRange(min.getValueObject(), max.getValueObject()); + p = state.popEvaluationStack(); + ListValue targetList = p instanceof ListValue ? (ListValue) p : null; - state.pushEvaluationStack(new ListValue(result)); - break; - } + if (targetList == null || min == null || max == null) + throw new StoryException("Expected List, minimum and maximum for LIST_RANGE"); - case ListRandom: { + InkList result = targetList.value.listWithSubRange(min.getValueObject(), max.getValueObject()); - RTObject o = state.popEvaluationStack(); - ListValue listVal = o instanceof ListValue ? (ListValue) o : null; + state.pushEvaluationStack(new ListValue(result)); + break; + } - if (listVal == null) - throw new StoryException("Expected list for LIST_RANDOM"); + case ListRandom: { - InkList list = listVal.value; + RTObject o = state.popEvaluationStack(); + ListValue listVal = o instanceof ListValue ? (ListValue) o : null; - InkList newList = null; + if (listVal == null) + throw new StoryException("Expected list for LIST_RANDOM"); - // List was empty: return empty list - if (list.size() == 0) { - newList = new InkList(); - } + InkList list = listVal.value; - // Non-empty source list - else { - // Generate a random index for the element to take - int resultSeed = state.getStorySeed() + state.getPreviousRandom(); - Random random = new Random(resultSeed); + InkList newList = null; - int nextRandom = random.nextInt(Integer.MAX_VALUE); - int listItemIndex = nextRandom % list.size(); + // List was empty: return empty list + if (list.size() == 0) { + newList = new InkList(); + } - // Iterate through to get the random element - Iterator> listEnumerator = list.entrySet().iterator(); + // Non-empty source list + else { + // Generate a random index for the element to take + int resultSeed = state.getStorySeed() + state.getPreviousRandom(); + Random random = new Random(resultSeed); - Entry randomItem = null; + int nextRandom = random.nextInt(Integer.MAX_VALUE); + int listItemIndex = nextRandom % list.size(); - for (int i = 0; i <= listItemIndex; i++) { - randomItem = listEnumerator.next(); - } + // Iterate through to get the random element + Iterator> listEnumerator = list.entrySet().iterator(); - // Origin list is simply the origin of the one element - newList = new InkList(randomItem.getKey().getOriginName(), this); - newList.put(randomItem.getKey(), randomItem.getValue()); + Entry randomItem = null; - state.setPreviousRandom(nextRandom); - } + for (int i = 0; i <= listItemIndex; i++) { + randomItem = listEnumerator.next(); + } - state.pushEvaluationStack(new ListValue(newList)); - break; - } + // Origin list is simply the origin of the one element + newList = new InkList(randomItem.getKey().getOriginName(), this); + newList.put(randomItem.getKey(), randomItem.getValue()); - default: - error("unhandled ControlCommand: " + evalCommand); - break; - } + state.setPreviousRandom(nextRandom); + } - return true; - } + state.pushEvaluationStack(new ListValue(newList)); + break; + } - // Variable assignment - else if (contentObj instanceof VariableAssignment) + default: + error("unhandled ControlCommand: " + evalCommand); + break; + } - { - VariableAssignment varAss = (VariableAssignment) contentObj; - RTObject assignedVal = state.popEvaluationStack(); + return true; + } - // When in temporary evaluation, don't create new variables purely - // within - // the temporary context, but attempt to create them globally - // var prioritiseHigherInCallStack = _temporaryEvaluationContainer - // != null; - - state.getVariablesState().assign(varAss, assignedVal); - - return true; - } - - // Variable reference - else if (contentObj instanceof VariableReference) { - VariableReference varRef = (VariableReference) contentObj; - RTObject foundValue = null; - - // Explicit read count value - if (varRef.getPathForCount() != null) { - - Container container = varRef.getContainerForCount(); - int count = state.visitCountForContainer(container); - foundValue = new IntValue(count); - } - - // Normal variable reference - else { - - foundValue = state.getVariablesState().getVariableWithName(varRef.getName()); - - if (foundValue == null) { - warning("Variable not found: '" + varRef.getName() - + "'. Using default value of 0 (false). This can happen with temporary variables if the declaration hasn't yet been hit. Globals are always given a default value on load if a value doesn't exist in the save state."); - foundValue = new IntValue(0); - } - } - - state.pushEvaluationStack(foundValue); - - return true; - } - - // Native function call - else if (contentObj instanceof NativeFunctionCall) { - NativeFunctionCall func = (NativeFunctionCall) contentObj; - List funcParams = state.popEvaluationStack(func.getNumberOfParameters()); - - RTObject result = func.call(funcParams); - state.pushEvaluationStack(result); - return true; - } - - // No control content, must be ordinary content - return false; - } - - // Assumption: prevText is the snapshot where we saw a newline, and we're - // checking whether we're really done - // with that line. Therefore prevText will definitely end in a newline. - // - // We take tags into account too, so that a tag following a content line: - // Content - // # tag - // ... doesn't cause the tag to be wrongly associated with the content above. - enum OutputStateChange { - NoChange, ExtendedBeyondNewline, NewlineRemoved - } - - OutputStateChange calculateNewlineOutputStateChange(String prevText, String currText, int prevTagCount, - int currTagCount) { - // Simple case: nothing's changed, and we still have a newline - // at the end of the current content - boolean newlineStillExists = currText.length() >= prevText.length() - && currText.charAt(prevText.length() - 1) == '\n'; - if (prevTagCount == currTagCount && prevText.length() == currText.length() && newlineStillExists) - return OutputStateChange.NoChange; - - // Old newline has been removed, it wasn't the end of the line after all - if (!newlineStillExists) { - return OutputStateChange.NewlineRemoved; - } - - // Tag added - definitely the start of a new line - if (currTagCount > prevTagCount) - return OutputStateChange.ExtendedBeyondNewline; - - // There must be new content - check whether it's just whitespace - for (int i = prevText.length(); i < currText.length(); i++) { - char c = currText.charAt(i); - if (c != ' ' && c != '\t') { - return OutputStateChange.ExtendedBeyondNewline; - } - } - - // There's new text but it's just spaces and tabs, so there's still the - // potential - // for glue to kill the newline. - return OutputStateChange.NoChange; - } - - Choice processChoice(ChoicePoint choicePoint) throws Exception { - boolean showChoice = true; - - // Don't create choice if choice point doesn't pass conditional - if (choicePoint.hasCondition()) { - RTObject conditionValue = state.popEvaluationStack(); - if (!isTruthy(conditionValue)) { - showChoice = false; - } - } - - String startText = ""; - String choiceOnlyText = ""; - - if (choicePoint.hasChoiceOnlyContent()) { - StringValue choiceOnlyStrVal = (StringValue) state.popEvaluationStack(); - choiceOnlyText = choiceOnlyStrVal.value; - } - - if (choicePoint.hasStartContent()) { - StringValue startStrVal = (StringValue) state.popEvaluationStack(); - startText = startStrVal.value; - } - - // Don't create choice if player has already read this content - if (choicePoint.isOnceOnly()) { - int visitCount = state.visitCountForContainer(choicePoint.getChoiceTarget()); - if (visitCount > 0) { - showChoice = false; - } - } - - // We go through the full process of creating the choice above so - // that we consume the content for it, since otherwise it'll - // be shown on the output stream. - if (!showChoice) { - return null; - } - - Choice choice = new Choice(); - choice.targetPath = choicePoint.getPathOnChoice(); - choice.sourcePath = choicePoint.getPath().toString(); - choice.isInvisibleDefault = choicePoint.isInvisibleDefault(); - - // We need to capture the state of the callstack at the point where - // the choice was generated, since after the generation of this choice - // we may go on to pop out from a tunnel (possible if the choice was - // wrapped in a conditional), or we may pop out from a thread, - // at which point that thread is discarded. - // Fork clones the thread, gives it a new ID, but without affecting - // the thread stack itself. - choice.setThreadAtGeneration(state.getCallStack().forkThread()); - - // Set final text for the choice - choice.setText((startText + choiceOnlyText).trim()); - - return choice; - } - - /** - * Unwinds the callstack. Useful to reset the Story's evaluation without - * actually changing any meaningful state, for example if you want to exit a - * section of story prematurely and tell it to go elsewhere with a call to - * ChoosePathString(...). Doing so without calling ResetCallstack() could cause - * unexpected issues if, for example, the Story was in a tunnel already. - */ - public void resetCallstack() throws Exception { - ifAsyncWeCant("ResetCallstack"); - - state.forceEnd(); - } - - void resetErrors() { - state.resetErrors(); - } - - void resetGlobals() throws Exception { - if (mainContentContainer.getNamedContent().containsKey("global decl")) { - final Pointer originalPointer = new Pointer(state.getCurrentPointer()); - - choosePath(new Path("global decl"), false); - - // Continue, but without validating external bindings, - // since we may be doing this reset at initialisation time. - continueInternal(); - - state.setCurrentPointer(originalPointer); - } - - state.getVariablesState().snapshotDefaultGlobals(); - } - - /** - * Reset the Story back to its initial state as it was when it was first - * constructed. - */ - public void resetState() throws Exception { - // TODO: Could make this possible - ifAsyncWeCant("ResetState"); - - state = new StoryState(this); - - state.getVariablesState().setVariableChangedEvent(this); - - resetGlobals(); - } - - Pointer pointerAtPath(Path path) throws Exception { - if (path.getLength() == 0) - return Pointer.Null; - - final Pointer p = new Pointer(); - - int pathLengthToUse = path.getLength(); - final SearchResult result; - - if (path.getLastComponent().isIndex()) { - pathLengthToUse = path.getLength() - 1; - result = new SearchResult(mainContentContainer.contentAtPath(path, 0, pathLengthToUse)); - p.container = result.getContainer(); - p.index = path.getLastComponent().getIndex(); - } else { - result = new SearchResult(mainContentContainer.contentAtPath(path)); - p.container = result.getContainer(); - p.index = -1; - } - - if (result.obj == null || result.obj == mainContentContainer && pathLengthToUse > 0) - error("Failed to find content at path '" + path + "', and no approximation of it was possible."); - else if (result.approximate) - warning("Failed to find content at path '" + path + "', so it was approximated to: '" + result.obj.getPath() - + "'."); - - return p; - } - - void step() throws Exception { - - boolean shouldAddToStream = true; - - // Get current content - final Pointer pointer = new Pointer(); - pointer.assign(state.getCurrentPointer()); - - if (pointer.isNull()) { - return; - } - - // Step directly to the first element of content in a container (if - // necessary) - RTObject r = pointer.resolve(); - Container containerToEnter = r instanceof Container ? (Container) r : null; - - while (containerToEnter != null) { - - // Mark container as being entered - visitContainer(containerToEnter, true); - - // No content? the most we can do is step past it - if (containerToEnter.getContent().size() == 0) - break; - - pointer.assign(Pointer.startOf(containerToEnter)); - - r = pointer.resolve(); - containerToEnter = r instanceof Container ? (Container) r : null; - } - - state.setCurrentPointer(pointer); - - if (profiler != null) { - profiler.step(state.getCallStack()); - } - - // Is the current content Object: - // - Normal content - // - Or a logic/flow statement - if so, do it - // Stop flow if we hit a stack pop when we're unable to pop (e.g. - // return/done statement in knot - // that was diverted to rather than called as a function) - RTObject currentContentObj = pointer.resolve(); - boolean isLogicOrFlowControl = performLogicAndFlowControl(currentContentObj); - - // Has flow been forced to end by flow control above? - if (state.getCurrentPointer().isNull()) { - return; - } - - if (isLogicOrFlowControl) { - shouldAddToStream = false; - } - - // Choice with condition? - ChoicePoint choicePoint = currentContentObj instanceof ChoicePoint ? (ChoicePoint) currentContentObj : null; - if (choicePoint != null) { - Choice choice = processChoice(choicePoint); - if (choice != null) { - state.getGeneratedChoices().add(choice); - } - - currentContentObj = null; - shouldAddToStream = false; - } - - // If the container has no content, then it will be - // the "content" itself, but we skip over it. - if (currentContentObj instanceof Container) { - shouldAddToStream = false; - } - - // Content to add to evaluation stack or the output stream - if (shouldAddToStream) { - - // If we're pushing a variable pointer onto the evaluation stack, - // ensure that it's specific - // to our current (possibly temporary) context index. And make a - // copy of the pointer - // so that we're not editing the original runtime Object. - VariablePointerValue varPointer = currentContentObj instanceof VariablePointerValue - ? (VariablePointerValue) currentContentObj - : null; - - if (varPointer != null && varPointer.getContextIndex() == -1) { - - // Create new Object so we're not overwriting the story's own - // data - int contextIdx = state.getCallStack().contextForVariableNamed(varPointer.getVariableName()); - currentContentObj = new VariablePointerValue(varPointer.getVariableName(), contextIdx); - } - - // Expression evaluation content - if (state.getInExpressionEvaluation()) { - state.pushEvaluationStack(currentContentObj); - } - // Output stream content (i.e. not expression evaluation) - else { - state.pushToOutputStream(currentContentObj); - } - } - - // Increment the content pointer, following diverts if necessary - nextContent(); - - // Starting a thread should be done after the increment to the content - // pointer, - // so that when returning from the thread, it returns to the content - // after this instruction. - ControlCommand controlCmd = currentContentObj instanceof ControlCommand ? (ControlCommand) currentContentObj - : null; - if (controlCmd != null && controlCmd.getCommandType() == ControlCommand.CommandType.StartThread) { - state.getCallStack().pushThread(); - } - } - - /** - * The Story itself in JSON representation. - * - * @throws Exception - */ - public String toJson() throws Exception { - // return ToJsonOld(); - SimpleJson.Writer writer = new SimpleJson.Writer(); - toJson(writer); - return writer.toString(); - } - - /** - * The Story itself in JSON representation. - * - * @throws Exception - */ - public void toJson(OutputStream stream) throws Exception { - SimpleJson.Writer writer = new SimpleJson.Writer(stream); - toJson(writer); - } - - void toJson(SimpleJson.Writer writer) throws Exception { - writer.writeObjectStart(); - - writer.writeProperty("inkVersion", inkVersionCurrent); - - // Main container content - writer.writeProperty("root", new InnerWriter() { - - @Override - public void write(Writer w) throws Exception { - Json.writeRuntimeContainer(w, mainContentContainer); - } - }); - - // List definitions - if (listDefinitions != null) { - - writer.writePropertyStart("listDefs"); - writer.writeObjectStart(); - - for (ListDefinition def : listDefinitions.getLists()) { - writer.writePropertyStart(def.getName()); - writer.writeObjectStart(); - - for (Entry itemToVal : def.getItems().entrySet()) { - InkListItem item = itemToVal.getKey(); - int val = itemToVal.getValue(); - writer.writeProperty(item.getItemName(), val); - } - - writer.writeObjectEnd(); - writer.writePropertyEnd(); - } - - writer.writeObjectEnd(); - writer.writePropertyEnd(); - } - - writer.writeObjectEnd(); - } - - boolean tryFollowDefaultInvisibleChoice() throws Exception { - List allChoices = state.getCurrentChoices(); - - // Is a default invisible choice the ONLY choice? - // var invisibleChoices = allChoices.Where (c => - // c.choicePoint.isInvisibleDefault).ToList(); - ArrayList invisibleChoices = new ArrayList<>(); - for (Choice c : allChoices) { - if (c.isInvisibleDefault) { - invisibleChoices.add(c); - } - } - - if (invisibleChoices.size() == 0 || allChoices.size() > invisibleChoices.size()) - return false; - - Choice choice = invisibleChoices.get(0); - - // Invisible choice may have been generated on a different thread, - // in which case we need to restore it before we continue - state.getCallStack().setCurrentThread(choice.getThreadAtGeneration()); - - // If there's a chance that this state will be rolled back to before - // the invisible choice then make sure that the choice thread is - // left intact, and it isn't re-entered in an old state. - if (stateSnapshotAtLastNewline != null) - state.getCallStack().setCurrentThread(state.getCallStack().forkThread()); - - choosePath(choice.targetPath, false); - - return true; - } - - /** - * Remove a binding for a named EXTERNAL ink function. - */ - public void unbindExternalFunction(String funcName) throws Exception { - ifAsyncWeCant("unbind an external a function"); - Assert(externals.containsKey(funcName), "Function '" + funcName + "' has not been bound."); - externals.remove(funcName); - } - - /** - * Check that all EXTERNAL ink functions have a valid bound C# function. Note - * that this is automatically called on the first call to Continue(). - */ - public void validateExternalBindings() throws Exception { - HashSet missingExternals = new HashSet<>(); - - validateExternalBindings(mainContentContainer, missingExternals); - hasValidatedExternals = true; - // No problem! Validation complete - if (missingExternals.size() == 0) { - hasValidatedExternals = true; - } else { // Error for all missing externals - - StringBuilder join = new StringBuilder(); - boolean first = true; - for (String item : missingExternals) { - if (first) - first = false; - else - join.append(", "); - - join.append(item); - } - - String message = String.format("ERROR: Missing function binding for external%s: '%s' %s", - missingExternals.size() > 1 ? "s" : "", join.toString(), - allowExternalFunctionFallbacks ? ", and no fallback ink function found." - : " (ink fallbacks disabled)"); - - error(message); - } - } - - void validateExternalBindings(Container c, HashSet missingExternals) throws Exception { - for (RTObject innerContent : c.getContent()) { - Container container = innerContent instanceof Container ? (Container) innerContent : null; - if (container == null || !container.hasValidName()) - validateExternalBindings(innerContent, missingExternals); - } - - for (INamedContent innerKeyValue : c.getNamedContent().values()) { - validateExternalBindings(innerKeyValue instanceof RTObject ? (RTObject) innerKeyValue : (RTObject) null, - missingExternals); - } - } - - void validateExternalBindings(RTObject o, HashSet missingExternals) throws Exception { - Container container = o instanceof Container ? (Container) o : null; - - if (container != null) { - validateExternalBindings(container, missingExternals); - return; - } - - Divert divert = o instanceof Divert ? (Divert) o : null; - - if (divert != null && divert.isExternal()) { - String name = divert.getTargetPathString(); - - if (!externals.containsKey(name)) { - - if (allowExternalFunctionFallbacks) { - boolean fallbackFound = mainContentContainer.getNamedContent().containsKey(name); - if (!fallbackFound) { - missingExternals.add(name); - } - } else { - missingExternals.add(name); - } - } - } - } - - void visitChangedContainersDueToDivert() throws Exception { - final Pointer previousPointer = new Pointer(state.getPreviousPointer()); - final Pointer pointer = new Pointer(state.getCurrentPointer()); - - // Unless we're pointing *directly* at a piece of content, we don't do - // counting here. Otherwise, the main stepping function will do the counting. - if (pointer.isNull() || pointer.index == -1) - return; - - // First, find the previously open set of containers - - prevContainers.clear(); - - if (!previousPointer.isNull()) { - - Container prevAncestor = null; - - if (previousPointer.resolve() instanceof Container) { - prevAncestor = (Container) previousPointer.resolve(); - } else if (previousPointer.container instanceof Container) { - prevAncestor = previousPointer.container; - } - - while (prevAncestor != null) { - prevContainers.add(prevAncestor); - prevAncestor = prevAncestor.getParent() instanceof Container ? (Container) prevAncestor.getParent() - : null; - } - } - - // If the new Object is a container itself, it will be visited - // automatically at the next actual - // content step. However, we need to walk up the new ancestry to see if - // there are more new containers - RTObject currentChildOfContainer = pointer.resolve(); - - // Invalid pointer? May happen if attemptingto - if (currentChildOfContainer == null) - return; - - Container currentContainerAncestor = currentChildOfContainer.getParent() instanceof Container - ? (Container) currentChildOfContainer.getParent() - : null; - - boolean allChildrenEnteredAtStart = true; - while (currentContainerAncestor != null && (!prevContainers.contains(currentContainerAncestor) - || currentContainerAncestor.getCountingAtStartOnly())) { - - // Check whether this ancestor container is being entered at the - // start, - // by checking whether the child Object is the first. - boolean enteringAtStart = currentContainerAncestor.getContent().size() > 0 - && currentChildOfContainer == currentContainerAncestor.getContent().get(0) - && allChildrenEnteredAtStart; - - // Don't count it as entering at start if we're entering random somewhere within - // a container B that happens to be nested at index 0 of container A. It only - // counts - // if we're diverting directly to the first leaf node. - if (!enteringAtStart) - allChildrenEnteredAtStart = false; - - // Mark a visit to this container - visitContainer(currentContainerAncestor, enteringAtStart); - - currentChildOfContainer = currentContainerAncestor; - currentContainerAncestor = currentContainerAncestor.getParent() instanceof Container - ? (Container) currentContainerAncestor.getParent() - : null; - - } - } - - // Mark a container as having been visited - void visitContainer(Container container, boolean atStart) throws Exception { - if (!container.getCountingAtStartOnly() || atStart) { - if (container.getVisitsShouldBeCounted()) - state.incrementVisitCountForContainer(container); - - if (container.getTurnIndexShouldBeCounted()) - state.recordTurnIndexVisitToContainer(container); - } - } - - public boolean allowExternalFunctionFallbacks() { - return allowExternalFunctionFallbacks; - } - - public void setAllowExternalFunctionFallbacks(boolean allowExternalFunctionFallbacks) { - this.allowExternalFunctionFallbacks = allowExternalFunctionFallbacks; - } - - /** - * Evaluates a function defined in ink. - * - * @param functionName The name of the function as declared in ink. - * @param arguments The arguments that the ink function takes, if any. Note - * that we don't (can't) do any validation on the number of - * arguments right now, so make sure you get it right! - * @return The return value as returned from the ink function with `~ return - * myValue`, or null if nothing is returned. - * @throws Exception - */ - public Object evaluateFunction(String functionName, Object[] arguments) throws Exception { - return evaluateFunction(functionName, null, arguments); - } - - public Object evaluateFunction(String functionName) throws Exception { - return evaluateFunction(functionName, null, null); - } - - /** - * Checks if a function exists. - * - * @return True if the function exists, else false. - * @param functionName The name of the function as declared in ink. - */ - public boolean hasFunction(String functionName) { - try { - return knotContainerWithName(functionName) != null; - } catch (Exception e) { - return false; - } - } - - /** - * Evaluates a function defined in ink, and gathers the possibly multi-line text - * as generated by the function. - * - * @param arguments The arguments that the ink function takes, if any. Note - * that we don't (can't) do any validation on the number of - * arguments right now, so make sure you get it right! - * @param functionName The name of the function as declared in ink. - * @param textOutput This text output is any text written as normal content - * within the function, as opposed to the return value, as - * returned with `~ return`. - * @return The return value as returned from the ink function with `~ return - * myValue`, or null if nothing is returned. - * @throws Exception - */ - public Object evaluateFunction(String functionName, StringBuilder textOutput, Object[] arguments) throws Exception { - ifAsyncWeCant("evaluate a function"); - - if (functionName == null) { - throw new Exception("Function is null"); - } else if (functionName.trim().isEmpty()) { - throw new Exception("Function is empty or white space."); - } - - // Get the content that we need to run - Container funcContainer = knotContainerWithName(functionName); - if (funcContainer == null) - throw new Exception("Function doesn't exist: '" + functionName + "'"); - - // Snapshot the output stream - ArrayList outputStreamBefore = new ArrayList<>(state.getOutputStream()); - state.resetOutput(); - - // State will temporarily replace the callstack in order to evaluate - state.startFunctionEvaluationFromGame(funcContainer, arguments); - - // Evaluate the function, and collect the string output - while (canContinue()) { - String text = Continue(); - - if (textOutput != null) - textOutput.append(text); - } - - // Restore the output stream in case this was called - // during main story evaluation. - state.resetOutput(outputStreamBefore); - - // Finish evaluation, and see whether anything was produced - Object result = state.completeFunctionEvaluationFromGame(); - return result; - } - - // Maximum snapshot stack: - // - stateSnapshotDuringSave -- not retained, but returned to game code - // - _stateSnapshotAtLastNewline (has older patch) - // - _state (current, being patched) - void stateSnapshot() { - stateSnapshotAtLastNewline = state; - state = state.copyAndStartPatching(); - } - - void restoreStateSnapshot() { - // Patched state had temporarily hijacked our - // VariablesState and set its own callstack on it, - // so we need to restore that. - // If we're in the middle of saving, we may also - // need to give the VariablesState the old patch. - stateSnapshotAtLastNewline.restoreAfterPatch(); - - state = stateSnapshotAtLastNewline; - stateSnapshotAtLastNewline = null; - - // If save completed while the above snapshot was - // active, we need to apply any changes made since - // the save was started but before the snapshot was made. - if (!asyncSaving) { - state.applyAnyPatch(); - } - } - - void discardSnapshot() { - // Normally we want to integrate the patch - // into the main global/counts dictionaries. - // However, if we're in the middle of async - // saving, we simply stay in a "patching" state, - // albeit with the newer cloned patch. - if (!asyncSaving) - state.applyAnyPatch(); - - // No longer need the snapshot. - stateSnapshotAtLastNewline = null; - } - - /** - * Advanced usage! If you have a large story, and saving state to JSON takes too - * long for your framerate, you can temporarily freeze a copy of the state for - * saving on a separate thread. Internally, the engine maintains a "diff patch". - * When you've finished saving your state, call BackgroundSaveComplete() and - * that diff patch will be applied, allowing the story to continue in its usual - * mode. - * - * @return The state for background thread save. - * @throws Exception - */ - public StoryState copyStateForBackgroundThreadSave() throws Exception { - ifAsyncWeCant("start saving on a background thread"); - if (asyncSaving) - throw new Exception( - "Story is already in background saving mode, can't call CopyStateForBackgroundThreadSave again!"); - StoryState stateToSave = state; - state = state.copyAndStartPatching(); - asyncSaving = true; - return stateToSave; - } - - /** - * See CopyStateForBackgroundThreadSave. This method releases the "frozen" save - * state, applying its patch that it was using internally. - */ - public void backgroundSaveComplete() { - // CopyStateForBackgroundThreadSave must be called outside - // of any async ink evaluation, since otherwise you'd be saving - // during an intermediate state. - // However, it's possible to *complete* the save in the middle of - // a glue-lookahead when there's a state stored in _stateSnapshotAtLastNewline. - // This state will have its own patch that is newer than the save patch. - // We hold off on the final apply until the glue-lookahead is finished. - // In that case, the apply is always done, it's just that it may - // apply the looked-ahead changes OR it may simply apply the changes - // made during the save process to the old _stateSnapshotAtLastNewline state. - if (stateSnapshotAtLastNewline == null) { - state.applyAnyPatch(); - } - - asyncSaving = false; - } + // Variable assignment + else if (contentObj instanceof VariableAssignment) { + VariableAssignment varAss = (VariableAssignment) contentObj; + RTObject assignedVal = state.popEvaluationStack(); + + // When in temporary evaluation, don't create new variables purely + // within + // the temporary context, but attempt to create them globally + // var prioritiseHigherInCallStack = _temporaryEvaluationContainer + // != null; + + state.getVariablesState().assign(varAss, assignedVal); + + return true; + } + + // Variable reference + else if (contentObj instanceof VariableReference) { + VariableReference varRef = (VariableReference) contentObj; + RTObject foundValue = null; + + // Explicit read count value + if (varRef.getPathForCount() != null) { + + Container container = varRef.getContainerForCount(); + int count = state.visitCountForContainer(container); + foundValue = new IntValue(count); + } + + // Normal variable reference + else { + + foundValue = state.getVariablesState().getVariableWithName(varRef.getName()); + + if (foundValue == null) { + warning("Variable not found: '" + varRef.getName() + + "'. Using default value of 0 (false). This can happen with temporary variables if the " + + "declaration hasn't yet been hit. Globals are always given a default value on load if a " + + "value doesn't exist in the save state."); + foundValue = new IntValue(0); + } + } + + state.pushEvaluationStack(foundValue); + + return true; + } + + // Native function call + else if (contentObj instanceof NativeFunctionCall) { + NativeFunctionCall func = (NativeFunctionCall) contentObj; + List funcParams = state.popEvaluationStack(func.getNumberOfParameters()); + + RTObject result = func.call(funcParams); + state.pushEvaluationStack(result); + return true; + } + + // No control content, must be ordinary content + return false; + } + + // Assumption: prevText is the snapshot where we saw a newline, and we're + // checking whether we're really done + // with that line. Therefore prevText will definitely end in a newline. + // + // We take tags into account too, so that a tag following a content line: + // Content + // # tag + // ... doesn't cause the tag to be wrongly associated with the content above. + enum OutputStateChange { + NoChange, ExtendedBeyondNewline, NewlineRemoved + } + + OutputStateChange calculateNewlineOutputStateChange(String prevText, String currText, int prevTagCount, + int currTagCount) { + // Simple case: nothing's changed, and we still have a newline + // at the end of the current content + boolean newlineStillExists = currText.length() >= prevText.length() && prevText.length() > 0 + && currText.charAt(prevText.length() - 1) == '\n'; + if (prevTagCount == currTagCount && prevText.length() == currText.length() && newlineStillExists) + return OutputStateChange.NoChange; + + // Old newline has been removed, it wasn't the end of the line after all + if (!newlineStillExists) { + return OutputStateChange.NewlineRemoved; + } + + // Tag added - definitely the start of a new line + if (currTagCount > prevTagCount) + return OutputStateChange.ExtendedBeyondNewline; + + // There must be new content - check whether it's just whitespace + for (int i = prevText.length(); i < currText.length(); i++) { + char c = currText.charAt(i); + if (c != ' ' && c != '\t') { + return OutputStateChange.ExtendedBeyondNewline; + } + } + + // There's new text but it's just spaces and tabs, so there's still the + // potential + // for glue to kill the newline. + return OutputStateChange.NoChange; + } + + String popChoiceStringAndTags(List tags) { + StringValue choiceOnlyStrVal = (StringValue) state.popEvaluationStack(); + + while (state.getEvaluationStack().size() > 0 && state.peekEvaluationStack() instanceof Tag) { + Tag tag = (Tag) state.popEvaluationStack(); + tags.add(0, tag.getText()); // popped in reverse order + } + + return choiceOnlyStrVal.value; + } + + Choice processChoice(ChoicePoint choicePoint) throws Exception { + boolean showChoice = true; + + // Don't create choice if choice point doesn't pass conditional + if (choicePoint.hasCondition()) { + RTObject conditionValue = state.popEvaluationStack(); + if (!isTruthy(conditionValue)) { + showChoice = false; + } + } + + String startText = ""; + String choiceOnlyText = ""; + List tags = new ArrayList<>(0); + + if (choicePoint.hasChoiceOnlyContent()) { + choiceOnlyText = popChoiceStringAndTags(tags); + } + + if (choicePoint.hasStartContent()) { + startText = popChoiceStringAndTags(tags); + } + + // Don't create choice if player has already read this content + if (choicePoint.isOnceOnly()) { + int visitCount = state.visitCountForContainer(choicePoint.getChoiceTarget()); + if (visitCount > 0) { + showChoice = false; + } + } + + // We go through the full process of creating the choice above so + // that we consume the content for it, since otherwise it'll + // be shown on the output stream. + if (!showChoice) { + return null; + } + + Choice choice = new Choice(); + choice.targetPath = choicePoint.getPathOnChoice(); + choice.sourcePath = choicePoint.getPath().toString(); + choice.isInvisibleDefault = choicePoint.isInvisibleDefault(); + choice.tags = tags; + + // We need to capture the state of the callstack at the point where + // the choice was generated, since after the generation of this choice + // we may go on to pop out from a tunnel (possible if the choice was + // wrapped in a conditional), or we may pop out from a thread, + // at which point that thread is discarded. + // Fork clones the thread, gives it a new ID, but without affecting + // the thread stack itself. + choice.setThreadAtGeneration(state.getCallStack().forkThread()); + + // Set final text for the choice + choice.setText((startText + choiceOnlyText).trim()); + + return choice; + } + + /** + * Unwinds the callstack. Useful to reset the Story's evaluation without + * actually changing any meaningful state, for example if you want to exit a + * section of story prematurely and tell it to go elsewhere with a call to + * ChoosePathString(...). Doing so without calling ResetCallstack() could cause + * unexpected issues if, for example, the Story was in a tunnel already. + */ + public void resetCallstack() throws Exception { + ifAsyncWeCant("ResetCallstack"); + + state.forceEnd(); + } + + void resetErrors() { + state.resetErrors(); + } + + void resetGlobals() throws Exception { + if (mainContentContainer.getNamedContent().containsKey("global decl")) { + final Pointer originalPointer = new Pointer(state.getCurrentPointer()); + + choosePath(new Path("global decl"), false); + + // Continue, but without validating external bindings, + // since we may be doing this reset at initialisation time. + continueInternal(); + + state.setCurrentPointer(originalPointer); + } + + state.getVariablesState().snapshotDefaultGlobals(); + } + + /** + * Reset the Story back to its initial state as it was when it was first + * constructed. + */ + public void resetState() throws Exception { + // TODO: Could make this possible + ifAsyncWeCant("ResetState"); + + state = new StoryState(this); + + state.getVariablesState().setVariableChangedEvent(this); + + resetGlobals(); + } + + Pointer pointerAtPath(Path path) throws Exception { + if (path.getLength() == 0) + return Pointer.Null; + + final Pointer p = new Pointer(); + + int pathLengthToUse = path.getLength(); + final SearchResult result; + + if (path.getLastComponent().isIndex()) { + pathLengthToUse = path.getLength() - 1; + result = new SearchResult(mainContentContainer.contentAtPath(path, 0, pathLengthToUse)); + p.container = result.getContainer(); + p.index = path.getLastComponent().getIndex(); + } else { + result = new SearchResult(mainContentContainer.contentAtPath(path)); + p.container = result.getContainer(); + p.index = -1; + } + + if (result.obj == null || result.obj == mainContentContainer && pathLengthToUse > 0) + error("Failed to find content at path '" + path + "', and no approximation of it was possible."); + else if (result.approximate) + warning("Failed to find content at path '" + path + "', so it was approximated to: '" + result.obj.getPath() + + "'."); + + return p; + } + + void step() throws Exception { + + boolean shouldAddToStream = true; + + // Get current content + final Pointer pointer = new Pointer(); + pointer.assign(state.getCurrentPointer()); + + if (pointer.isNull()) { + return; + } + + // Step directly to the first element of content in a container (if + // necessary) + RTObject r = pointer.resolve(); + Container containerToEnter = r instanceof Container ? (Container) r : null; + + while (containerToEnter != null) { + + // Mark container as being entered + visitContainer(containerToEnter, true); + + // No content? the most we can do is step past it + if (containerToEnter.getContent().size() == 0) + break; + + pointer.assign(Pointer.startOf(containerToEnter)); + + r = pointer.resolve(); + containerToEnter = r instanceof Container ? (Container) r : null; + } + + state.setCurrentPointer(pointer); + + if (profiler != null) { + profiler.step(state.getCallStack()); + } + + // Is the current content Object: + // - Normal content + // - Or a logic/flow statement - if so, do it + // Stop flow if we hit a stack pop when we're unable to pop (e.g. + // return/done statement in knot + // that was diverted to rather than called as a function) + RTObject currentContentObj = pointer.resolve(); + boolean isLogicOrFlowControl = performLogicAndFlowControl(currentContentObj); + + // Has flow been forced to end by flow control above? + if (state.getCurrentPointer().isNull()) { + return; + } + + if (isLogicOrFlowControl) { + shouldAddToStream = false; + } + + // Choice with condition? + ChoicePoint choicePoint = currentContentObj instanceof ChoicePoint ? (ChoicePoint) currentContentObj : null; + if (choicePoint != null) { + Choice choice = processChoice(choicePoint); + if (choice != null) { + state.getGeneratedChoices().add(choice); + } + + currentContentObj = null; + shouldAddToStream = false; + } + + // If the container has no content, then it will be + // the "content" itself, but we skip over it. + if (currentContentObj instanceof Container) { + shouldAddToStream = false; + } + + // Content to add to evaluation stack or the output stream + if (shouldAddToStream) { + + // If we're pushing a variable pointer onto the evaluation stack, + // ensure that it's specific + // to our current (possibly temporary) context index. And make a + // copy of the pointer + // so that we're not editing the original runtime Object. + VariablePointerValue varPointer = currentContentObj instanceof VariablePointerValue + ? (VariablePointerValue) currentContentObj + : null; + + if (varPointer != null && varPointer.getContextIndex() == -1) { + + // Create new Object so we're not overwriting the story's own + // data + int contextIdx = state.getCallStack().contextForVariableNamed(varPointer.getVariableName()); + currentContentObj = new VariablePointerValue(varPointer.getVariableName(), contextIdx); + } + + // Expression evaluation content + if (state.getInExpressionEvaluation()) { + state.pushEvaluationStack(currentContentObj); + } + // Output stream content (i.e. not expression evaluation) + else { + state.pushToOutputStream(currentContentObj); + } + } + + // Increment the content pointer, following diverts if necessary + nextContent(); + + // Starting a thread should be done after the increment to the content + // pointer, + // so that when returning from the thread, it returns to the content + // after this instruction. + ControlCommand controlCmd = currentContentObj instanceof ControlCommand ? (ControlCommand) currentContentObj + : null; + if (controlCmd != null && controlCmd.getCommandType() == ControlCommand.CommandType.StartThread) { + state.getCallStack().pushThread(); + } + } + + /** + * The Story itself in JSON representation. + * + * @throws Exception + */ + public String toJson() throws Exception { + // return ToJsonOld(); + SimpleJson.Writer writer = new SimpleJson.Writer(); + toJson(writer); + return writer.toString(); + } + + /** + * The Story itself in JSON representation. + * + * @throws Exception + */ + public void toJson(OutputStream stream) throws Exception { + SimpleJson.Writer writer = new SimpleJson.Writer(stream); + toJson(writer); + } + + void toJson(SimpleJson.Writer writer) throws Exception { + writer.writeObjectStart(); + + writer.writeProperty("inkVersion", inkVersionCurrent); + + // Main container content + writer.writeProperty("root", new InnerWriter() { + + @Override + public void write(Writer w) throws Exception { + Json.writeRuntimeContainer(w, mainContentContainer); + } + }); + + // List definitions + if (listDefinitions != null) { + + writer.writePropertyStart("listDefs"); + writer.writeObjectStart(); + + for (ListDefinition def : listDefinitions.getLists()) { + writer.writePropertyStart(def.getName()); + writer.writeObjectStart(); + + for (Entry itemToVal : def.getItems().entrySet()) { + InkListItem item = itemToVal.getKey(); + int val = itemToVal.getValue(); + writer.writeProperty(item.getItemName(), val); + } + + writer.writeObjectEnd(); + writer.writePropertyEnd(); + } + + writer.writeObjectEnd(); + writer.writePropertyEnd(); + } + + writer.writeObjectEnd(); + } + + boolean tryFollowDefaultInvisibleChoice() throws Exception { + List allChoices = state.getCurrentChoices(); + + // Is a default invisible choice the ONLY choice? + // var invisibleChoices = allChoices.Where (c => + // c.choicePoint.isInvisibleDefault).ToList(); + ArrayList invisibleChoices = new ArrayList<>(); + for (Choice c : allChoices) { + if (c.isInvisibleDefault) { + invisibleChoices.add(c); + } + } + + if (invisibleChoices.size() == 0 || allChoices.size() > invisibleChoices.size()) + return false; + + Choice choice = invisibleChoices.get(0); + + // Invisible choice may have been generated on a different thread, + // in which case we need to restore it before we continue + state.getCallStack().setCurrentThread(choice.getThreadAtGeneration()); + + // If there's a chance that this state will be rolled back to before + // the invisible choice then make sure that the choice thread is + // left intact, and it isn't re-entered in an old state. + if (stateSnapshotAtLastNewline != null) + state.getCallStack().setCurrentThread(state.getCallStack().forkThread()); + + choosePath(choice.targetPath, false); + + return true; + } + + /** + * Remove a binding for a named EXTERNAL ink function. + */ + public void unbindExternalFunction(String funcName) throws Exception { + ifAsyncWeCant("unbind an external a function"); + Assert(externals.containsKey(funcName), "Function '" + funcName + "' has not been bound."); + externals.remove(funcName); + } + + /** + * Check that all EXTERNAL ink functions have a valid bound C# function. Note + * that this is automatically called on the first call to Continue(). + */ + public void validateExternalBindings() throws Exception { + HashSet missingExternals = new HashSet<>(); + + validateExternalBindings(mainContentContainer, missingExternals); + hasValidatedExternals = true; + // No problem! Validation complete + if (missingExternals.size() == 0) { + hasValidatedExternals = true; + } else { // Error for all missing externals + + StringBuilder join = new StringBuilder(); + boolean first = true; + for (String item : missingExternals) { + if (first) + first = false; + else + join.append(", "); + + join.append(item); + } + + String message = String.format("ERROR: Missing function binding for external%s: '%s' %s", + missingExternals.size() > 1 ? "s" : "", join.toString(), + allowExternalFunctionFallbacks ? ", and no fallback ink function found." + : " (ink fallbacks disabled)"); + + error(message); + } + } + + void validateExternalBindings(Container c, HashSet missingExternals) throws Exception { + for (RTObject innerContent : c.getContent()) { + Container container = innerContent instanceof Container ? (Container) innerContent : null; + if (container == null || !container.hasValidName()) + validateExternalBindings(innerContent, missingExternals); + } + + for (INamedContent innerKeyValue : c.getNamedContent().values()) { + validateExternalBindings(innerKeyValue instanceof RTObject ? (RTObject) innerKeyValue : (RTObject) null, + missingExternals); + } + } + + void validateExternalBindings(RTObject o, HashSet missingExternals) throws Exception { + Container container = o instanceof Container ? (Container) o : null; + + if (container != null) { + validateExternalBindings(container, missingExternals); + return; + } + + Divert divert = o instanceof Divert ? (Divert) o : null; + + if (divert != null && divert.isExternal()) { + String name = divert.getTargetPathString(); + + if (!externals.containsKey(name)) { + + if (allowExternalFunctionFallbacks) { + boolean fallbackFound = mainContentContainer.getNamedContent().containsKey(name); + if (!fallbackFound) { + missingExternals.add(name); + } + } else { + missingExternals.add(name); + } + } + } + } + + void visitChangedContainersDueToDivert() throws Exception { + final Pointer previousPointer = new Pointer(state.getPreviousPointer()); + final Pointer pointer = new Pointer(state.getCurrentPointer()); + + // Unless we're pointing *directly* at a piece of content, we don't do + // counting here. Otherwise, the main stepping function will do the counting. + if (pointer.isNull() || pointer.index == -1) + return; + + // First, find the previously open set of containers + + prevContainers.clear(); + + if (!previousPointer.isNull()) { + + Container prevAncestor = null; + + if (previousPointer.resolve() instanceof Container) { + prevAncestor = (Container) previousPointer.resolve(); + } else if (previousPointer.container instanceof Container) { + prevAncestor = previousPointer.container; + } + + while (prevAncestor != null) { + prevContainers.add(prevAncestor); + prevAncestor = prevAncestor.getParent() instanceof Container ? (Container) prevAncestor.getParent() + : null; + } + } + + // If the new Object is a container itself, it will be visited + // automatically at the next actual + // content step. However, we need to walk up the new ancestry to see if + // there are more new containers + RTObject currentChildOfContainer = pointer.resolve(); + + // Invalid pointer? May happen if attemptingto + if (currentChildOfContainer == null) + return; + + Container currentContainerAncestor = currentChildOfContainer.getParent() instanceof Container + ? (Container) currentChildOfContainer.getParent() + : null; + + boolean allChildrenEnteredAtStart = true; + while (currentContainerAncestor != null && (!prevContainers.contains(currentContainerAncestor) + || currentContainerAncestor.getCountingAtStartOnly())) { + + // Check whether this ancestor container is being entered at the + // start, + // by checking whether the child Object is the first. + boolean enteringAtStart = currentContainerAncestor.getContent().size() > 0 + && currentChildOfContainer == currentContainerAncestor.getContent().get(0) + && allChildrenEnteredAtStart; + + // Don't count it as entering at start if we're entering random somewhere within + // a container B that happens to be nested at index 0 of container A. It only + // counts + // if we're diverting directly to the first leaf node. + if (!enteringAtStart) + allChildrenEnteredAtStart = false; + + // Mark a visit to this container + visitContainer(currentContainerAncestor, enteringAtStart); + + currentChildOfContainer = currentContainerAncestor; + currentContainerAncestor = currentContainerAncestor.getParent() instanceof Container + ? (Container) currentContainerAncestor.getParent() + : null; + + } + } + + // Mark a container as having been visited + void visitContainer(Container container, boolean atStart) throws Exception { + if (!container.getCountingAtStartOnly() || atStart) { + if (container.getVisitsShouldBeCounted()) + state.incrementVisitCountForContainer(container); + + if (container.getTurnIndexShouldBeCounted()) + state.recordTurnIndexVisitToContainer(container); + } + } + + public boolean allowExternalFunctionFallbacks() { + return allowExternalFunctionFallbacks; + } + + public void setAllowExternalFunctionFallbacks(boolean allowExternalFunctionFallbacks) { + this.allowExternalFunctionFallbacks = allowExternalFunctionFallbacks; + } + + /** + * Evaluates a function defined in ink. + * + * @param functionName The name of the function as declared in ink. + * @param arguments The arguments that the ink function takes, if any. Note + * that we don't (can't) do any validation on the number of + * arguments right now, so make sure you get it right! + * @return The return value as returned from the ink function with `~ return + * myValue`, or null if nothing is returned. + * @throws Exception + */ + public Object evaluateFunction(String functionName, Object[] arguments) throws Exception { + return evaluateFunction(functionName, null, arguments); + } + + public Object evaluateFunction(String functionName) throws Exception { + return evaluateFunction(functionName, null, null); + } + + /** + * Checks if a function exists. + * + * @param functionName The name of the function as declared in ink. + * @return True if the function exists, else false. + */ + public boolean hasFunction(String functionName) { + try { + return knotContainerWithName(functionName) != null; + } catch (Exception e) { + return false; + } + } + + /** + * Evaluates a function defined in ink, and gathers the possibly multi-line text + * as generated by the function. + * + * @param arguments The arguments that the ink function takes, if any. Note + * that we don't (can't) do any validation on the number of + * arguments right now, so make sure you get it right! + * @param functionName The name of the function as declared in ink. + * @param textOutput This text output is any text written as normal content + * within the function, as opposed to the return value, as + * returned with `~ return`. + * @return The return value as returned from the ink function with `~ return + * myValue`, or null if nothing is returned. + * @throws Exception + */ + public Object evaluateFunction(String functionName, StringBuilder textOutput, Object[] arguments) throws Exception { + ifAsyncWeCant("evaluate a function"); + + if (functionName == null) { + throw new Exception("Function is null"); + } else if (functionName.trim().isEmpty()) { + throw new Exception("Function is empty or white space."); + } + + // Get the content that we need to run + Container funcContainer = knotContainerWithName(functionName); + if (funcContainer == null) + throw new Exception("Function doesn't exist: '" + functionName + "'"); + + // Snapshot the output stream + ArrayList outputStreamBefore = new ArrayList<>(state.getOutputStream()); + state.resetOutput(); + + // State will temporarily replace the callstack in order to evaluate + state.startFunctionEvaluationFromGame(funcContainer, arguments); + + // Evaluate the function, and collect the string output + while (canContinue()) { + String text = Continue(); + + if (textOutput != null) + textOutput.append(text); + } + + // Restore the output stream in case this was called + // during main story evaluation. + state.resetOutput(outputStreamBefore); + + // Finish evaluation, and see whether anything was produced + Object result = state.completeFunctionEvaluationFromGame(); + return result; + } + + // Maximum snapshot stack: + // - stateSnapshotDuringSave -- not retained, but returned to game code + // - _stateSnapshotAtLastNewline (has older patch) + // - _state (current, being patched) + void stateSnapshot() { + stateSnapshotAtLastNewline = state; + state = state.copyAndStartPatching(); + } + + void restoreStateSnapshot() { + // Patched state had temporarily hijacked our + // VariablesState and set its own callstack on it, + // so we need to restore that. + // If we're in the middle of saving, we may also + // need to give the VariablesState the old patch. + stateSnapshotAtLastNewline.restoreAfterPatch(); + + state = stateSnapshotAtLastNewline; + stateSnapshotAtLastNewline = null; + + // If save completed while the above snapshot was + // active, we need to apply any changes made since + // the save was started but before the snapshot was made. + if (!asyncSaving) { + state.applyAnyPatch(); + } + } + + void discardSnapshot() { + // Normally we want to integrate the patch + // into the main global/counts dictionaries. + // However, if we're in the middle of async + // saving, we simply stay in a "patching" state, + // albeit with the newer cloned patch. + if (!asyncSaving) + state.applyAnyPatch(); + + // No longer need the snapshot. + stateSnapshotAtLastNewline = null; + } + + /** + * Advanced usage! If you have a large story, and saving state to JSON takes too + * long for your framerate, you can temporarily freeze a copy of the state for + * saving on a separate thread. Internally, the engine maintains a "diff patch". + * When you've finished saving your state, call BackgroundSaveComplete() and + * that diff patch will be applied, allowing the story to continue in its usual + * mode. + * + * @return The state for background thread save. + * @throws Exception + */ + public StoryState copyStateForBackgroundThreadSave() throws Exception { + ifAsyncWeCant("start saving on a background thread"); + if (asyncSaving) + throw new Exception( + "Story is already in background saving mode, can't call CopyStateForBackgroundThreadSave again!"); + StoryState stateToSave = state; + state = state.copyAndStartPatching(); + asyncSaving = true; + return stateToSave; + } + + /** + * See CopyStateForBackgroundThreadSave. This method releases the "frozen" save + * state, applying its patch that it was using internally. + */ + public void backgroundSaveComplete() { + // CopyStateForBackgroundThreadSave must be called outside + // of any async ink evaluation, since otherwise you'd be saving + // during an intermediate state. + // However, it's possible to *complete* the save in the middle of + // a glue-lookahead when there's a state stored in _stateSnapshotAtLastNewline. + // This state will have its own patch that is newer than the save patch. + // We hold off on the final apply until the glue-lookahead is finished. + // In that case, the apply is always done, it's just that it may + // apply the looked-ahead changes OR it may simply apply the changes + // made during the save process to the old _stateSnapshotAtLastNewline state. + if (stateSnapshotAtLastNewline == null) { + state.applyAnyPatch(); + } + + asyncSaving = false; + } } diff --git a/src/main/java/com/bladecoder/ink/runtime/StoryState.java b/src/main/java/com/bladecoder/ink/runtime/StoryState.java index 58b227b..61b8c54 100644 --- a/src/main/java/com/bladecoder/ink/runtime/StoryState.java +++ b/src/main/java/com/bladecoder/ink/runtime/StoryState.java @@ -24,7 +24,11 @@ public class StoryState { /** * The current version of the state save file JSON-based format. */ - public static final int kInkSaveStateVersion = 9; + // + // Backward compatible changes since v8: + // v10: dynamic tags + // v9: multi-flows + public static final int kInkSaveStateVersion = 10; public static final int kMinCompatibleLoadVersion = 8; public static final String kDefaultFlowName = "DEFAULT_FLOW"; @@ -184,14 +188,25 @@ void popFromOutputStream(int count) { String getCurrentText() { if (outputStreamTextDirty) { StringBuilder sb = new StringBuilder(); + boolean inTag = false; for (RTObject outputObj : getOutputStream()) { StringValue textContent = null; if (outputObj instanceof StringValue) textContent = (StringValue) outputObj; - if (textContent != null) { + if (!inTag && textContent != null) { sb.append(textContent.value); + } else { + if (outputObj instanceof ControlCommand) { + ControlCommand controlCommand = (ControlCommand) outputObj; + + if (controlCommand.getCommandType() == ControlCommand.CommandType.BeginTag) { + inTag = true; + } else if (controlCommand.getCommandType() == ControlCommand.CommandType.EndTag) { + inTag = false; + } + } } } @@ -313,15 +328,48 @@ List getCurrentTags() { if (outputStreamTagsDirty) { currentTags = new ArrayList<>(); + boolean inTag = false; + StringBuilder sb = new StringBuilder(); + for (RTObject outputObj : getOutputStream()) { - Tag tag = null; - if (outputObj instanceof Tag) - tag = (Tag) outputObj; - if (tag != null) { - currentTags.add(tag.getText()); + if (outputObj instanceof ControlCommand) { + ControlCommand controlCommand = (ControlCommand) outputObj; + + if (controlCommand.getCommandType() == ControlCommand.CommandType.BeginTag) { + if (inTag && sb.length() > 0) { + String txt = cleanOutputWhitespace(sb.toString()); + currentTags.add(txt); + sb.setLength(0); + } + inTag = true; + } else if (controlCommand.getCommandType() == ControlCommand.CommandType.EndTag) { + if (sb.length() > 0) { + String txt = cleanOutputWhitespace(sb.toString()); + currentTags.add(txt); + sb.setLength(0); + } + inTag = false; + } + } else if (inTag) { + if (outputObj instanceof StringValue) { + StringValue strVal = (StringValue) outputObj; + sb.append(strVal.value); + } + } else if (outputObj instanceof Tag) { + Tag tag = (Tag) outputObj; + if (tag.getText() != null && tag.getText().length() > 0) { + currentTags.add(tag.getText()); // tag.text has whitespace already cleaned + } } } + + if (sb.length() > 0) { + String txt = cleanOutputWhitespace(sb.toString()); + currentTags.add(txt); + sb.setLength(0); + } + outputStreamTagsDirty = false; } diff --git a/src/main/java/com/bladecoder/ink/runtime/Tag.java b/src/main/java/com/bladecoder/ink/runtime/Tag.java index 77c37d4..51ea5a8 100644 --- a/src/main/java/com/bladecoder/ink/runtime/Tag.java +++ b/src/main/java/com/bladecoder/ink/runtime/Tag.java @@ -1,17 +1,17 @@ package com.bladecoder.ink.runtime; public class Tag extends RTObject { - private String text; + private String text; - public String getText() { - return text; - } + public String getText() { + return text; + } - public Tag(String tagText) { - this.text = tagText; - } - - public String toString() { - return "# " + text; - } + public Tag(String tagText) { + this.text = tagText; + } + + public String toString() { + return "# " + text; + } } diff --git a/src/test/java/com/bladecoder/ink/runtime/test/TagSpecTest.java b/src/test/java/com/bladecoder/ink/runtime/test/TagSpecTest.java index bd204fe..f8c6828 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/TagSpecTest.java +++ b/src/test/java/com/bladecoder/ink/runtime/test/TagSpecTest.java @@ -1,40 +1,78 @@ package com.bladecoder.ink.runtime.test; +import com.bladecoder.ink.runtime.Story; import org.junit.Assert; import org.junit.Test; -import com.bladecoder.ink.runtime.Story; - public class TagSpecTest { - /** - * "- basic test for tags" - */ - @Test - public void testTags() throws Exception { + /** + * "- basic test for tags" + */ + @Test + public void testTags() throws Exception { + + String json = TestUtils.getJsonString("inkfiles/tags/tags.ink.json"); + Story story = new Story(json); + + String[] globalTags = {"author: Joe", "title: My Great Story"}; + String[] knotTags = {"knot tag"}; + String[] knotTagWhenContinuedTwice = {"end of knot tag"}; + String[] stitchTags = {"stitch tag"}; + + Assert.assertArrayEquals(globalTags, story.getGlobalTags().toArray()); + + Assert.assertEquals("This is the content\n", story.Continue()); + + Assert.assertArrayEquals(globalTags, story.getCurrentTags().toArray()); + + Assert.assertArrayEquals(knotTags, story.tagsForContentAtPath("knot").toArray()); + Assert.assertArrayEquals(stitchTags, story.tagsForContentAtPath("knot.stitch").toArray()); + + story.choosePathString("knot"); + Assert.assertEquals("Knot content\n", story.Continue()); + Assert.assertArrayEquals(knotTags, story.getCurrentTags().toArray()); + Assert.assertEquals("", story.Continue()); + Assert.assertArrayEquals(knotTagWhenContinuedTwice, story.getCurrentTags().toArray()); + } + + @Test + public void testTagsInSeq() throws Exception { + + String json = TestUtils.getJsonString("inkfiles/tags/tagsInSeq.ink.json"); + Story story = new Story(json); + + Assert.assertEquals("A red sequence.\n", story.Continue()); + Assert.assertArrayEquals(new String[]{"red"}, story.getCurrentTags().toArray()); + + Assert.assertEquals("A white sequence.\n", story.Continue()); + Assert.assertArrayEquals(new String[]{"white"}, story.getCurrentTags().toArray()); + } - String json = TestUtils.getJsonString("inkfiles/tags/tags.ink.json"); - Story story = new Story(json); + @Test + public void testTagsInChoice() throws Exception { - String[] globalTags = { "author: Joe", "title: My Great Story" }; - String[] knotTags = { "knot tag" }; - String[] knotTagWhenContinuedTwice = { "end of knot tag" }; - String[] stitchTags = { "stitch tag" }; + String json = TestUtils.getJsonString("inkfiles/tags/tagsInChoice.ink.json"); + Story story = new Story(json); - Assert.assertArrayEquals(globalTags, story.getGlobalTags().toArray()); + story.Continue(); + Assert.assertEquals(0, story.getCurrentTags().size()); + Assert.assertEquals(1, story.getCurrentChoices().size()); + Assert.assertArrayEquals(new String[]{"one", "two"}, story.getCurrentChoices().get(0).getTags().toArray()); - Assert.assertEquals("This is the content\n", story.Continue()); + story.chooseChoiceIndex(0); - Assert.assertArrayEquals(globalTags, story.getCurrentTags().toArray()); + Assert.assertEquals("one three", story.Continue()); + Assert.assertArrayEquals(new String[]{"one", "three"}, story.getCurrentTags().toArray()); + } - Assert.assertArrayEquals(knotTags, story.tagsForContentAtPath("knot").toArray()); - Assert.assertArrayEquals(stitchTags, story.tagsForContentAtPath("knot.stitch").toArray()); + @Test + public void testTagsDynamicContent() throws Exception { - story.choosePathString("knot"); - Assert.assertEquals("Knot content\n", story.Continue()); - Assert.assertArrayEquals(knotTags, story.getCurrentTags().toArray()); - Assert.assertEquals("", story.Continue()); - Assert.assertArrayEquals(knotTagWhenContinuedTwice, story.getCurrentTags().toArray()); - } + String json = TestUtils.getJsonString("inkfiles/tags/tagsDynamicContent.ink.json"); + Story story = new Story(json); + Assert.assertEquals("tag\n", story.Continue()); + Assert.assertArrayEquals(new String[]{"pic8red.jpg"}, story.getCurrentTags().toArray()); + } } diff --git a/src/test/java/com/bladecoder/ink/runtime/test/VariableSpecTest.java b/src/test/java/com/bladecoder/ink/runtime/test/VariableSpecTest.java index a15d419..5ca0b22 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/VariableSpecTest.java +++ b/src/test/java/com/bladecoder/ink/runtime/test/VariableSpecTest.java @@ -1,83 +1,82 @@ package com.bladecoder.ink.runtime.test; -import java.util.ArrayList; -import java.util.List; - +import com.bladecoder.ink.runtime.Story; import org.junit.Assert; import org.junit.Test; -import com.bladecoder.ink.runtime.Story; +import java.util.ArrayList; +import java.util.List; public class VariableSpecTest { - /** - * "- be declared with a VAR statement and print out a text value when used in - * content" - */ - @Test - public void variableDeclaration() throws Exception { - List text = new ArrayList<>(); - - String json = TestUtils.getJsonString("inkfiles/variable/variable-declaration.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("\"My name is Jean Passepartout, but my friend's call me Jackie. I'm 23 years old.\"", - text.get(0)); - } - - /** - * "- be declared with a VAR statement and print out a text value when used in - * content" - */ - @Test - public void varCalc() throws Exception { - List text = new ArrayList<>(); - - String json = TestUtils.getJsonString("inkfiles/variable/varcalc.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The values are 1 and -1 and -6 and aa.", text.get(0)); - } - - @Test - public void varStringIncBug() throws Exception { - List text = new ArrayList<>(); - - String json = TestUtils.getJsonString("inkfiles/variable/varstringinc.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - - story.chooseChoiceIndex(0); - System.out.println("-VAR A->" + story.getVariablesState().get("v").toString()); - text.clear(); - TestUtils.nextAll(story, text); - System.out.println("--VAR A->" + story.getVariablesState().get("v").toString()); - - Assert.assertEquals(2, text.size()); - Assert.assertEquals("ab.", text.get(1)); - } - - /** - * "- be declarable as diverts and be usable in text" - */ - @Test - public void varDivert() throws Exception { - List text = new ArrayList<>(); - - String json = TestUtils.getJsonString("inkfiles/variable/var-divert.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - story.chooseChoiceIndex(1); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("Everybody dies.", text.get(0)); - } + /** + * "- be declared with a VAR statement and print out a text value when used in + * content" + */ + @Test + public void variableDeclaration() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/variable/variable-declaration.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("\"My name is Jean Passepartout, but my friend's call me Jackie. I'm 23 years old.\"", + text.get(0)); + } + + /** + * "- be declared with a VAR statement and print out a text value when used in + * content" + */ + @Test + public void varCalc() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/variable/varcalc.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The values are true and -1 and -6 and aa.", text.get(0)); + } + + @Test + public void varStringIncBug() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/variable/varstringinc.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + + story.chooseChoiceIndex(0); + System.out.println("-VAR A->" + story.getVariablesState().get("v").toString()); + text.clear(); + TestUtils.nextAll(story, text); + System.out.println("--VAR A->" + story.getVariablesState().get("v").toString()); + + Assert.assertEquals(2, text.size()); + Assert.assertEquals("ab.", text.get(1)); + } + + /** + * "- be declarable as diverts and be usable in text" + */ + @Test + public void varDivert() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/variable/var-divert.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + story.chooseChoiceIndex(1); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("Everybody dies.", text.get(0)); + } } diff --git a/src/test/resources/inkfiles/basictext/oneline.ink.json b/src/test/resources/inkfiles/basictext/oneline.ink.json index fff0ff9..8a6354a 100644 --- a/src/test/resources/inkfiles/basictext/oneline.ink.json +++ b/src/test/resources/inkfiles/basictext/oneline.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^Line.","\n",["done",{"#n":"g-0"}],null],"done",null],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^Line.","\n",["done",{"#n":"g-0"}],null],"done",null],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/basictext/twolines.ink.json b/src/test/resources/inkfiles/basictext/twolines.ink.json index ab85b69..f1301db 100644 --- a/src/test/resources/inkfiles/basictext/twolines.ink.json +++ b/src/test/resources/inkfiles/basictext/twolines.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^Line.","\n","^Other line.","\n",["done",{"#n":"g-0"}],null],"done",null],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^Line.","\n","^Other line.","\n",["done",{"#n":"g-0"}],null],"done",null],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/choices/conditional-choice.ink.json b/src/test/resources/inkfiles/choices/conditional-choice.ink.json index ca924f8..c7055d6 100644 --- a/src/test/resources/inkfiles/choices/conditional-choice.ink.json +++ b/src/test/resources/inkfiles/choices/conditional-choice.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^Test conditional choices","\n",["ev",{"^->":"0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",1,0,"&&","/ev",{"*":"0.c-0","flg":19},{"s":["^not displayed",{"->":"$r","var":true},null]}],["ev",{"^->":"0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",1,1,"&&",1,1,"&&","&&","/ev",{"*":"0.c-1","flg":19},{"s":["^one",{"->":"$r","var":true},null]}],["ev",{"^->":"0.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",0,"/ev",{"*":"0.c-2","flg":19},{"s":["^not displayed",{"->":"$r","var":true},null]}],["ev",{"^->":"0.5.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",1,"/ev",{"*":"0.c-3","flg":19},{"s":["^two",{"->":"$r","var":true},null]}],["ev",{"^->":"0.6.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",1,1,"&&","/ev",{"*":"0.c-4","flg":19},{"s":["^three",{"->":"$r","var":true},null]}],["ev",{"^->":"0.7.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",1,"/ev",{"*":"0.c-5","flg":19},{"s":["^four",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":"0.2.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}],"c-1":["ev",{"^->":"0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":"0.3.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}],"c-2":["ev",{"^->":"0.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":"0.4.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}],"c-3":["ev",{"^->":"0.c-3.$r2"},"/ev",{"temp=":"$r"},{"->":"0.5.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}],"c-4":["ev",{"^->":"0.c-4.$r2"},"/ev",{"temp=":"$r"},{"->":"0.6.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}],"c-5":["ev",{"^->":"0.c-5.$r2"},"/ev",{"temp=":"$r"},{"->":"0.7.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",null],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^Test conditional choices","\n",["ev",{"^->":"0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",true,false,"&&","/ev",{"*":"0.c-0","flg":19},{"s":["^not displayed",{"->":"$r","var":true},null]}],["ev",{"^->":"0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",true,true,"&&",true,true,"&&","&&","/ev",{"*":"0.c-1","flg":19},{"s":["^one",{"->":"$r","var":true},null]}],["ev",{"^->":"0.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",false,"/ev",{"*":"0.c-2","flg":19},{"s":["^not displayed",{"->":"$r","var":true},null]}],["ev",{"^->":"0.5.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",true,"/ev",{"*":"0.c-3","flg":19},{"s":["^two",{"->":"$r","var":true},null]}],["ev",{"^->":"0.6.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",true,true,"&&","/ev",{"*":"0.c-4","flg":19},{"s":["^three",{"->":"$r","var":true},null]}],["ev",{"^->":"0.7.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",true,"/ev",{"*":"0.c-5","flg":19},{"s":["^four",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":"0.2.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}],"c-1":["ev",{"^->":"0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":"0.3.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}],"c-2":["ev",{"^->":"0.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":"0.4.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}],"c-3":["ev",{"^->":"0.c-3.$r2"},"/ev",{"temp=":"$r"},{"->":"0.5.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}],"c-4":["ev",{"^->":"0.c-4.$r2"},"/ev",{"temp=":"$r"},{"->":"0.6.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}],"c-5":["ev",{"^->":"0.c-5.$r2"},"/ev",{"temp=":"$r"},{"->":"0.7.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",null],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/choices/divert-choice.ink.json b/src/test/resources/inkfiles/choices/divert-choice.ink.json index 0d2f5fe..c0992f8 100644 --- a/src/test/resources/inkfiles/choices/divert-choice.ink.json +++ b/src/test/resources/inkfiles/choices/divert-choice.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[{"->":"knot"},["done",{"#n":"g-0"}],null],"done",{"knot":[["^You see a soldier.","\n","ev","str","^Pull a face","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Shove the guard aside","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Grapple and fight","/str",{"CNT?":".^.c-1"},"/ev",{"*":".^.c-2","flg":21},{"c-0":["\n","^You pull a face, and the soldier comes at you! ",{"->":".^.^.c-1"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ You shove the guard to one side, but he comes back swinging.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"knot"},"end",null]}],null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[{"->":"knot"},["done",{"#n":"g-0"}],null],"done",{"knot":[["^You see a soldier.","\n","ev","str","^Pull a face","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Shove the guard aside","/str","/ev",{"*":".^.c-1","flg":20},"ev","str","^Grapple and fight","/str",{"CNT?":".^.c-1"},"/ev",{"*":".^.c-2","flg":21},{"c-0":["\n","^You pull a face, and the soldier comes at you! ",{"->":".^.^.c-1"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["^ You shove the guard to one side, but he comes back swinging.","\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":[{"->":"knot"},"end",null]}],null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/choices/fallback-choice.ink.json b/src/test/resources/inkfiles/choices/fallback-choice.ink.json index 6082bde..1c748a0 100644 --- a/src/test/resources/inkfiles/choices/fallback-choice.ink.json +++ b/src/test/resources/inkfiles/choices/fallback-choice.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[{"->":"find_help"},["done",{"#n":"g-0"}],null],"done",{"find_help":[["^You search desperately for a friendly face in the crowd.","\n",["ev",{"^->":"find_help.0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^?","/str","/ev",{"*":".^.^.c-0","flg":22},{"s":["^The woman in the hat",{"->":"$r","var":true},null]}],["ev",{"^->":"find_help.0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^?","/str","/ev",{"*":".^.^.c-1","flg":22},{"s":["^The man with the briefcase",{"->":"$r","var":true},null]}],{"*":".^.c-2","flg":24},{"c-0":["ev",{"^->":"find_help.0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"^ pushes you roughly aside. ",{"->":".^.^.^"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["ev",{"^->":"find_help.0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"^ looks disgusted as you stumble past him. ",{"->":".^.^.^"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^But it is too late: you collapse onto the station platform. This is the end.","\n","end",null]}],null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[{"->":"find_help"},["done",{"#n":"g-0"}],null],"done",{"find_help":[["^You search desperately for a friendly face in the crowd.","\n",["ev",{"^->":"find_help.0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^?","/str","/ev",{"*":".^.^.c-0","flg":22},{"s":["^The woman in the hat",{"->":"$r","var":true},null]}],["ev",{"^->":"find_help.0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^?","/str","/ev",{"*":".^.^.c-1","flg":22},{"s":["^The man with the briefcase",{"->":"$r","var":true},null]}],{"*":".^.c-2","flg":24},{"c-0":["ev",{"^->":"find_help.0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"^ pushes you roughly aside. ",{"->":".^.^.^"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["ev",{"^->":"find_help.0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"^ looks disgusted as you stumble past him. ",{"->":".^.^.^"},"\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^But it is too late: you collapse onto the station platform. This is the end.","\n","end",null]}],null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/choices/label-flow.ink.json b/src/test/resources/inkfiles/choices/label-flow.ink.json index 49f79c1..a468c3b 100644 --- a/src/test/resources/inkfiles/choices/label-flow.ink.json +++ b/src/test/resources/inkfiles/choices/label-flow.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[{"->":"meet_guard"},["done",{"#n":"g-0"}],null],"done",{"meet_guard":[["^The guard frowns at you.","\n","ev","str","^Greet him","/str","/ev",{"*":".^.c-0","flg":20},["ev",{"^->":"meet_guard.0.8.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.'","/str","/ev",{"*":".^.^.c-1","flg":22},{"s":["^'Get out of my way",{"->":"$r","var":true},null]}],{"c-0":["\n","^'Greetings.'","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["ev",{"^->":"meet_guard.0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.8.s"},[{"#n":"$r2"}],"^,' you tell the guard.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^'Hmm,' replies the guard.","\n",["ev",{"^->":"meet_guard.0.g-0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",{"CNT?":".^.^.^.c-0"},"/ev",{"*":".^.^.c-2","flg":19},{"s":["^'Having a nice day?'",{"->":"$r","var":true},null]}],["ev",{"^->":"meet_guard.0.g-0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-3","flg":18},{"s":["^'Hmm?'",{"->":"$r","var":true},null]}],"ev","str","^Shove him aside","/str",{"CNT?":".^.^.c-1"},"/ev",{"*":".^.c-4","flg":21},{"c-2":["ev",{"^->":"meet_guard.0.g-0.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-3":["ev",{"^->":"meet_guard.0.g-0.c-3.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"^ you reply. ","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-4":["\n","^You shove him sharply. He stares in reply, and draws his sword!","\n","end",{"->":".^.^.^.g-1"},{"#f":5}]}],"g-1":["^'Mff,' the guard replies, and then offers you a paper bag. 'Toffee?'","\n","end",null]}],null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[{"->":"meet_guard"},["done",{"#n":"g-0"}],null],"done",{"meet_guard":[["^The guard frowns at you.","\n","ev","str","^Greet him","/str","/ev",{"*":".^.c-0","flg":20},["ev",{"^->":"meet_guard.0.8.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.'","/str","/ev",{"*":".^.^.c-1","flg":22},{"s":["^'Get out of my way",{"->":"$r","var":true},null]}],{"c-0":["\n","^'Greetings.'","\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["ev",{"^->":"meet_guard.0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.8.s"},[{"#n":"$r2"}],"^,' you tell the guard.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^'Hmm,' replies the guard.","\n",["ev",{"^->":"meet_guard.0.g-0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",{"CNT?":".^.^.^.c-0"},"/ev",{"*":".^.^.c-2","flg":19},{"s":["^'Having a nice day?'",{"->":"$r","var":true},null]}],["ev",{"^->":"meet_guard.0.g-0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-3","flg":18},{"s":["^'Hmm?'",{"->":"$r","var":true},null]}],"ev","str","^Shove him aside","/str",{"CNT?":".^.^.c-1"},"/ev",{"*":".^.c-4","flg":21},{"c-2":["ev",{"^->":"meet_guard.0.g-0.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-3":["ev",{"^->":"meet_guard.0.g-0.c-3.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"^ you reply. ","\n",{"->":".^.^.^.g-1"},{"#f":5}],"c-4":["\n","^You shove him sharply. He stares in reply, and draws his sword!","\n","end",{"->":".^.^.^.g-1"},{"#f":5}]}],"g-1":["^'Mff,' the guard replies, and then offers you a paper bag. 'Toffee?'","\n","end",null]}],null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/choices/label-scope-error.ink.json b/src/test/resources/inkfiles/choices/label-scope-error.ink.json index 2a4f6a8..f65ba89 100644 --- a/src/test/resources/inkfiles/choices/label-scope-error.ink.json +++ b/src/test/resources/inkfiles/choices/label-scope-error.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[{"->":"knot"},["done",{"#n":"g-0"}],null],"done",{"knot":[{"->":".^.stitch_one"},{"stitch_one":[[["ev",{"^->":"knot.stitch_one.0.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^an option",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"knot.stitch_one.0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.gatherpoint"},{"#f":5}],"gatherpoint":["^Some content.","\n",{"->":"knot.stitch_two"},{"#f":5}]}],null],"stitch_two":[[["ev",{"^->":"knot.stitch_two.0.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",{"CNT?":"knot.stitch_one.0.gatherpoint"},"/ev",{"*":".^.^.c-0","flg":19},{"s":["^Found gatherpoint",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"knot.stitch_two.0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n",{"#f":5}]}],null]}]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[{"->":"knot"},["done",{"#n":"g-0"}],null],"done",{"knot":[{"->":".^.stitch_one"},{"stitch_one":[[["ev",{"^->":"knot.stitch_one.0.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^an option",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"knot.stitch_one.0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.gatherpoint"},{"#f":5}],"gatherpoint":["^Some content.","\n",{"->":"knot.stitch_two"},{"#f":5}]}],null],"stitch_two":[[["ev",{"^->":"knot.stitch_two.0.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",{"CNT?":"knot.stitch_one.0.gatherpoint"},"/ev",{"*":".^.^.c-0","flg":19},{"s":["^Found gatherpoint",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"knot.stitch_two.0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n",{"#f":5}]}],null]}]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/choices/label-scope.ink.json b/src/test/resources/inkfiles/choices/label-scope.ink.json index 630b1e8..eb00f09 100644 --- a/src/test/resources/inkfiles/choices/label-scope.ink.json +++ b/src/test/resources/inkfiles/choices/label-scope.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[{"->":"knot"},["done",{"#n":"g-0"}],null],"done",{"knot":[{"->":".^.stitch_one"},{"stitch_one":[[["ev",{"^->":"knot.stitch_one.0.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^an option",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"knot.stitch_one.0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.gatherpoint"},{"#f":5}],"gatherpoint":["^Some content.","\n",{"->":"knot.stitch_two"},{"#f":5}]}],null],"stitch_two":[[["ev",{"^->":"knot.stitch_two.0.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",{"CNT?":"knot.stitch_one.0.gatherpoint"},"/ev",{"*":".^.^.c-0","flg":19},{"s":["^Found gatherpoint",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"knot.stitch_two.0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n","end",{"#f":5}]}],null]}]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[{"->":"knot"},["done",{"#n":"g-0"}],null],"done",{"knot":[{"->":".^.stitch_one"},{"stitch_one":[[["ev",{"^->":"knot.stitch_one.0.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^an option",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"knot.stitch_one.0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.gatherpoint"},{"#f":5}],"gatherpoint":["^Some content.","\n",{"->":"knot.stitch_two"},{"#f":5}]}],null],"stitch_two":[[["ev",{"^->":"knot.stitch_two.0.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",{"CNT?":"knot.stitch_one.0.gatherpoint"},"/ev",{"*":".^.^.c-0","flg":19},{"s":["^Found gatherpoint",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"knot.stitch_two.0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n","end",{"#f":5}]}],null]}]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/choices/mixed-choice.ink.json b/src/test/resources/inkfiles/choices/mixed-choice.ink.json index 5345668..d51fd82 100644 --- a/src/test/resources/inkfiles/choices/mixed-choice.ink.json +++ b/src/test/resources/inkfiles/choices/mixed-choice.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^Hello world!","\n",["ev",{"^->":"0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^back!","/str","/ev",{"*":"0.c-0","flg":22},{"s":["^Hello ",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":"0.2.s"},[{"#n":"$r2"}],"^ right back to you!","\n","^Nice to hear from you.","\n","done",{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",null],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^Hello world!","\n",["ev",{"^->":"0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^back!","/str","/ev",{"*":"0.c-0","flg":22},{"s":["^Hello ",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":"0.2.s"},[{"#n":"$r2"}],"^ right back to you!","\n","^Nice to hear from you.","\n","done",{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",null],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/choices/multi-choice.ink.json b/src/test/resources/inkfiles/choices/multi-choice.ink.json index 7b3458a..8b0ec6e 100644 --- a/src/test/resources/inkfiles/choices/multi-choice.ink.json +++ b/src/test/resources/inkfiles/choices/multi-choice.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^Hello, world!","\n",["ev",{"^->":"0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-0","flg":18},{"s":["^Hello back!",{"->":"$r","var":true},null]}],["ev",{"^->":"0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-1","flg":18},{"s":["^Goodbye",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":"0.2.s"},[{"#n":"$r2"}],"\n","^Nice to hear from you","\n",{"->":"0.g-0"},{"#f":5}],"c-1":["ev",{"^->":"0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":"0.3.s"},[{"#n":"$r2"}],"\n","^See you later","\n",{"->":"0.g-0"},{"#f":5}],"g-0":["end",["done",{"#n":"g-1"}],null]}],"done",null],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^Hello, world!","\n",["ev",{"^->":"0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-0","flg":18},{"s":["^Hello back!",{"->":"$r","var":true},null]}],["ev",{"^->":"0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-1","flg":18},{"s":["^Goodbye",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":"0.2.s"},[{"#n":"$r2"}],"\n","^Nice to hear from you","\n",{"->":"0.g-0"},{"#f":5}],"c-1":["ev",{"^->":"0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":"0.3.s"},[{"#n":"$r2"}],"\n","^See you later","\n",{"->":"0.g-0"},{"#f":5}],"g-0":["end",["done",{"#n":"g-1"}],null]}],"done",null],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/choices/no-choice-text.ink.json b/src/test/resources/inkfiles/choices/no-choice-text.ink.json index 27c2c81..20f6628 100644 --- a/src/test/resources/inkfiles/choices/no-choice-text.ink.json +++ b/src/test/resources/inkfiles/choices/no-choice-text.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^Hello world!","\n","ev","str","^Hello back!","/str","/ev",{"*":"0.c-0","flg":20},{"c-0":["\n","done",{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",null],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^Hello world!","\n","ev","str","^Hello back!","/str","/ev",{"*":"0.c-0","flg":20},{"c-0":["\n","done",{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",null],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/choices/one.ink.json b/src/test/resources/inkfiles/choices/one.ink.json index ba25d20..7ab756e 100644 --- a/src/test/resources/inkfiles/choices/one.ink.json +++ b/src/test/resources/inkfiles/choices/one.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^Hello world!","\n",["ev",{"^->":"0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-0","flg":18},{"s":["^Hello back!",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":"0.2.s"},[{"#n":"$r2"}],"\n","done",{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",null],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^Hello world!","\n",["ev",{"^->":"0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-0","flg":18},{"s":["^Hello back!",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":"0.2.s"},[{"#n":"$r2"}],"\n","done",{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",null],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/choices/single-choice.ink.json b/src/test/resources/inkfiles/choices/single-choice.ink.json index b4b2050..1a50964 100644 --- a/src/test/resources/inkfiles/choices/single-choice.ink.json +++ b/src/test/resources/inkfiles/choices/single-choice.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^Hello, world!","\n",["ev",{"^->":"0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-0","flg":18},{"s":["^Hello back!",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":"0.2.s"},[{"#n":"$r2"}],"\n","^Nice to hear from you","\n","end",{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",null],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^Hello, world!","\n",["ev",{"^->":"0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-0","flg":18},{"s":["^Hello back!",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":"0.2.s"},[{"#n":"$r2"}],"\n","^Nice to hear from you","\n","end",{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",null],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/choices/sticky-choice.ink.json b/src/test/resources/inkfiles/choices/sticky-choice.ink.json index 307a465..191e7fc 100644 --- a/src/test/resources/inkfiles/choices/sticky-choice.ink.json +++ b/src/test/resources/inkfiles/choices/sticky-choice.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[{"->":"homers_couch"},["done",{"#n":"g-0"}],null],"done",{"homers_couch":[["ev","str","^Eat another donut","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Get off the couch","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^You eat another donut. ",{"->":".^.^.^"},"\n",null],"c-1":["\n","^You struggle up off the couch to go and compose epic poetry.","\n","end",{"#f":5}]}],null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[{"->":"homers_couch"},["done",{"#n":"g-0"}],null],"done",{"homers_couch":[["ev","str","^Eat another donut","/str","/ev",{"*":".^.c-0","flg":4},"ev","str","^Get off the couch","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n","^You eat another donut. ",{"->":".^.^.^"},"\n",null],"c-1":["\n","^You struggle up off the couch to go and compose epic poetry.","\n","end",{"#f":5}]}],null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/choices/suppress-choice.ink.json b/src/test/resources/inkfiles/choices/suppress-choice.ink.json index fde20f2..308bcbd 100644 --- a/src/test/resources/inkfiles/choices/suppress-choice.ink.json +++ b/src/test/resources/inkfiles/choices/suppress-choice.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^Hello world!","\n","ev","str","^Hello back!","/str","/ev",{"*":"0.c-0","flg":20},{"c-0":["\n","^Nice to hear from you.","\n","end",{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",null],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^Hello world!","\n","ev","str","^Hello back!","/str","/ev",{"*":"0.c-0","flg":20},{"c-0":["\n","^Nice to hear from you.","\n","end",{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",null],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/choices/varying-choice.ink.json b/src/test/resources/inkfiles/choices/varying-choice.ink.json index c224920..8644e3e 100644 --- a/src/test/resources/inkfiles/choices/varying-choice.ink.json +++ b/src/test/resources/inkfiles/choices/varying-choice.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[{"->":"find_help"},["done",{"#n":"g-0"}],null],"done",{"find_help":[["^You search desperately for a friendly face in the crowd.","\n",["ev",{"^->":"find_help.0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^?","/str","/ev",{"*":".^.^.c-0","flg":22},{"s":["^The woman in the hat",{"->":"$r","var":true},null]}],["ev",{"^->":"find_help.0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^?","/str","/ev",{"*":".^.^.c-1","flg":22},{"s":["^The man with the briefcase",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"find_help.0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"^ pushes you roughly aside. ",{"->":".^.^.^"},"\n",{"#f":5}],"c-1":["ev",{"^->":"find_help.0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"^ looks disgusted as you stumble past him. ",{"->":".^.^.^"},"\n","done",{"#f":5}]}],null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[{"->":"find_help"},["done",{"#n":"g-0"}],null],"done",{"find_help":[["^You search desperately for a friendly face in the crowd.","\n",["ev",{"^->":"find_help.0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^?","/str","/ev",{"*":".^.^.c-0","flg":22},{"s":["^The woman in the hat",{"->":"$r","var":true},null]}],["ev",{"^->":"find_help.0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^?","/str","/ev",{"*":".^.^.c-1","flg":22},{"s":["^The man with the briefcase",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"find_help.0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"^ pushes you roughly aside. ",{"->":".^.^.^"},"\n",{"#f":5}],"c-1":["ev",{"^->":"find_help.0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"^ looks disgusted as you stumble past him. ",{"->":".^.^.^"},"\n","done",{"#f":5}]}],null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/conditional/condopt.ink.json b/src/test/resources/inkfiles/conditional/condopt.ink.json index 53414f3..c3ce23b 100644 --- a/src/test/resources/inkfiles/conditional/condopt.ink.json +++ b/src/test/resources/inkfiles/conditional/condopt.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^I looked...","\n","ev","str","^at the door","/str","/ev",{"*":"0.c-0","flg":20},"ev","str","^outside","/str","/ev",{"*":"0.c-1","flg":20},{"c-0":["\n",{"->":"door_open"},{"->":"0.g-0"},{"#f":5}],"c-1":["\n",{"->":"leave"},{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",{"door_open":["^at the door. It was open.","\n",{"->":"leave"},{"#f":1}],"leave":["^I stood up and...","\n","ev",{"CNT?":"door_open"},"/ev",[{"->":".^.b","c":true},{"b":["\n",["ev",{"^->":"leave.5.b.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^I strode out of the compartment",{"->":"$r","var":true},null]}],{"->":"leave.7"},{"c-0":["ev",{"^->":"leave.5.b.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"^ and I fancied I heard my master quietly tutting to himself. ","end","\n",{"#f":5}]}]}],[{"->":".^.b"},{"b":["\n",["ev",{"^->":"leave.6.b.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^I asked permission to leave",{"->":"$r","var":true},null]}],["ev",{"^->":"leave.6.b.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^I stood and went to open the door",{"->":"$r","var":true},null]}],{"->":"leave.7"},{"c-0":["ev",{"^->":"leave.6.b.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"^ and Monsieur Fogg looked surprised. ","end","\n",{"#f":5}],"c-1":["ev",{"^->":"leave.6.b.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"^. Monsieur Fogg seemed untroubled by this small rebellion. ","end","\n",{"#f":5}]}]}],"nop","\n",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^I looked...","\n","ev","str","^at the door","/str","/ev",{"*":"0.c-0","flg":20},"ev","str","^outside","/str","/ev",{"*":"0.c-1","flg":20},{"c-0":["\n",{"->":"door_open"},{"->":"0.g-0"},{"#f":5}],"c-1":["\n",{"->":"leave"},{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",{"door_open":["^at the door. It was open.","\n",{"->":"leave"},{"#f":1}],"leave":["^I stood up and...","\n","ev",{"CNT?":"door_open"},"/ev",[{"->":".^.b","c":true},{"b":["\n",["ev",{"^->":"leave.5.b.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^I strode out of the compartment",{"->":"$r","var":true},null]}],{"->":"leave.7"},{"c-0":["ev",{"^->":"leave.5.b.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"^ and I fancied I heard my master quietly tutting to himself. ","end","\n",{"#f":5}]}]}],[{"->":".^.b"},{"b":["\n",["ev",{"^->":"leave.6.b.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^I asked permission to leave",{"->":"$r","var":true},null]}],["ev",{"^->":"leave.6.b.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^I stood and went to open the door",{"->":"$r","var":true},null]}],{"->":"leave.7"},{"c-0":["ev",{"^->":"leave.6.b.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"^ and Monsieur Fogg looked surprised. ","end","\n",{"#f":5}],"c-1":["ev",{"^->":"leave.6.b.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"^. Monsieur Fogg seemed untroubled by this small rebellion. ","end","\n",{"#f":5}]}]}],"nop","\n",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/conditional/condtext.ink.json b/src/test/resources/inkfiles/conditional/condtext.ink.json index a0aac99..f527164 100644 --- a/src/test/resources/inkfiles/conditional/condtext.ink.json +++ b/src/test/resources/inkfiles/conditional/condtext.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^\"We are going on a trip,\" said Monsieur Fogg.","\n","ev","str","^The wager.","/str","/ev",{"*":"0.c-0","flg":20},"ev","str","^I was surprised.","/str","/ev",{"*":"0.c-1","flg":20},{"c-0":["^ ",{"->":"know_about_wager"},"\n",{"->":"0.g-0"},{"#f":5}],"c-1":["^ ",{"->":"i_stared"},"\n",{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",{"know_about_wager":["^I had heard about the wager.","\n",{"->":"i_stared"},{"#f":1}],"i_stared":["^I stared at Monsieur Fogg.","\n","ev",{"CNT?":"know_about_wager"},"/ev",[{"->":".^.b","c":true},{"b":["\n","<>","^ \"But surely you are not serious?\" I demanded.","\n",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["\n","<>","^ \"But there must be a reason for this trip,\" I observed.","\n",{"->":".^.^.^.7"},null]}],"nop","\n","^He said nothing in reply, merely considering his newspaper with as much thoroughness as entomologist considering his latest pinned addition.","\n","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^\"We are going on a trip,\" said Monsieur Fogg.","\n","ev","str","^The wager.","/str","/ev",{"*":"0.c-0","flg":20},"ev","str","^I was surprised.","/str","/ev",{"*":"0.c-1","flg":20},{"c-0":["^ ",{"->":"know_about_wager"},"\n",{"->":"0.g-0"},{"#f":5}],"c-1":["^ ",{"->":"i_stared"},"\n",{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",{"know_about_wager":["^I had heard about the wager.","\n",{"->":"i_stared"},{"#f":1}],"i_stared":["^I stared at Monsieur Fogg.","\n","ev",{"CNT?":"know_about_wager"},"/ev",[{"->":".^.b","c":true},{"b":["\n","<>","^ \"But surely you are not serious?\" I demanded.","\n",{"->":".^.^.^.7"},null]}],[{"->":".^.b"},{"b":["\n","<>","^ \"But there must be a reason for this trip,\" I observed.","\n",{"->":".^.^.^.7"},null]}],"nop","\n","^He said nothing in reply, merely considering his newspaper with as much thoroughness as entomologist considering his latest pinned addition.","\n","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/conditional/cycle.ink.json b/src/test/resources/inkfiles/conditional/cycle.ink.json index d3ba01f..5ff8ec9 100644 --- a/src/test/resources/inkfiles/conditional/cycle.ink.json +++ b/src/test/resources/inkfiles/conditional/cycle.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[{"->":"test"},["done",{"#n":"g-0"}],null],"done",{"test":[[["ev","visit",3,"%","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"nop",{"s0":["pop","\n","^I held my breath.","\n",{"->":".^.^.23"},null],"s1":["pop","\n","^I waited impatiently.","\n",{"->":".^.^.23"},null],"s2":["pop","\n","^I paused.","\n",{"->":".^.^.23"},null],"#f":5}],"\n","ev","str","^Try again","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"test"},"\n",null]}],null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[{"->":"test"},["done",{"#n":"g-0"}],null],"done",{"test":[[["ev","visit",3,"%","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"nop",{"s0":["pop","\n","^I held my breath.","\n",{"->":".^.^.23"},null],"s1":["pop","\n","^I waited impatiently.","\n",{"->":".^.^.23"},null],"s2":["pop","\n","^I paused.","\n",{"->":".^.^.23"},null],"#f":5}],"\n","ev","str","^Try again","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"test"},"\n",null]}],null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/conditional/ifelse-ext-text1.ink.json b/src/test/resources/inkfiles/conditional/ifelse-ext-text1.ink.json index 0dbbb55..ec72908 100644 --- a/src/test/resources/inkfiles/conditional/ifelse-ext-text1.ink.json +++ b/src/test/resources/inkfiles/conditional/ifelse-ext-text1.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[["ev",{"VAR?":"x"},0,"==","/ev",{"->":".^.b","c":true},{"b":["\n","^This is text 1.","\n",{"->":"0.3"},null]}],["ev",{"VAR?":"x"},0,">","/ev",{"->":".^.b","c":true},{"b":["\n","^This is text 2.","\n",{"->":"0.3"},null]}],[{"->":".^.b"},{"b":["\n","^This is text 3.","\n",{"->":"0.3"},null]}],"nop","\n","ev","str","^The Choice.","/str","/ev",{"*":"0.c-0","flg":4},{"c-0":["^ ",{"->":"to_end"},"\n",{"->":"0.g-0"},null],"g-0":["done",null]}],"done",{"to_end":["^This is the end. ","end","\n",null],"global decl":["ev",0,{"VAR=":"x"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[["ev",{"VAR?":"x"},0,"==","/ev",{"->":".^.b","c":true},{"b":["\n","^This is text 1.","\n",{"->":"0.3"},null]}],["ev",{"VAR?":"x"},0,">","/ev",{"->":".^.b","c":true},{"b":["\n","^This is text 2.","\n",{"->":"0.3"},null]}],[{"->":".^.b"},{"b":["\n","^This is text 3.","\n",{"->":"0.3"},null]}],"nop","\n","ev","str","^The Choice.","/str","/ev",{"*":"0.c-0","flg":4},{"c-0":["^ ",{"->":"to_end"},"\n",{"->":"0.g-0"},null],"g-0":["done",null]}],"done",{"to_end":["^This is the end. ","end","\n",null],"global decl":["ev",0,{"VAR=":"x"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/conditional/ifelse-ext-text2.ink.json b/src/test/resources/inkfiles/conditional/ifelse-ext-text2.ink.json index 208b441..431cd02 100644 --- a/src/test/resources/inkfiles/conditional/ifelse-ext-text2.ink.json +++ b/src/test/resources/inkfiles/conditional/ifelse-ext-text2.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[["ev",{"VAR?":"x"},0,"==","/ev",{"->":".^.b","c":true},{"b":["\n","^This is text 1.","\n",{"->":"0.3"},null]}],["ev",{"VAR?":"x"},0,">","/ev",{"->":".^.b","c":true},{"b":["\n","^This is text 2.","\n",{"->":"0.3"},null]}],[{"->":".^.b"},{"b":["\n","^This is text 3.","\n",{"->":"0.3"},null]}],"nop","\n","ev","str","^The Choice.","/str","/ev",{"*":"0.c-0","flg":4},{"c-0":["^ ",{"->":"to_end"},"\n",{"->":"0.g-0"},null],"g-0":["done",null]}],"done",{"to_end":["^This is the end. ","end","\n",null],"global decl":["ev",2,{"VAR=":"x"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[["ev",{"VAR?":"x"},0,"==","/ev",{"->":".^.b","c":true},{"b":["\n","^This is text 1.","\n",{"->":"0.3"},null]}],["ev",{"VAR?":"x"},0,">","/ev",{"->":".^.b","c":true},{"b":["\n","^This is text 2.","\n",{"->":"0.3"},null]}],[{"->":".^.b"},{"b":["\n","^This is text 3.","\n",{"->":"0.3"},null]}],"nop","\n","ev","str","^The Choice.","/str","/ev",{"*":"0.c-0","flg":4},{"c-0":["^ ",{"->":"to_end"},"\n",{"->":"0.g-0"},null],"g-0":["done",null]}],"done",{"to_end":["^This is the end. ","end","\n",null],"global decl":["ev",2,{"VAR=":"x"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/conditional/ifelse-ext-text3.ink.json b/src/test/resources/inkfiles/conditional/ifelse-ext-text3.ink.json index e68b209..aeb8611 100644 --- a/src/test/resources/inkfiles/conditional/ifelse-ext-text3.ink.json +++ b/src/test/resources/inkfiles/conditional/ifelse-ext-text3.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[["ev",{"VAR?":"x"},0,"==","/ev",{"->":".^.b","c":true},{"b":["\n","^This is text 1.","\n",{"->":"0.3"},null]}],["ev",{"VAR?":"x"},0,">","/ev",{"->":".^.b","c":true},{"b":["\n","^This is text 2.","\n",{"->":"0.3"},null]}],[{"->":".^.b"},{"b":["\n","^This is text 3.","\n",{"->":"0.3"},null]}],"nop","\n","ev","str","^The Choice.","/str","/ev",{"*":"0.c-0","flg":4},{"c-0":["^ ",{"->":"to_end"},"\n",{"->":"0.g-0"},null],"g-0":["done",null]}],"done",{"to_end":["^This is the end. ","end","\n",null],"global decl":["ev",-2,{"VAR=":"x"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[["ev",{"VAR?":"x"},0,"==","/ev",{"->":".^.b","c":true},{"b":["\n","^This is text 1.","\n",{"->":"0.3"},null]}],["ev",{"VAR?":"x"},0,">","/ev",{"->":".^.b","c":true},{"b":["\n","^This is text 2.","\n",{"->":"0.3"},null]}],[{"->":".^.b"},{"b":["\n","^This is text 3.","\n",{"->":"0.3"},null]}],"nop","\n","ev","str","^The Choice.","/str","/ev",{"*":"0.c-0","flg":4},{"c-0":["^ ",{"->":"to_end"},"\n",{"->":"0.g-0"},null],"g-0":["done",null]}],"done",{"to_end":["^This is the end. ","end","\n",null],"global decl":["ev",-2,{"VAR=":"x"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/conditional/ifelse-ext.ink.json b/src/test/resources/inkfiles/conditional/ifelse-ext.ink.json index 4a1f458..03e8041 100644 --- a/src/test/resources/inkfiles/conditional/ifelse-ext.ink.json +++ b/src/test/resources/inkfiles/conditional/ifelse-ext.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[["ev",{"VAR?":"x"},0,"==","/ev",{"->":".^.b","c":true},{"b":["\n","ev",0,"/ev",{"VAR=":"y","re":true},{"->":"0.3"},null]}],["ev",{"VAR?":"x"},0,">","/ev",{"->":".^.b","c":true},{"b":["\n","ev",{"VAR?":"x"},1,"-","/ev",{"VAR=":"y","re":true},{"->":"0.3"},null]}],[{"->":".^.b"},{"b":["\n","ev",{"VAR?":"x"},1,"+","/ev",{"VAR=":"y","re":true},{"->":"0.3"},null]}],"nop","\n","^The value is ","ev",{"VAR?":"y"},"out","/ev","^. ","end","\n",["done",{"#n":"g-0"}],null],"done",{"global decl":["ev",-2,{"VAR=":"x"},3,{"VAR=":"y"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[["ev",{"VAR?":"x"},0,"==","/ev",{"->":".^.b","c":true},{"b":["\n","ev",0,"/ev",{"VAR=":"y","re":true},{"->":"0.3"},null]}],["ev",{"VAR?":"x"},0,">","/ev",{"->":".^.b","c":true},{"b":["\n","ev",{"VAR?":"x"},1,"-","/ev",{"VAR=":"y","re":true},{"->":"0.3"},null]}],[{"->":".^.b"},{"b":["\n","ev",{"VAR?":"x"},1,"+","/ev",{"VAR=":"y","re":true},{"->":"0.3"},null]}],"nop","\n","^The value is ","ev",{"VAR?":"y"},"out","/ev","^. ","end","\n",["done",{"#n":"g-0"}],null],"done",{"global decl":["ev",-2,{"VAR=":"x"},3,{"VAR=":"y"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/conditional/ifelse.ink.json b/src/test/resources/inkfiles/conditional/ifelse.ink.json index ddaa5f7..0084640 100644 --- a/src/test/resources/inkfiles/conditional/ifelse.ink.json +++ b/src/test/resources/inkfiles/conditional/ifelse.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["ev",{"VAR?":"x"},0,">","/ev",[{"->":".^.b","c":true},{"b":["\n","ev",{"VAR?":"x"},1,"-","/ev",{"VAR=":"y","re":true},{"->":"0.7"},null]}],[{"->":".^.b"},{"b":["\n","ev",{"VAR?":"x"},1,"+","/ev",{"VAR=":"y","re":true},{"->":"0.7"},null]}],"nop","\n","^The value is ","ev",{"VAR?":"y"},"out","/ev","^. ","end","\n",["done",{"#n":"g-0"}],null],"done",{"global decl":["ev",0,{"VAR=":"x"},3,{"VAR=":"y"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["ev",{"VAR?":"x"},0,">","/ev",[{"->":".^.b","c":true},{"b":["\n","ev",{"VAR?":"x"},1,"-","/ev",{"VAR=":"y","re":true},{"->":"0.7"},null]}],[{"->":".^.b"},{"b":["\n","ev",{"VAR?":"x"},1,"+","/ev",{"VAR=":"y","re":true},{"->":"0.7"},null]}],"nop","\n","^The value is ","ev",{"VAR?":"y"},"out","/ev","^. ","end","\n",["done",{"#n":"g-0"}],null],"done",{"global decl":["ev",0,{"VAR=":"x"},3,{"VAR=":"y"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/conditional/iffalse.ink.json b/src/test/resources/inkfiles/conditional/iffalse.ink.json index 5281b56..e38e29c 100644 --- a/src/test/resources/inkfiles/conditional/iffalse.ink.json +++ b/src/test/resources/inkfiles/conditional/iffalse.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["ev",{"VAR?":"x"},0,">","/ev",[{"->":".^.b","c":true},{"b":["\n","ev",{"VAR?":"x"},1,"-","/ev",{"VAR=":"y","re":true},{"->":"0.6"},null]}],"nop","\n","^The value is ","ev",{"VAR?":"y"},"out","/ev","^. ","end","\n",["done",{"#n":"g-0"}],null],"done",{"global decl":["ev",0,{"VAR=":"x"},3,{"VAR=":"y"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["ev",{"VAR?":"x"},0,">","/ev",[{"->":".^.b","c":true},{"b":["\n","ev",{"VAR?":"x"},1,"-","/ev",{"VAR=":"y","re":true},{"->":"0.6"},null]}],"nop","\n","^The value is ","ev",{"VAR?":"y"},"out","/ev","^. ","end","\n",["done",{"#n":"g-0"}],null],"done",{"global decl":["ev",0,{"VAR=":"x"},3,{"VAR=":"y"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/conditional/iftrue.ink.json b/src/test/resources/inkfiles/conditional/iftrue.ink.json index fa78335..31e02cd 100644 --- a/src/test/resources/inkfiles/conditional/iftrue.ink.json +++ b/src/test/resources/inkfiles/conditional/iftrue.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["ev",{"VAR?":"x"},0,">","/ev",[{"->":".^.b","c":true},{"b":["\n","ev",{"VAR?":"x"},1,"-","/ev",{"VAR=":"y","re":true},{"->":"0.6"},null]}],"nop","\n","^The value is ","ev",{"VAR?":"y"},"out","/ev","^. ","end","\n",["done",{"#n":"g-0"}],null],"done",{"global decl":["ev",2,{"VAR=":"x"},0,{"VAR=":"y"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["ev",{"VAR?":"x"},0,">","/ev",[{"->":".^.b","c":true},{"b":["\n","ev",{"VAR?":"x"},1,"-","/ev",{"VAR=":"y","re":true},{"->":"0.6"},null]}],"nop","\n","^The value is ","ev",{"VAR?":"y"},"out","/ev","^. ","end","\n",["done",{"#n":"g-0"}],null],"done",{"global decl":["ev",2,{"VAR=":"x"},0,{"VAR=":"y"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/conditional/multiline-choice.ink.json b/src/test/resources/inkfiles/conditional/multiline-choice.ink.json index 83da1d4..2d47d41 100644 --- a/src/test/resources/inkfiles/conditional/multiline-choice.ink.json +++ b/src/test/resources/inkfiles/conditional/multiline-choice.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[{"->":"test"},["done",{"#n":"g-0"}],null],"done",{"test":[[["ev","visit",2,"MIN","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"nop",{"s0":["pop","\n","^At the table, I drew a card. Ace of Hearts.","\n",{"->":".^.^.23"},null],"s1":["pop","\n","^2 of Diamonds.","\n","^\"Should I hit you again,\" the croupier asks.","\n","ev","str","^No.","/str","/ev",{"*":".^.c-0","flg":20},{"->":".^.^.23"},{"c-0":["^ I left the table. ","end","\n",{"#f":5}]}],"s2":["pop","\n","^King of Spades.","\n","^\"You lose,\" he crowed.","\n","end",{"->":".^.^.23"},null],"#f":5}],"\n","ev","str","^Draw a card","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ I drew a card. ",{"->":"test"},"\n",null]}],null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[{"->":"test"},["done",{"#n":"g-0"}],null],"done",{"test":[[["ev","visit",2,"MIN","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"nop",{"s0":["pop","\n","^At the table, I drew a card. Ace of Hearts.","\n",{"->":".^.^.23"},null],"s1":["pop","\n","^2 of Diamonds.","\n","^\"Should I hit you again,\" the croupier asks.","\n","ev","str","^No.","/str","/ev",{"*":".^.c-0","flg":20},{"->":".^.^.23"},{"c-0":["^ I left the table. ","end","\n",{"#f":5}]}],"s2":["pop","\n","^King of Spades.","\n","^\"You lose,\" he crowed.","\n","end",{"->":".^.^.23"},null],"#f":5}],"\n","ev","str","^Draw a card","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ I drew a card. ",{"->":"test"},"\n",null]}],null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/conditional/multiline-divert.ink.json b/src/test/resources/inkfiles/conditional/multiline-divert.ink.json index 830d6c8..805a979 100644 --- a/src/test/resources/inkfiles/conditional/multiline-divert.ink.json +++ b/src/test/resources/inkfiles/conditional/multiline-divert.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[{"->":"test"},["done",{"#n":"g-0"}],null],"done",{"test":[[["ev","visit",2,"MIN","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"nop",{"s0":["pop","\n","^At the table, I drew a card. Ace of Hearts.","\n",{"->":".^.^.23"},null],"s1":["pop","\n","<>","^ 2 of Diamonds.","\n","^\"Should I hit you again,\" the croupier asks.","\n",{"->":".^.^.23"},null],"s2":["pop","\n","<>","^ King of Spades.","\n",{"->":"he_crowed"},{"->":".^.^.23"},null],"#f":5}],"\n","ev","str","^Draw a card","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ I drew a card. ",{"->":"test"},"\n",null]}],null],"he_crowed":["^\"You lose,\" he crowed.","\n","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[{"->":"test"},["done",{"#n":"g-0"}],null],"done",{"test":[[["ev","visit",2,"MIN","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"nop",{"s0":["pop","\n","^At the table, I drew a card. Ace of Hearts.","\n",{"->":".^.^.23"},null],"s1":["pop","\n","<>","^ 2 of Diamonds.","\n","^\"Should I hit you again,\" the croupier asks.","\n",{"->":".^.^.23"},null],"s2":["pop","\n","<>","^ King of Spades.","\n",{"->":"he_crowed"},{"->":".^.^.23"},null],"#f":5}],"\n","ev","str","^Draw a card","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ I drew a card. ",{"->":"test"},"\n",null]}],null],"he_crowed":["^\"You lose,\" he crowed.","\n","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/conditional/multiline.ink.json b/src/test/resources/inkfiles/conditional/multiline.ink.json index b9a26a9..99132bc 100644 --- a/src/test/resources/inkfiles/conditional/multiline.ink.json +++ b/src/test/resources/inkfiles/conditional/multiline.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[{"->":"test"},["done",{"#n":"g-0"}],null],"done",{"test":[[["ev","visit",2,"MIN","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"nop",{"s0":["pop","\n","^At the table, I drew a card. Ace of Hearts.","\n",{"->":".^.^.23"},null],"s1":["pop","\n","<>","^ 2 of Diamonds.","\n","^\"Should I hit you again,\" the croupier asks.","\n",{"->":".^.^.23"},null],"s2":["pop","\n","<>","^ King of Spades.","\n","^\"You lose,\" he crowed.","\n",{"->":".^.^.23"},null],"#f":5}],"\n","ev","str","^Draw a card","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ I drew a card. ",{"->":"test"},"\n",null]}],null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[{"->":"test"},["done",{"#n":"g-0"}],null],"done",{"test":[[["ev","visit",2,"MIN","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"nop",{"s0":["pop","\n","^At the table, I drew a card. Ace of Hearts.","\n",{"->":".^.^.23"},null],"s1":["pop","\n","<>","^ 2 of Diamonds.","\n","^\"Should I hit you again,\" the croupier asks.","\n",{"->":".^.^.23"},null],"s2":["pop","\n","<>","^ King of Spades.","\n","^\"You lose,\" he crowed.","\n",{"->":".^.^.23"},null],"#f":5}],"\n","ev","str","^Draw a card","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ I drew a card. ",{"->":"test"},"\n",null]}],null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/conditional/once.ink.json b/src/test/resources/inkfiles/conditional/once.ink.json index d329ac5..39bead8 100644 --- a/src/test/resources/inkfiles/conditional/once.ink.json +++ b/src/test/resources/inkfiles/conditional/once.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[{"->":"test"},["done",{"#n":"g-0"}],null],"done",{"test":[[["ev","visit",2,"MIN","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"nop",{"s0":["pop","\n","^Would my luck hold?","\n",{"->":".^.^.23"},null],"s1":["pop","\n","^Could I win the hand?","\n",{"->":".^.^.23"},null],"s2":["pop",{"->":".^.^.23"},null],"#f":5}],"\n","ev","str","^Try again","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"test"},"\n",null]}],null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[{"->":"test"},["done",{"#n":"g-0"}],null],"done",{"test":[[["ev","visit",2,"MIN","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"nop",{"s0":["pop","\n","^Would my luck hold?","\n",{"->":".^.^.23"},null],"s1":["pop","\n","^Could I win the hand?","\n",{"->":".^.^.23"},null],"s2":["pop",{"->":".^.^.23"},null],"#f":5}],"\n","ev","str","^Try again","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"test"},"\n",null]}],null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/conditional/shuffle.ink.json b/src/test/resources/inkfiles/conditional/shuffle.ink.json index 8255cd9..7a3493d 100644 --- a/src/test/resources/inkfiles/conditional/shuffle.ink.json +++ b/src/test/resources/inkfiles/conditional/shuffle.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[{"->":"test"},["done",{"#n":"g-0"}],null],"done",{"test":[[["ev","visit",3,"seq","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"nop",{"s0":["pop","\n","^Ace of Hearts.","\n",{"->":".^.^.23"},null],"s1":["pop","\n","^King of Spades.","\n",{"->":".^.^.23"},null],"s2":["pop","\n","^2 of Diamonds.","\n",{"->":".^.^.23"},null],"#f":5}],"\n","ev","str","^Try again","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"test"},"\n",null]}],null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[{"->":"test"},["done",{"#n":"g-0"}],null],"done",{"test":[[["ev","visit",3,"seq","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"nop",{"s0":["pop","\n","^Ace of Hearts.","\n",{"->":".^.^.23"},null],"s1":["pop","\n","^King of Spades.","\n",{"->":".^.^.23"},null],"s2":["pop","\n","^2 of Diamonds.","\n",{"->":".^.^.23"},null],"#f":5}],"\n","ev","str","^Try again","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"test"},"\n",null]}],null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/conditional/shuffle_once.ink.json b/src/test/resources/inkfiles/conditional/shuffle_once.ink.json index a6bd40c..38075a4 100644 --- a/src/test/resources/inkfiles/conditional/shuffle_once.ink.json +++ b/src/test/resources/inkfiles/conditional/shuffle_once.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[{"->":"test"},["done",{"#n":"g-0"}],null],"done",{"test":[[["ev","visit",2,"MIN","du",2,"==",{"->":".^.10","c":true},2,"seq","nop","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"nop",{"s0":["pop","\n","^one","\n",{"->":".^.^.30"},null],"s1":["pop","\n","^two","\n",{"->":".^.^.30"},null],"s2":["pop",{"->":".^.^.30"},null],"#f":5}],"\n","ev","str","^Try again","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"test"},"\n",null]}],null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[{"->":"test"},["done",{"#n":"g-0"}],null],"done",{"test":[[["ev","visit",2,"MIN","du",2,"==",{"->":".^.10","c":true},2,"seq","nop","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"nop",{"s0":["pop","\n","^one","\n",{"->":".^.^.30"},null],"s1":["pop","\n","^two","\n",{"->":".^.^.30"},null],"s2":["pop",{"->":".^.^.30"},null],"#f":5}],"\n","ev","str","^Try again","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"test"},"\n",null]}],null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/conditional/shuffle_stopping.ink.json b/src/test/resources/inkfiles/conditional/shuffle_stopping.ink.json index 205e6a2..0eb4bd8 100644 --- a/src/test/resources/inkfiles/conditional/shuffle_stopping.ink.json +++ b/src/test/resources/inkfiles/conditional/shuffle_stopping.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[{"->":"test"},["done",{"#n":"g-0"}],null],"done",{"test":[[["ev","visit",2,"MIN","du",2,"==",{"->":".^.10","c":true},2,"seq","nop","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"nop",{"s0":["pop","\n","^one","\n",{"->":".^.^.30"},null],"s1":["pop","\n","^two","\n",{"->":".^.^.30"},null],"s2":["pop","\n","^final","\n",{"->":".^.^.30"},null],"#f":5}],"\n","ev","str","^Try again","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"test"},"\n",null]}],null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[{"->":"test"},["done",{"#n":"g-0"}],null],"done",{"test":[[["ev","visit",2,"MIN","du",2,"==",{"->":".^.10","c":true},2,"seq","nop","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"nop",{"s0":["pop","\n","^one","\n",{"->":".^.^.30"},null],"s1":["pop","\n","^two","\n",{"->":".^.^.30"},null],"s2":["pop","\n","^final","\n",{"->":".^.^.30"},null],"#f":5}],"\n","ev","str","^Try again","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"test"},"\n",null]}],null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/conditional/stopping.ink.json b/src/test/resources/inkfiles/conditional/stopping.ink.json index a5182d5..9b5cd6c 100644 --- a/src/test/resources/inkfiles/conditional/stopping.ink.json +++ b/src/test/resources/inkfiles/conditional/stopping.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[{"->":"test"},["done",{"#n":"g-0"}],null],"done",{"test":[[["ev","visit",2,"MIN","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"nop",{"s0":["pop","\n","^I entered the casino.","\n",{"->":".^.^.23"},null],"s1":["pop","\n","^I entered the casino again.","\n",{"->":".^.^.23"},null],"s2":["pop","\n","^Once more, I went inside.","\n",{"->":".^.^.23"},null],"#f":5}],"\n","ev","str","^Try again","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"test"},"\n",null]}],null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[{"->":"test"},["done",{"#n":"g-0"}],null],"done",{"test":[[["ev","visit",2,"MIN","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"nop",{"s0":["pop","\n","^I entered the casino.","\n",{"->":".^.^.23"},null],"s1":["pop","\n","^I entered the casino again.","\n",{"->":".^.^.23"},null],"s2":["pop","\n","^Once more, I went inside.","\n",{"->":".^.^.23"},null],"#f":5}],"\n","ev","str","^Try again","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"test"},"\n",null]}],null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/divert/complex-branching.ink.json b/src/test/resources/inkfiles/divert/complex-branching.ink.json index 5019871..4f76827 100644 --- a/src/test/resources/inkfiles/divert/complex-branching.ink.json +++ b/src/test/resources/inkfiles/divert/complex-branching.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[{"->":"back_in_london"},["done",{"#n":"g-0"}],null],"done",{"back_in_london":[["^We arrived into London at 9.45pm exactly.","\n",["ev",{"^->":"back_in_london.0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"There is not a moment to lose!\"",{"->":"$r","var":true},null]}],["ev",{"^->":"back_in_london.0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^\"Monsieur, let us savour this moment!\"",{"->":"$r","var":true},null]}],"ev","str","^We hurried home","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["ev",{"^->":"back_in_london.0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"^ I declared.","\n",{"->":"hurry_outside"},{"#f":5}],"c-1":["ev",{"^->":"back_in_london.0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"^ I declared.","\n","^My master clouted me firmly around the head and dragged me out of the door.","\n",{"->":"dragged_outside"},{"#f":5}],"c-2":["^ ",{"->":"hurry_outside"},"\n",{"#f":5}]}],null],"hurry_outside":["^We hurried home to Savile Row ",{"->":"as_fast_as_we_could"},"\n",null],"dragged_outside":["^He insisted that we hurried home to Savile Row ",{"->":"as_fast_as_we_could"},"\n",null],"as_fast_as_we_could":["<>","^as fast as we could. ","end","\n",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[{"->":"back_in_london"},["done",{"#n":"g-0"}],null],"done",{"back_in_london":[["^We arrived into London at 9.45pm exactly.","\n",["ev",{"^->":"back_in_london.0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"There is not a moment to lose!\"",{"->":"$r","var":true},null]}],["ev",{"^->":"back_in_london.0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^\"Monsieur, let us savour this moment!\"",{"->":"$r","var":true},null]}],"ev","str","^We hurried home","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["ev",{"^->":"back_in_london.0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"^ I declared.","\n",{"->":"hurry_outside"},{"#f":5}],"c-1":["ev",{"^->":"back_in_london.0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"^ I declared.","\n","^My master clouted me firmly around the head and dragged me out of the door.","\n",{"->":"dragged_outside"},{"#f":5}],"c-2":["^ ",{"->":"hurry_outside"},"\n",{"#f":5}]}],null],"hurry_outside":["^We hurried home to Savile Row ",{"->":"as_fast_as_we_could"},"\n",null],"dragged_outside":["^He insisted that we hurried home to Savile Row ",{"->":"as_fast_as_we_could"},"\n",null],"as_fast_as_we_could":["<>","^as fast as we could. ","end","\n",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/divert/divert-on-choice.ink.json b/src/test/resources/inkfiles/divert/divert-on-choice.ink.json index d0d58f0..b0386c1 100644 --- a/src/test/resources/inkfiles/divert/divert-on-choice.ink.json +++ b/src/test/resources/inkfiles/divert/divert-on-choice.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[{"->":"paragraph_1"},["done",{"#n":"g-0"}],null],"done",{"paragraph_1":[["^You stand by the wall of Analand, sword in hand.","\n","ev","str","^Open the gate","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["^ ",{"->":"paragraph_2"},"\n",{"#f":5}]}],null],"paragraph_2":["^You open the gate, and step out onto the path. ","end","\n",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[{"->":"paragraph_1"},["done",{"#n":"g-0"}],null],"done",{"paragraph_1":[["^You stand by the wall of Analand, sword in hand.","\n","ev","str","^Open the gate","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["^ ",{"->":"paragraph_2"},"\n",{"#f":5}]}],null],"paragraph_2":["^You open the gate, and step out onto the path. ","end","\n",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/divert/invisible-divert.ink.json b/src/test/resources/inkfiles/divert/invisible-divert.ink.json index 0f5eba9..2164d63 100644 --- a/src/test/resources/inkfiles/divert/invisible-divert.ink.json +++ b/src/test/resources/inkfiles/divert/invisible-divert.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^We hurried home to Savile Row ",{"->":"as_fast_as_we_could"},"\n",["done",{"#n":"g-0"}],null],"done",{"as_fast_as_we_could":["^as fast as we could. ","end","\n",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^We hurried home to Savile Row ",{"->":"as_fast_as_we_could"},"\n",["done",{"#n":"g-0"}],null],"done",{"as_fast_as_we_could":["^as fast as we could. ","end","\n",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/divert/simple-divert.ink.json b/src/test/resources/inkfiles/divert/simple-divert.ink.json index b27160d..137863e 100644 --- a/src/test/resources/inkfiles/divert/simple-divert.ink.json +++ b/src/test/resources/inkfiles/divert/simple-divert.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^We arrived into London at 9.45pm exactly.","\n",{"->":"hurry_home"},["done",{"#n":"g-0"}],null],"done",{"hurry_home":["^We hurried home to Savile Row as fast as we could. ","end","\n",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^We arrived into London at 9.45pm exactly.","\n",{"->":"hurry_home"},["done",{"#n":"g-0"}],null],"done",{"hurry_home":["^We hurried home to Savile Row as fast as we could. ","end","\n",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/function/complex-func1.ink.json b/src/test/resources/inkfiles/function/complex-func1.ink.json index 65cf72c..30de178 100644 --- a/src/test/resources/inkfiles/function/complex-func1.ink.json +++ b/src/test/resources/inkfiles/function/complex-func1.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["ev",2,3,4,{"f()":"derp"},"pop","/ev","\n","^The values are ","ev",{"VAR?":"x"},"out","/ev","^ and ","ev",{"VAR?":"y"},"out","/ev","^.","\n","end",["done",{"#n":"g-0"}],null],"done",{"derp":[{"temp=":"c"},{"temp=":"b"},{"temp=":"a"},"ev",{"VAR?":"a"},{"VAR?":"b"},"+","/ev",{"VAR=":"x","re":true},"ev",{"VAR?":"x"},5,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","ev",6,"/ev",{"VAR=":"x","re":true},{"->":"derp.15"},null]}],"nop","\n","ev",{"VAR?":"x"},{"VAR?":"c"},"+","/ev",{"VAR=":"y","re":true},null],"global decl":["ev",0,{"VAR=":"x"},3,{"VAR=":"y"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["ev",2,3,4,{"f()":"derp"},"pop","/ev","\n","^The values are ","ev",{"VAR?":"x"},"out","/ev","^ and ","ev",{"VAR?":"y"},"out","/ev","^.","\n","end",["done",{"#n":"g-0"}],null],"done",{"derp":[{"temp=":"c"},{"temp=":"b"},{"temp=":"a"},"ev",{"VAR?":"a"},{"VAR?":"b"},"+","/ev",{"VAR=":"x","re":true},"ev",{"VAR?":"x"},5,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","ev",6,"/ev",{"VAR=":"x","re":true},{"->":"derp.15"},null]}],"nop","\n","ev",{"VAR?":"x"},{"VAR?":"c"},"+","/ev",{"VAR=":"y","re":true},null],"global decl":["ev",0,{"VAR=":"x"},3,{"VAR=":"y"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/function/complex-func2.ink.json b/src/test/resources/inkfiles/function/complex-func2.ink.json index eedcc92..9a39f21 100644 --- a/src/test/resources/inkfiles/function/complex-func2.ink.json +++ b/src/test/resources/inkfiles/function/complex-func2.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["ev",2,3,{"f()":"derp"},"pop","/ev","\n","^The values are ","ev",{"VAR?":"x"},"out","/ev","^ and ","ev",{"VAR?":"y"},"out","/ev","^ and ","ev",{"VAR?":"z"},"out","/ev","^.","\n","end",["done",{"#n":"g-0"}],null],"done",{"derp":[{"temp=":"b"},{"temp=":"a"},"ev",{"VAR?":"a"},{"VAR?":"b"},"-","/ev",{"VAR=":"x","re":true},["ev",{"VAR?":"x"},0,"==","/ev",{"->":".^.b","c":true},{"b":["\n","ev",0,"/ev",{"VAR=":"y","re":true},{"->":"derp.11"},null]}],["ev",{"VAR?":"x"},0,">","/ev",{"->":".^.b","c":true},{"b":["\n","ev",{"VAR?":"x"},1,"-","/ev",{"VAR=":"y","re":true},{"->":"derp.11"},null]}],[{"->":".^.b"},{"b":["\n","ev",{"VAR?":"x"},1,"+","/ev",{"VAR=":"y","re":true},{"->":"derp.11"},null]}],"nop","\n",null],"global decl":["ev",0,{"VAR=":"x"},3,{"VAR=":"y"},1,{"VAR=":"z"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["ev",2,3,{"f()":"derp"},"pop","/ev","\n","^The values are ","ev",{"VAR?":"x"},"out","/ev","^ and ","ev",{"VAR?":"y"},"out","/ev","^ and ","ev",{"VAR?":"z"},"out","/ev","^.","\n","end",["done",{"#n":"g-0"}],null],"done",{"derp":[{"temp=":"b"},{"temp=":"a"},"ev",{"VAR?":"a"},{"VAR?":"b"},"-","/ev",{"VAR=":"x","re":true},["ev",{"VAR?":"x"},0,"==","/ev",{"->":".^.b","c":true},{"b":["\n","ev",0,"/ev",{"VAR=":"y","re":true},{"->":"derp.11"},null]}],["ev",{"VAR?":"x"},0,">","/ev",{"->":".^.b","c":true},{"b":["\n","ev",{"VAR?":"x"},1,"-","/ev",{"VAR=":"y","re":true},{"->":"derp.11"},null]}],[{"->":".^.b"},{"b":["\n","ev",{"VAR?":"x"},1,"+","/ev",{"VAR=":"y","re":true},{"->":"derp.11"},null]}],"nop","\n",null],"global decl":["ev",0,{"VAR=":"x"},3,{"VAR=":"y"},1,{"VAR=":"z"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/function/complex-func3.ink.json b/src/test/resources/inkfiles/function/complex-func3.ink.json index 0d66173..85ded53 100644 --- a/src/test/resources/inkfiles/function/complex-func3.ink.json +++ b/src/test/resources/inkfiles/function/complex-func3.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["ev",{"f()":"merchant_init"},"pop","/ev","\n","^\"I will pay you ","ev",{"VAR?":"fee"},"out","/ev","^ reales if you get the goods to their destination. The goods will take up ","ev",{"VAR?":"weight"},"out","/ev","^ cargo spaces.\"","\n","end",["done",{"#n":"g-0"}],null],"done",{"merchant_init":["ev",{"VAR?":"roll"},0,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","ev",2,"/ev",{"VAR=":"mult","re":true},{"->":".^.^.^.6"},null]}],"nop","\n","ev",{"VAR?":"mult"},2,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","ev",1,"/ev",{"VAR=":"roll","re":true},{"->":".^.^.^.14"},null]}],"nop","\n","ev",{"VAR?":"roll"},0,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","ev",3,"/ev",{"VAR=":"mult","re":true},{"->":".^.^.^.22"},null]}],"nop","\n","ev",{"VAR?":"dst"},100,"*",100,"/","/ev",{"VAR=":"deadline","re":true},"ev",1,{"VAR?":"dst"},"+",10,"*",{"VAR?":"mult"},"*","/ev",{"VAR=":"fee","re":true},null],"global decl":["ev",20,{"VAR=":"weight"},0,{"VAR=":"roll"},1,{"VAR=":"mult"},5,{"VAR=":"dst"},0,{"VAR=":"deadline"},0,{"VAR=":"fee"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["ev",{"f()":"merchant_init"},"pop","/ev","\n","^\"I will pay you ","ev",{"VAR?":"fee"},"out","/ev","^ reales if you get the goods to their destination. The goods will take up ","ev",{"VAR?":"weight"},"out","/ev","^ cargo spaces.\"","\n","end",["done",{"#n":"g-0"}],null],"done",{"merchant_init":["ev",{"VAR?":"roll"},0,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","ev",2,"/ev",{"VAR=":"mult","re":true},{"->":".^.^.^.6"},null]}],"nop","\n","ev",{"VAR?":"mult"},2,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","ev",1,"/ev",{"VAR=":"roll","re":true},{"->":".^.^.^.14"},null]}],"nop","\n","ev",{"VAR?":"roll"},0,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","ev",3,"/ev",{"VAR=":"mult","re":true},{"->":".^.^.^.22"},null]}],"nop","\n","ev",{"VAR?":"dst"},100,"*",100,"/","/ev",{"VAR=":"deadline","re":true},"ev",1,{"VAR?":"dst"},"+",10,"*",{"VAR?":"mult"},"*","/ev",{"VAR=":"fee","re":true},null],"global decl":["ev",20,{"VAR=":"weight"},0,{"VAR=":"roll"},1,{"VAR=":"mult"},5,{"VAR=":"dst"},0,{"VAR=":"deadline"},0,{"VAR=":"fee"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/function/evaluating-function-variablestate-bug.ink.json b/src/test/resources/inkfiles/function/evaluating-function-variablestate-bug.ink.json index 10e456a..97debc1 100644 --- a/src/test/resources/inkfiles/function/evaluating-function-variablestate-bug.ink.json +++ b/src/test/resources/inkfiles/function/evaluating-function-variablestate-bug.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^Start","\n",{"->t->":"tunnel"},"^End","\n","end",["done",{"#n":"g-0"}],null],"done",{"tunnel":["^In tunnel.","\n","ev","void","/ev","->->",null],"function_to_evaluate":["ev",1,{"f()":"zero_equals_"},"/ev",[{"->":".^.b","c":true},{"b":["\n","ev","str","^WRONG","/str","/ev","~ret",{"->":".^.^.^.6"},null]}],[{"->":".^.b"},{"b":["\n","ev","str","^RIGHT","/str","/ev","~ret",{"->":".^.^.^.6"},null]}],"nop","\n",null],"zero_equals_":[{"temp=":"k"},"ev",0,{"f()":"do_nothing"},"pop","/ev","\n","ev",0,{"VAR?":"k"},"==","/ev","~ret",null],"do_nothing":[{"temp=":"k"},"ev",0,"/ev","~ret",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^Start","\n",{"->t->":"tunnel"},"^End","\n","end",["done",{"#n":"g-0"}],null],"done",{"tunnel":["^In tunnel.","\n","ev","void","/ev","->->",null],"function_to_evaluate":["ev",1,{"f()":"zero_equals_"},"/ev",[{"->":".^.b","c":true},{"b":["\n","ev","str","^WRONG","/str","/ev","~ret",{"->":".^.^.^.6"},null]}],[{"->":".^.b"},{"b":["\n","ev","str","^RIGHT","/str","/ev","~ret",{"->":".^.^.^.6"},null]}],"nop","\n",null],"zero_equals_":[{"temp=":"k"},"ev",0,{"f()":"do_nothing"},"pop","/ev","\n","ev",0,{"VAR?":"k"},"==","/ev","~ret",null],"do_nothing":[{"temp=":"k"},"ev",0,"/ev","~ret",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/function/func-basic.ink.json b/src/test/resources/inkfiles/function/func-basic.ink.json index c906889..e0f6ed4 100644 --- a/src/test/resources/inkfiles/function/func-basic.ink.json +++ b/src/test/resources/inkfiles/function/func-basic.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["ev",2,8,0.4,{"f()":"lerp"},"/ev",{"VAR=":"x","re":true},"\n","^The value of x is ","ev",{"VAR?":"x"},"out","/ev","^.","\n","end",["done",{"#n":"g-0"}],null],"done",{"lerp":[{"temp=":"k"},{"temp=":"b"},{"temp=":"a"},"ev",{"VAR?":"b"},{"VAR?":"a"},"-",{"VAR?":"k"},"*",{"VAR?":"a"},"+","/ev","~ret",null],"global decl":["ev",0.0,{"VAR=":"x"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["ev",2,8,0.4,{"f()":"lerp"},"/ev",{"VAR=":"x","re":true},"\n","^The value of x is ","ev",{"VAR?":"x"},"out","/ev","^.","\n","end",["done",{"#n":"g-0"}],null],"done",{"lerp":[{"temp=":"k"},{"temp=":"b"},{"temp=":"a"},"ev",{"VAR?":"b"},{"VAR?":"a"},"-",{"VAR?":"k"},"*",{"VAR?":"a"},"+","/ev","~ret",null],"global decl":["ev",0.0,{"VAR=":"x"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/function/func-inline.ink.json b/src/test/resources/inkfiles/function/func-inline.ink.json index b0cb840..1b64dd4 100644 --- a/src/test/resources/inkfiles/function/func-inline.ink.json +++ b/src/test/resources/inkfiles/function/func-inline.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^The value of x is ","ev",2,8,0.4,{"f()":"lerp"},"out","/ev","^.","\n","end",["done",{"#n":"g-0"}],null],"done",{"lerp":[{"temp=":"k"},{"temp=":"b"},{"temp=":"a"},"ev",{"VAR?":"b"},{"VAR?":"a"},"-",{"VAR?":"k"},"*",{"VAR?":"a"},"+","/ev","~ret",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^The value of x is ","ev",2,8,0.4,{"f()":"lerp"},"out","/ev","^.","\n","end",["done",{"#n":"g-0"}],null],"done",{"lerp":[{"temp=":"k"},{"temp=":"b"},{"temp=":"a"},"ev",{"VAR?":"b"},{"VAR?":"a"},"-",{"VAR?":"k"},"*",{"VAR?":"a"},"+","/ev","~ret",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/function/func-none.ink.json b/src/test/resources/inkfiles/function/func-none.ink.json index 049d909..99d4160 100644 --- a/src/test/resources/inkfiles/function/func-none.ink.json +++ b/src/test/resources/inkfiles/function/func-none.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["ev",{"f()":"f"},"/ev",{"VAR=":"x","re":true},"\n","^The value of x is ","ev",{"VAR?":"x"},"out","/ev","^.","\n","end",["done",{"#n":"g-0"}],null],"done",{"f":["ev",3.8,"/ev","~ret",null],"global decl":["ev",0.0,{"VAR=":"x"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["ev",{"f()":"f"},"/ev",{"VAR=":"x","re":true},"\n","^The value of x is ","ev",{"VAR?":"x"},"out","/ev","^.","\n","end",["done",{"#n":"g-0"}],null],"done",{"f":["ev",3.8,"/ev","~ret",null],"global decl":["ev",0.0,{"VAR=":"x"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/function/rnd-func.ink.json b/src/test/resources/inkfiles/function/rnd-func.ink.json index 78c1ae8..8b444c5 100644 --- a/src/test/resources/inkfiles/function/rnd-func.ink.json +++ b/src/test/resources/inkfiles/function/rnd-func.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["ev",10,"srnd","pop","/ev","\n","^Rolling dice 1: ","ev",1,6,"rnd","out","/ev","^.","\n","^Rolling dice 2: ","ev",1,6,"rnd","out","/ev","^.","\n","^Rolling dice 3: ","ev",1,6,"rnd","out","/ev","^.","\n","^Rolling dice 4: ","ev",1,6,"rnd","out","/ev","^.","\n",["done",{"#n":"g-0"}],null],"done",null],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["ev",10,"srnd","pop","/ev","\n","^Rolling dice 1: ","ev",1,6,"rnd","out","/ev","^.","\n","^Rolling dice 2: ","ev",1,6,"rnd","out","/ev","^.","\n","^Rolling dice 3: ","ev",1,6,"rnd","out","/ev","^.","\n","^Rolling dice 4: ","ev",1,6,"rnd","out","/ev","^.","\n",["done",{"#n":"g-0"}],null],"done",null],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/function/setvar-func.ink.json b/src/test/resources/inkfiles/function/setvar-func.ink.json index 3101bb8..f8426a4 100644 --- a/src/test/resources/inkfiles/function/setvar-func.ink.json +++ b/src/test/resources/inkfiles/function/setvar-func.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["ev",2,3,{"f()":"herp"},"pop","/ev","\n","^The value is ","ev",{"VAR?":"x"},"out","/ev","^.","\n","end",["done",{"#n":"g-0"}],null],"done",{"herp":[{"temp=":"b"},{"temp=":"a"},"ev",{"VAR?":"a"},{"VAR?":"b"},"*","/ev",{"VAR=":"x","re":true},null],"global decl":["ev",0.0,{"VAR=":"x"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["ev",2,3,{"f()":"herp"},"pop","/ev","\n","^The value is ","ev",{"VAR?":"x"},"out","/ev","^.","\n","end",["done",{"#n":"g-0"}],null],"done",{"herp":[{"temp=":"b"},{"temp=":"a"},"ev",{"VAR?":"a"},{"VAR?":"b"},"*","/ev",{"VAR=":"x","re":true},null],"global decl":["ev",0.0,{"VAR=":"x"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/function/test-error.ink.json b/src/test/resources/inkfiles/function/test-error.ink.json index 94bdec9..d271f64 100644 --- a/src/test/resources/inkfiles/function/test-error.ink.json +++ b/src/test/resources/inkfiles/function/test-error.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["ev",{"VAR?":"roll"},0,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","ev",2,"/ev",{"VAR=":"roll","re":true},{"->":"0.6"},null]}],"nop","\n","ev",{"VAR?":"roll"},0,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","ev",1,"/ev",{"VAR=":"roll","re":true},{"->":"0.14"},null]}],"nop","\n",["done",{"#n":"g-0"}],null],"done",{"global decl":["ev",0,{"VAR=":"roll"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["ev",{"VAR?":"roll"},0,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","ev",2,"/ev",{"VAR=":"roll","re":true},{"->":"0.6"},null]}],"nop","\n","ev",{"VAR?":"roll"},0,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","ev",1,"/ev",{"VAR=":"roll","re":true},{"->":"0.14"},null]}],"nop","\n",["done",{"#n":"g-0"}],null],"done",{"global decl":["ev",0,{"VAR=":"roll"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/gather/complex-flow.ink.json b/src/test/resources/inkfiles/gather/complex-flow.ink.json index 7fbf303..f90c262 100644 --- a/src/test/resources/inkfiles/gather/complex-flow.ink.json +++ b/src/test/resources/inkfiles/gather/complex-flow.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^I looked at Monsieur Fogg","\n",["ev",{"^->":"0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-0","flg":18},{"s":["^... and I could contain myself no longer.",{"->":"$r","var":true},null]}],["ev",{"^->":"0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-1","flg":18},{"s":["^... but I said nothing",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":"0.2.s"},[{"#n":"$r2"}],"\n","^'What is the purpose of our journey, Monsieur?'","\n","^'A wager,' he replied.","\n",[["ev",{"^->":"0.c-0.11.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^'A wager!'",{"->":"$r","var":true},null]}],["ev",{"^->":"0.c-0.11.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.'","/str","/ev",{"*":".^.^.c-1","flg":22},{"s":["^'Ah",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.11.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"^ I returned.","\n","^He nodded.","\n",[["ev",{"^->":"0.c-0.11.c-0.10.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^'But surely that is foolishness!'",{"->":"$r","var":true},null]}],["ev",{"^->":"0.c-0.11.c-0.10.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^'A most serious matter then!'",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.11.c-0.10.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["ev",{"^->":"0.c-0.11.c-0.10.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^He nodded again.","\n",["ev",{"^->":"0.c-0.11.c-0.10.g-0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-2","flg":18},{"s":["^'But can we win?'",{"->":"$r","var":true},null]}],["ev",{"^->":"0.c-0.11.c-0.10.g-0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-3","flg":18},{"s":["^'A modest wager, I trust?'",{"->":"$r","var":true},null]}],["ev",{"^->":"0.c-0.11.c-0.10.g-0.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.","/str","/ev",{"*":".^.^.c-4","flg":22},{"s":["^I asked nothing further of him then",{"->":"$r","var":true},null]}],{"c-2":["ev",{"^->":"0.c-0.11.c-0.10.g-0.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n","^'That is what we will endeavour to find out,' he answered.","\n",{"->":"0.c-0.11.g-0"},{"#f":5}],"c-3":["ev",{"^->":"0.c-0.11.c-0.10.g-0.c-3.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"\n","^'Twenty thousand pounds,' he replied, quite flatly.","\n",{"->":"0.c-0.11.g-0"},{"#f":5}],"c-4":["ev",{"^->":"0.c-0.11.c-0.10.g-0.c-4.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.4.s"},[{"#n":"$r2"}],"^, and after a final, polite cough, he offered nothing more to me. ","<>","\n",{"->":"0.c-0.11.g-0"},{"#f":5}]}]}],{"#f":5}],"c-1":["ev",{"^->":"0.c-0.11.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"^,' I replied, uncertain what I thought.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^After that, ","<>","\n",{"->":"0.g-0"},null]}],{"#f":5}],"c-1":["ev",{"^->":"0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":"0.3.s"},[{"#n":"$r2"}],"^ and ","<>","\n",{"->":"0.g-0"},{"#f":5}],"g-0":["^we passed the day in silence.","\n",["end",["done",{"#n":"g-2"}],{"#n":"g-1"}],null]}],"done",null],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^I looked at Monsieur Fogg","\n",["ev",{"^->":"0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-0","flg":18},{"s":["^... and I could contain myself no longer.",{"->":"$r","var":true},null]}],["ev",{"^->":"0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-1","flg":18},{"s":["^... but I said nothing",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":"0.2.s"},[{"#n":"$r2"}],"\n","^'What is the purpose of our journey, Monsieur?'","\n","^'A wager,' he replied.","\n",[["ev",{"^->":"0.c-0.11.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^'A wager!'",{"->":"$r","var":true},null]}],["ev",{"^->":"0.c-0.11.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.'","/str","/ev",{"*":".^.^.c-1","flg":22},{"s":["^'Ah",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.11.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"^ I returned.","\n","^He nodded.","\n",[["ev",{"^->":"0.c-0.11.c-0.10.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^'But surely that is foolishness!'",{"->":"$r","var":true},null]}],["ev",{"^->":"0.c-0.11.c-0.10.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^'A most serious matter then!'",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.11.c-0.10.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["ev",{"^->":"0.c-0.11.c-0.10.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^He nodded again.","\n",["ev",{"^->":"0.c-0.11.c-0.10.g-0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-2","flg":18},{"s":["^'But can we win?'",{"->":"$r","var":true},null]}],["ev",{"^->":"0.c-0.11.c-0.10.g-0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-3","flg":18},{"s":["^'A modest wager, I trust?'",{"->":"$r","var":true},null]}],["ev",{"^->":"0.c-0.11.c-0.10.g-0.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.","/str","/ev",{"*":".^.^.c-4","flg":22},{"s":["^I asked nothing further of him then",{"->":"$r","var":true},null]}],{"c-2":["ev",{"^->":"0.c-0.11.c-0.10.g-0.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n","^'That is what we will endeavour to find out,' he answered.","\n",{"->":"0.c-0.11.g-0"},{"#f":5}],"c-3":["ev",{"^->":"0.c-0.11.c-0.10.g-0.c-3.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"\n","^'Twenty thousand pounds,' he replied, quite flatly.","\n",{"->":"0.c-0.11.g-0"},{"#f":5}],"c-4":["ev",{"^->":"0.c-0.11.c-0.10.g-0.c-4.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.4.s"},[{"#n":"$r2"}],"^, and after a final, polite cough, he offered nothing more to me. ","<>","\n",{"->":"0.c-0.11.g-0"},{"#f":5}]}]}],{"#f":5}],"c-1":["ev",{"^->":"0.c-0.11.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"^,' I replied, uncertain what I thought.","\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^After that, ","<>","\n",{"->":"0.g-0"},null]}],{"#f":5}],"c-1":["ev",{"^->":"0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":"0.3.s"},[{"#n":"$r2"}],"^ and ","<>","\n",{"->":"0.g-0"},{"#f":5}],"g-0":["^we passed the day in silence.","\n",["end",["done",{"#n":"g-2"}],{"#n":"g-1"}],null]}],"done",null],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/gather/deep-nesting.ink.json b/src/test/resources/inkfiles/gather/deep-nesting.ink.json index 3c6c739..9b63ecd 100644 --- a/src/test/resources/inkfiles/gather/deep-nesting.ink.json +++ b/src/test/resources/inkfiles/gather/deep-nesting.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^Tell us a tale, Captain!\"","\n",["ev",{"^->":"0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-0","flg":18},{"s":["^\"Very well, you sea-dogs. Here's a tale...\"",{"->":"$r","var":true},null]}],["ev",{"^->":"0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-1","flg":18},{"s":["^\"No, it's past your bed-time.\"",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":"0.2.s"},[{"#n":"$r2"}],"\n",[["ev",{"^->":"0.c-0.7.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"It was a dark and stormy night...\"",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.7.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n",[["ev",{"^->":"0.c-0.7.c-0.7.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"...and the crew were restless...\"",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.7.c-0.7.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n",[["ev",{"^->":"0.c-0.7.c-0.7.c-0.7.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"... and they said to their Captain...\"",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.7.c-0.7.c-0.7.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n",[["ev",{"^->":"0.c-0.7.c-0.7.c-0.7.c-0.7.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"...Tell us a tale Captain!\"",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.7.c-0.7.c-0.7.c-0.7.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}]}],{"#f":5}]}],{"#f":5}]}],{"#f":5}]}],{"#f":5}],"c-1":["ev",{"^->":"0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":"0.3.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}],"g-0":["^To a man, the crew began to yawn.","\n","end",["done",{"#n":"g-1"}],null]}],"done",null],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^Tell us a tale, Captain!\"","\n",["ev",{"^->":"0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-0","flg":18},{"s":["^\"Very well, you sea-dogs. Here's a tale...\"",{"->":"$r","var":true},null]}],["ev",{"^->":"0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-1","flg":18},{"s":["^\"No, it's past your bed-time.\"",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":"0.2.s"},[{"#n":"$r2"}],"\n",[["ev",{"^->":"0.c-0.7.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"It was a dark and stormy night...\"",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.7.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n",[["ev",{"^->":"0.c-0.7.c-0.7.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"...and the crew were restless...\"",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.7.c-0.7.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n",[["ev",{"^->":"0.c-0.7.c-0.7.c-0.7.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"... and they said to their Captain...\"",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.7.c-0.7.c-0.7.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n",[["ev",{"^->":"0.c-0.7.c-0.7.c-0.7.c-0.7.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"...Tell us a tale Captain!\"",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.7.c-0.7.c-0.7.c-0.7.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}]}],{"#f":5}]}],{"#f":5}]}],{"#f":5}]}],{"#f":5}],"c-1":["ev",{"^->":"0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":"0.3.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}],"g-0":["^To a man, the crew began to yawn.","\n","end",["done",{"#n":"g-1"}],null]}],"done",null],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/gather/gather-basic.ink.json b/src/test/resources/inkfiles/gather/gather-basic.ink.json index 0426e6f..d4401c6 100644 --- a/src/test/resources/inkfiles/gather/gather-basic.ink.json +++ b/src/test/resources/inkfiles/gather/gather-basic.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^What's that?\" my master asked.","\n",["ev",{"^->":"0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.\"","/str","/ev",{"*":"0.c-0","flg":22},{"s":["^\"I am somewhat tired",{"->":"$r","var":true},null]}],["ev",{"^->":"0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-1","flg":18},{"s":["^\"Nothing, Monsieur!\"",{"->":"$r","var":true},null]}],["ev",{"^->":"0.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.\"","/str","/ev",{"*":"0.c-2","flg":22},{"s":["^\"I said, this journey is appalling",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":"0.2.s"},[{"#n":"$r2"}],"^,\" I repeated.","\n","^\"Really,\" he responded. \"How deleterious.\"","\n",{"->":"0.g-0"},{"#f":5}],"c-1":["ev",{"^->":"0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":"0.3.s"},[{"#n":"$r2"}],"^ I replied.","\n","^\"Very good, then.\"","\n",{"->":"0.g-0"},{"#f":5}],"c-2":["ev",{"^->":"0.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":"0.4.s"},[{"#n":"$r2"}],"^ and I want no more of it.\"","\n","^\"Ah,\" he replied, not unkindly. \"I see you are feeling frustrated. Tomorrow, things will improve.\"","\n",{"->":"0.g-0"},{"#f":5}],"g-0":["^With that Monsieur Fogg left the room.","\n","end",["done",{"#n":"g-1"}],null]}],"done",null],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^What's that?\" my master asked.","\n",["ev",{"^->":"0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.\"","/str","/ev",{"*":"0.c-0","flg":22},{"s":["^\"I am somewhat tired",{"->":"$r","var":true},null]}],["ev",{"^->":"0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-1","flg":18},{"s":["^\"Nothing, Monsieur!\"",{"->":"$r","var":true},null]}],["ev",{"^->":"0.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^.\"","/str","/ev",{"*":"0.c-2","flg":22},{"s":["^\"I said, this journey is appalling",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":"0.2.s"},[{"#n":"$r2"}],"^,\" I repeated.","\n","^\"Really,\" he responded. \"How deleterious.\"","\n",{"->":"0.g-0"},{"#f":5}],"c-1":["ev",{"^->":"0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":"0.3.s"},[{"#n":"$r2"}],"^ I replied.","\n","^\"Very good, then.\"","\n",{"->":"0.g-0"},{"#f":5}],"c-2":["ev",{"^->":"0.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":"0.4.s"},[{"#n":"$r2"}],"^ and I want no more of it.\"","\n","^\"Ah,\" he replied, not unkindly. \"I see you are feeling frustrated. Tomorrow, things will improve.\"","\n",{"->":"0.g-0"},{"#f":5}],"g-0":["^With that Monsieur Fogg left the room.","\n","end",["done",{"#n":"g-1"}],null]}],"done",null],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/gather/gather-chain.ink.json b/src/test/resources/inkfiles/gather/gather-chain.ink.json index cf78d3f..51a5184 100644 --- a/src/test/resources/inkfiles/gather/gather-chain.ink.json +++ b/src/test/resources/inkfiles/gather/gather-chain.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^I ran through the forest, the dogs snapping at my heels.","\n",["ev",{"^->":"0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-0","flg":18},{"s":["^I checked the jewels",{"->":"$r","var":true},null]}],["ev",{"^->":"0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-1","flg":18},{"s":["^I did not pause for breath",{"->":"$r","var":true},null]}],["ev",{"^->":"0.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-2","flg":18},{"s":["^I cheered with joy. ","<>",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":"0.2.s"},[{"#n":"$r2"}],"^ were still in my pocket, and the feel of them brought a spring to my step. ","<>","\n",{"->":"0.g-0"},{"#f":5}],"c-1":["ev",{"^->":"0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":"0.3.s"},[{"#n":"$r2"}],"^ but kept on running. ","<>","\n",{"->":"0.g-0"},{"#f":5}],"c-2":["ev",{"^->":"0.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":"0.4.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}],"g-0":["^The road could not be much further! Mackie would have the engine running, and then I'd be safe.","\n",["ev",{"^->":"0.g-0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-3","flg":18},{"s":["^I reached the road and looked about",{"->":"$r","var":true},null]}],["ev",{"^->":"0.g-0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-4","flg":18},{"s":["^I should interrupt to say Mackie is normally very reliable",{"->":"$r","var":true},null]}],{"c-3":["ev",{"^->":"0.g-0.c-3.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"^. And would you believe it?","\n",{"->":"0.g-1"},{"#f":5}],"c-4":["ev",{"^->":"0.g-0.c-4.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"^. He's never once let me down. Or rather, never once, previously to that night.","\n",{"->":"0.g-1"},{"#f":5}]}],"g-1":["^The road was empty. Mackie was nowhere to be seen.","\n","end",["done",{"#n":"g-2"}],null]}],"done",null],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^I ran through the forest, the dogs snapping at my heels.","\n",["ev",{"^->":"0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-0","flg":18},{"s":["^I checked the jewels",{"->":"$r","var":true},null]}],["ev",{"^->":"0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-1","flg":18},{"s":["^I did not pause for breath",{"->":"$r","var":true},null]}],["ev",{"^->":"0.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-2","flg":18},{"s":["^I cheered with joy. ","<>",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":"0.2.s"},[{"#n":"$r2"}],"^ were still in my pocket, and the feel of them brought a spring to my step. ","<>","\n",{"->":"0.g-0"},{"#f":5}],"c-1":["ev",{"^->":"0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":"0.3.s"},[{"#n":"$r2"}],"^ but kept on running. ","<>","\n",{"->":"0.g-0"},{"#f":5}],"c-2":["ev",{"^->":"0.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":"0.4.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}],"g-0":["^The road could not be much further! Mackie would have the engine running, and then I'd be safe.","\n",["ev",{"^->":"0.g-0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-3","flg":18},{"s":["^I reached the road and looked about",{"->":"$r","var":true},null]}],["ev",{"^->":"0.g-0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-4","flg":18},{"s":["^I should interrupt to say Mackie is normally very reliable",{"->":"$r","var":true},null]}],{"c-3":["ev",{"^->":"0.g-0.c-3.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"^. And would you believe it?","\n",{"->":"0.g-1"},{"#f":5}],"c-4":["ev",{"^->":"0.g-0.c-4.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"^. He's never once let me down. Or rather, never once, previously to that night.","\n",{"->":"0.g-1"},{"#f":5}]}],"g-1":["^The road was empty. Mackie was nowhere to be seen.","\n","end",["done",{"#n":"g-2"}],null]}],"done",null],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/gather/nested-flow.ink.json b/src/test/resources/inkfiles/gather/nested-flow.ink.json index 1b8ce0b..0851a1d 100644 --- a/src/test/resources/inkfiles/gather/nested-flow.ink.json +++ b/src/test/resources/inkfiles/gather/nested-flow.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^Well, Poirot? Murder or suicide?\"","\n",["ev",{"^->":"0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-0","flg":18},{"s":["^\"Murder!\"",{"->":"$r","var":true},null]}],["ev",{"^->":"0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-1","flg":18},{"s":["^\"Suicide!\"",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":"0.2.s"},[{"#n":"$r2"}],"\n","^\"And who did it?\"","\n",[["ev",{"^->":"0.c-0.9.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"Detective-Inspector Japp!\"",{"->":"$r","var":true},null]}],["ev",{"^->":"0.c-0.9.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^\"Captain Hastings!\"",{"->":"$r","var":true},null]}],["ev",{"^->":"0.c-0.9.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-2","flg":18},{"s":["^\"Myself!\"",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.9.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}],"c-1":["ev",{"^->":"0.c-0.9.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}],"c-2":["ev",{"^->":"0.c-0.9.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}]}],{"#f":5}],"c-1":["ev",{"^->":"0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":"0.3.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}],"g-0":["^Mrs. Christie lowered her manuscript a moment. The rest of the writing group sat, open-mouthed.","\n","end",["done",{"#n":"g-1"}],null]}],"done",null],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^Well, Poirot? Murder or suicide?\"","\n",["ev",{"^->":"0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-0","flg":18},{"s":["^\"Murder!\"",{"->":"$r","var":true},null]}],["ev",{"^->":"0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-1","flg":18},{"s":["^\"Suicide!\"",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":"0.2.s"},[{"#n":"$r2"}],"\n","^\"And who did it?\"","\n",[["ev",{"^->":"0.c-0.9.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"Detective-Inspector Japp!\"",{"->":"$r","var":true},null]}],["ev",{"^->":"0.c-0.9.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^\"Captain Hastings!\"",{"->":"$r","var":true},null]}],["ev",{"^->":"0.c-0.9.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-2","flg":18},{"s":["^\"Myself!\"",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.9.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}],"c-1":["ev",{"^->":"0.c-0.9.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}],"c-2":["ev",{"^->":"0.c-0.9.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}]}],{"#f":5}],"c-1":["ev",{"^->":"0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":"0.3.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}],"g-0":["^Mrs. Christie lowered her manuscript a moment. The rest of the writing group sat, open-mouthed.","\n","end",["done",{"#n":"g-1"}],null]}],"done",null],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/gather/nested-gather.ink.json b/src/test/resources/inkfiles/gather/nested-gather.ink.json index f59cf39..dc7d8d0 100644 --- a/src/test/resources/inkfiles/gather/nested-gather.ink.json +++ b/src/test/resources/inkfiles/gather/nested-gather.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^Well, Poirot? Murder or suicide?\"","\n",["ev",{"^->":"0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-0","flg":18},{"s":["^\"Murder!\"",{"->":"$r","var":true},null]}],["ev",{"^->":"0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-1","flg":18},{"s":["^\"Suicide!\"",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":"0.2.s"},[{"#n":"$r2"}],"\n","^\"And who did it?\"","\n",[["ev",{"^->":"0.c-0.9.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"Detective-Inspector Japp!\"",{"->":"$r","var":true},null]}],["ev",{"^->":"0.c-0.9.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^\"Captain Hastings!\"",{"->":"$r","var":true},null]}],["ev",{"^->":"0.c-0.9.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-2","flg":18},{"s":["^\"Myself!\"",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.9.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["ev",{"^->":"0.c-0.9.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["ev",{"^->":"0.c-0.9.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^\"You must be joking!\"","\n",["ev",{"^->":"0.c-0.9.g-0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-3","flg":18},{"s":["^\"Mon ami, I am deadly serious.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"0.c-0.9.g-0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-4","flg":18},{"s":["^\"If only...\"",{"->":"$r","var":true},null]}],{"c-3":["ev",{"^->":"0.c-0.9.g-0.c-3.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}],"c-4":["ev",{"^->":"0.c-0.9.g-0.c-4.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}]}]}],{"#f":5}],"c-1":["ev",{"^->":"0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":"0.3.s"},[{"#n":"$r2"}],"\n","^\"Really, Poirot? Are you quite sure?\"","\n",[["ev",{"^->":"0.c-1.9.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"Quite sure.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"0.c-1.9.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^\"It is perfectly obvious.\"",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-1.9.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}],"c-1":["ev",{"^->":"0.c-1.9.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}]}],{"#f":5}],"g-0":["^Mrs. Christie lowered her manuscript a moment. The rest of the writing group sat, open-mouthed.","\n",["done",{"#n":"g-1"}],null]}],"done",null],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^Well, Poirot? Murder or suicide?\"","\n",["ev",{"^->":"0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-0","flg":18},{"s":["^\"Murder!\"",{"->":"$r","var":true},null]}],["ev",{"^->":"0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-1","flg":18},{"s":["^\"Suicide!\"",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":"0.2.s"},[{"#n":"$r2"}],"\n","^\"And who did it?\"","\n",[["ev",{"^->":"0.c-0.9.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"Detective-Inspector Japp!\"",{"->":"$r","var":true},null]}],["ev",{"^->":"0.c-0.9.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^\"Captain Hastings!\"",{"->":"$r","var":true},null]}],["ev",{"^->":"0.c-0.9.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-2","flg":18},{"s":["^\"Myself!\"",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.9.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.g-0"},{"#f":5}],"c-1":["ev",{"^->":"0.c-0.9.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.g-0"},{"#f":5}],"c-2":["ev",{"^->":"0.c-0.9.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n",{"->":".^.^.g-0"},{"#f":5}],"g-0":["^\"You must be joking!\"","\n",["ev",{"^->":"0.c-0.9.g-0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-3","flg":18},{"s":["^\"Mon ami, I am deadly serious.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"0.c-0.9.g-0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-4","flg":18},{"s":["^\"If only...\"",{"->":"$r","var":true},null]}],{"c-3":["ev",{"^->":"0.c-0.9.g-0.c-3.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}],"c-4":["ev",{"^->":"0.c-0.9.g-0.c-4.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}]}]}],{"#f":5}],"c-1":["ev",{"^->":"0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":"0.3.s"},[{"#n":"$r2"}],"\n","^\"Really, Poirot? Are you quite sure?\"","\n",[["ev",{"^->":"0.c-1.9.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"Quite sure.\"",{"->":"$r","var":true},null]}],["ev",{"^->":"0.c-1.9.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^\"It is perfectly obvious.\"",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-1.9.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}],"c-1":["ev",{"^->":"0.c-1.9.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n",{"->":"0.g-0"},{"#f":5}]}],{"#f":5}],"g-0":["^Mrs. Christie lowered her manuscript a moment. The rest of the writing group sat, open-mouthed.","\n",["done",{"#n":"g-1"}],null]}],"done",null],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/glue/glue-with-divert.ink.json b/src/test/resources/inkfiles/glue/glue-with-divert.ink.json index 2a73be5..dc39220 100644 --- a/src/test/resources/inkfiles/glue/glue-with-divert.ink.json +++ b/src/test/resources/inkfiles/glue/glue-with-divert.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^We hurried home ","<>","\n",{"->":"to_savile_row"},["done",{"#n":"g-0"}],null],"done",{"to_savile_row":["^to Savile Row","\n",{"->":"as_fast_as_we_could"},null],"as_fast_as_we_could":["<>","^ as fast as we could.","\n","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^We hurried home ","<>","\n",{"->":"to_savile_row"},["done",{"#n":"g-0"}],null],"done",{"to_savile_row":["^to Savile Row","\n",{"->":"as_fast_as_we_could"},null],"as_fast_as_we_could":["<>","^ as fast as we could.","\n","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/glue/left-right-glue-matching.ink.json b/src/test/resources/inkfiles/glue/left-right-glue-matching.ink.json index a98a969..1c3f8dc 100644 --- a/src/test/resources/inkfiles/glue/left-right-glue-matching.ink.json +++ b/src/test/resources/inkfiles/glue/left-right-glue-matching.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^A line.","\n","ev",{"f()":"f"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^Another line.","\n",{"->":"0.6"},null]}],"nop","\n",["done",{"#n":"g-0"}],null],"done",{"f":["ev",0,"/ev",[{"->":".^.b","c":true},{"b":["^nothing",{"->":"f.4"},null]}],"nop","\n","ev",1,"/ev","~ret",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^A line.","\n","ev",{"f()":"f"},"/ev",[{"->":".^.b","c":true},{"b":["\n","^Another line.","\n",{"->":"0.6"},null]}],"nop","\n",["done",{"#n":"g-0"}],null],"done",{"f":["ev",false,"/ev",[{"->":".^.b","c":true},{"b":["^nothing",{"->":"f.4"},null]}],"nop","\n","ev",true,"/ev","~ret",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/glue/simple-glue.ink.json b/src/test/resources/inkfiles/glue/simple-glue.ink.json index 59137cf..cf307d4 100644 --- a/src/test/resources/inkfiles/glue/simple-glue.ink.json +++ b/src/test/resources/inkfiles/glue/simple-glue.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^Some ","<>","\n","^content ","<>","\n","^with glue.","\n",["done",{"#n":"g-0"}],null],"done",null],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^Some ","<>","\n","^content ","<>","\n","^with glue.","\n",["done",{"#n":"g-0"}],null],"done",null],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/glue/testbugfix1.ink.json b/src/test/resources/inkfiles/glue/testbugfix1.ink.json index 9b45a42..3f49454 100644 --- a/src/test/resources/inkfiles/glue/testbugfix1.ink.json +++ b/src/test/resources/inkfiles/glue/testbugfix1.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^A","\n","ev",{"f()":"f"},"/ev",[{"->":".^.b","c":true},{"b":["^X",{"->":"0.6"},null]}],"nop","\n","^C","\n",["done",{"#n":"g-0"}],null],"done",{"f":["ev",1,"/ev",[{"->":".^.b","c":true},{"b":["\n","ev",0,"/ev","~ret",{"->":"f.4"},null]}],"nop","\n",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^A","\n","ev",{"f()":"f"},"/ev",[{"->":".^.b","c":true},{"b":["^X",{"->":"0.6"},null]}],"nop","\n","^C","\n",["done",{"#n":"g-0"}],null],"done",{"f":["ev",true,"/ev",[{"->":".^.b","c":true},{"b":["\n","ev",false,"/ev","~ret",{"->":"f.4"},null]}],"nop","\n",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/glue/testbugfix2.ink.json b/src/test/resources/inkfiles/glue/testbugfix2.ink.json index 5d51783..a81b31d 100644 --- a/src/test/resources/inkfiles/glue/testbugfix2.ink.json +++ b/src/test/resources/inkfiles/glue/testbugfix2.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^A ","ev",{"f()":"f"},"/ev",[{"->":".^.b","c":true},{"b":["^B",{"->":"0.5"},null]}],"nop","\n","^X","\n",["done",{"#n":"g-0"}],null],"done",{"f":["ev",1,"/ev",[{"->":".^.b","c":true},{"b":["\n","ev",0,"/ev","~ret",{"->":"f.4"},null]}],"nop","\n",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^A ","ev",{"f()":"f"},"/ev",[{"->":".^.b","c":true},{"b":["^B",{"->":"0.5"},null]}],"nop","\n","^X","\n",["done",{"#n":"g-0"}],null],"done",{"f":["ev",true,"/ev",[{"->":".^.b","c":true},{"b":["\n","ev",false,"/ev","~ret",{"->":"f.4"},null]}],"nop","\n",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/knot/multi-line.ink.json b/src/test/resources/inkfiles/knot/multi-line.ink.json index 6c91a05..3dbc4e8 100644 --- a/src/test/resources/inkfiles/knot/multi-line.ink.json +++ b/src/test/resources/inkfiles/knot/multi-line.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^Hello, world!","\n","^Hello?","\n","^Hello, are you there?","\n",["done",{"#n":"g-0"}],null],"done",null],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^Hello, world!","\n","^Hello?","\n","^Hello, are you there?","\n",["done",{"#n":"g-0"}],null],"done",null],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/knot/param-floats.ink.json b/src/test/resources/inkfiles/knot/param-floats.ink.json index 2adaf35..5da4669 100644 --- a/src/test/resources/inkfiles/knot/param-floats.ink.json +++ b/src/test/resources/inkfiles/knot/param-floats.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^How much do you give?","\n","ev","str","^$1","/str","/ev",{"*":"0.c-0","flg":20},"ev","str","^$2","/str","/ev",{"*":"0.c-1","flg":20},"ev","str","^Nothing","/str","/ev",{"*":"0.c-2","flg":20},{"c-0":["^ ","ev",1.2,"/ev",{"->":"give"},"\n",{"->":"0.g-0"},{"#f":5}],"c-1":["^ ","ev",2.5,"/ev",{"->":"give"},"\n",{"->":"0.g-0"},{"#f":5}],"c-2":["^ ","ev",0,"/ev",{"->":"give"},"\n",{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",{"give":[{"temp=":"amount"},"^You give ","ev",{"VAR?":"amount"},"out","/ev","^ dollars.","\n","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^How much do you give?","\n","ev","str","^$1","/str","/ev",{"*":"0.c-0","flg":20},"ev","str","^$2","/str","/ev",{"*":"0.c-1","flg":20},"ev","str","^Nothing","/str","/ev",{"*":"0.c-2","flg":20},{"c-0":["^ ","ev",1.2,"/ev",{"->":"give"},"\n",{"->":"0.g-0"},{"#f":5}],"c-1":["^ ","ev",2.5,"/ev",{"->":"give"},"\n",{"->":"0.g-0"},{"#f":5}],"c-2":["^ ","ev",0,"/ev",{"->":"give"},"\n",{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",{"give":[{"temp=":"amount"},"^You give ","ev",{"VAR?":"amount"},"out","/ev","^ dollars.","\n","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/knot/param-ints.ink.json b/src/test/resources/inkfiles/knot/param-ints.ink.json index 57eed3f..dc550d0 100644 --- a/src/test/resources/inkfiles/knot/param-ints.ink.json +++ b/src/test/resources/inkfiles/knot/param-ints.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^How much do you give?","\n","ev","str","^$1","/str","/ev",{"*":"0.c-0","flg":20},"ev","str","^$2","/str","/ev",{"*":"0.c-1","flg":20},"ev","str","^Nothing","/str","/ev",{"*":"0.c-2","flg":20},{"c-0":["^ ","ev",1,"/ev",{"->":"give"},"\n",{"->":"0.g-0"},{"#f":5}],"c-1":["^ ","ev",2,"/ev",{"->":"give"},"\n",{"->":"0.g-0"},{"#f":5}],"c-2":["^ ","ev",0,"/ev",{"->":"give"},"\n",{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",{"give":[{"temp=":"amount"},"^You give ","ev",{"VAR?":"amount"},"out","/ev","^ dollars.","\n","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^How much do you give?","\n","ev","str","^$1","/str","/ev",{"*":"0.c-0","flg":20},"ev","str","^$2","/str","/ev",{"*":"0.c-1","flg":20},"ev","str","^Nothing","/str","/ev",{"*":"0.c-2","flg":20},{"c-0":["^ ","ev",1,"/ev",{"->":"give"},"\n",{"->":"0.g-0"},{"#f":5}],"c-1":["^ ","ev",2,"/ev",{"->":"give"},"\n",{"->":"0.g-0"},{"#f":5}],"c-2":["^ ","ev",0,"/ev",{"->":"give"},"\n",{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",{"give":[{"temp=":"amount"},"^You give ","ev",{"VAR?":"amount"},"out","/ev","^ dollars.","\n","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/knot/param-multi.ink.json b/src/test/resources/inkfiles/knot/param-multi.ink.json index e200874..7e4cec8 100644 --- a/src/test/resources/inkfiles/knot/param-multi.ink.json +++ b/src/test/resources/inkfiles/knot/param-multi.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^How much do you give?","\n","ev","str","^I don't know","/str","/ev",{"*":"0.c-0","flg":20},{"c-0":["^ ","ev",{"VAR?":"x"},2,{"VAR?":"y"},"/ev",{"->":"give"},"\n",{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",{"give":[{"temp=":"c"},{"temp=":"b"},{"temp=":"a"},"^You give ","ev",{"VAR?":"a"},"out","/ev","^ or ","ev",{"VAR?":"b"},"out","/ev","^ dollars. ","ev",{"VAR?":"y"},"out","/ev","\n","end",null],"global decl":["ev",1,{"VAR=":"x"},"str","^Hmm.","/str",{"VAR=":"y"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^How much do you give?","\n","ev","str","^I don't know","/str","/ev",{"*":"0.c-0","flg":20},{"c-0":["^ ","ev",{"VAR?":"x"},2,{"VAR?":"y"},"/ev",{"->":"give"},"\n",{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",{"give":[{"temp=":"c"},{"temp=":"b"},{"temp=":"a"},"^You give ","ev",{"VAR?":"a"},"out","/ev","^ or ","ev",{"VAR?":"b"},"out","/ev","^ dollars. ","ev",{"VAR?":"y"},"out","/ev","\n","end",null],"global decl":["ev",1,{"VAR=":"x"},"str","^Hmm.","/str",{"VAR=":"y"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/knot/param-recurse.ink.json b/src/test/resources/inkfiles/knot/param-recurse.ink.json index 89224ea..faff9e9 100644 --- a/src/test/resources/inkfiles/knot/param-recurse.ink.json +++ b/src/test/resources/inkfiles/knot/param-recurse.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["ev",0,1,"/ev",{"->":"add_one_to_one_hundred"},["done",{"#n":"g-0"}],null],"done",{"add_one_to_one_hundred":[{"temp=":"x"},{"temp=":"total"},"ev",{"VAR?":"total"},{"VAR?":"x"},"+","/ev",{"temp=":"total","re":true},"ev",{"VAR?":"x"},15,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","ev",{"VAR?":"total"},"/ev",{"->":"finished"},{"->":".^.^.^.15"},null]}],[{"->":".^.b"},{"b":["\n","ev",{"VAR?":"total"},{"VAR?":"x"},1,"+","/ev",{"->":".^.^.^"},{"->":".^.^.^.15"},null]}],"nop","\n",null],"finished":[{"temp=":"total"},"^\"The result is ","ev",{"VAR?":"total"},"out","/ev","^!\" you announce.","\n","^Gauss stares at you in horror.","\n","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["ev",0,1,"/ev",{"->":"add_one_to_one_hundred"},["done",{"#n":"g-0"}],null],"done",{"add_one_to_one_hundred":[{"temp=":"x"},{"temp=":"total"},"ev",{"VAR?":"total"},{"VAR?":"x"},"+","/ev",{"temp=":"total","re":true},"ev",{"VAR?":"x"},15,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","ev",{"VAR?":"total"},"/ev",{"->":"finished"},{"->":".^.^.^.15"},null]}],[{"->":".^.b"},{"b":["\n","ev",{"VAR?":"total"},{"VAR?":"x"},1,"+","/ev",{"->":".^.^.^"},{"->":".^.^.^.15"},null]}],"nop","\n",null],"finished":[{"temp=":"total"},"^\"The result is ","ev",{"VAR?":"total"},"out","/ev","^!\" you announce.","\n","^Gauss stares at you in horror.","\n","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/knot/param-strings.ink.json b/src/test/resources/inkfiles/knot/param-strings.ink.json index d969c99..9c73ca1 100644 --- a/src/test/resources/inkfiles/knot/param-strings.ink.json +++ b/src/test/resources/inkfiles/knot/param-strings.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^Who do you accuse?","\n","ev","str","^Accuse Hasting","/str","/ev",{"*":"0.c-0","flg":20},"ev","str","^Accuse Mrs Black","/str","/ev",{"*":"0.c-1","flg":20},"ev","str","^Accuse myself","/str","/ev",{"*":"0.c-2","flg":20},{"c-0":["^ ","ev","str","^Hastings","/str","/ev",{"->":"accuse"},"\n",{"->":"0.g-0"},{"#f":5}],"c-1":["^ ","ev","str","^Claudia","/str","/ev",{"->":"accuse"},"\n",{"->":"0.g-0"},{"#f":5}],"c-2":["^ ","ev","str","^myself","/str","/ev",{"->":"accuse"},"\n",{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",{"accuse":[{"temp=":"who"},"^\"I accuse ","ev",{"VAR?":"who"},"out","/ev","^!\" Poirot declared.","\n","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^Who do you accuse?","\n","ev","str","^Accuse Hasting","/str","/ev",{"*":"0.c-0","flg":20},"ev","str","^Accuse Mrs Black","/str","/ev",{"*":"0.c-1","flg":20},"ev","str","^Accuse myself","/str","/ev",{"*":"0.c-2","flg":20},{"c-0":["^ ","ev","str","^Hastings","/str","/ev",{"->":"accuse"},"\n",{"->":"0.g-0"},{"#f":5}],"c-1":["^ ","ev","str","^Claudia","/str","/ev",{"->":"accuse"},"\n",{"->":"0.g-0"},{"#f":5}],"c-2":["^ ","ev","str","^myself","/str","/ev",{"->":"accuse"},"\n",{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",{"accuse":[{"temp=":"who"},"^\"I accuse ","ev",{"VAR?":"who"},"out","/ev","^!\" Poirot declared.","\n","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/knot/param-vars.ink.json b/src/test/resources/inkfiles/knot/param-vars.ink.json index 800db9a..709477a 100644 --- a/src/test/resources/inkfiles/knot/param-vars.ink.json +++ b/src/test/resources/inkfiles/knot/param-vars.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^How much do you give?","\n","ev","str","^$1","/str","/ev",{"*":"0.c-0","flg":20},"ev","str","^$2","/str","/ev",{"*":"0.c-1","flg":20},"ev","str","^Nothing","/str","/ev",{"*":"0.c-2","flg":20},{"c-0":["^ ","ev",{"VAR?":"x"},"/ev",{"->":"give"},"\n",{"->":"0.g-0"},{"#f":5}],"c-1":["^ ","ev",{"VAR?":"y"},"/ev",{"->":"give"},"\n",{"->":"0.g-0"},{"#f":5}],"c-2":["^ ","ev",{"VAR?":"z"},"/ev",{"->":"give"},"\n",{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",{"give":[{"temp=":"amount"},"^You give ","ev",{"VAR?":"amount"},"out","/ev","^ dollars.","\n","end",null],"global decl":["ev",1,{"VAR=":"x"},2,{"VAR=":"y"},0,{"VAR=":"z"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^How much do you give?","\n","ev","str","^$1","/str","/ev",{"*":"0.c-0","flg":20},"ev","str","^$2","/str","/ev",{"*":"0.c-1","flg":20},"ev","str","^Nothing","/str","/ev",{"*":"0.c-2","flg":20},{"c-0":["^ ","ev",{"VAR?":"x"},"/ev",{"->":"give"},"\n",{"->":"0.g-0"},{"#f":5}],"c-1":["^ ","ev",{"VAR?":"y"},"/ev",{"->":"give"},"\n",{"->":"0.g-0"},{"#f":5}],"c-2":["^ ","ev",{"VAR?":"z"},"/ev",{"->":"give"},"\n",{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",{"give":[{"temp=":"amount"},"^You give ","ev",{"VAR?":"amount"},"out","/ev","^ dollars.","\n","end",null],"global decl":["ev",1,{"VAR=":"x"},2,{"VAR=":"y"},0,{"VAR=":"z"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/knot/single-line.ink.json b/src/test/resources/inkfiles/knot/single-line.ink.json index cdae3f4..9965165 100644 --- a/src/test/resources/inkfiles/knot/single-line.ink.json +++ b/src/test/resources/inkfiles/knot/single-line.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^Hello, world!","\n",["done",{"#n":"g-0"}],null],"done",null],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^Hello, world!","\n",["done",{"#n":"g-0"}],null],"done",null],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/knot/strip-empty-lines.ink.json b/src/test/resources/inkfiles/knot/strip-empty-lines.ink.json index 6c91a05..3dbc4e8 100644 --- a/src/test/resources/inkfiles/knot/strip-empty-lines.ink.json +++ b/src/test/resources/inkfiles/knot/strip-empty-lines.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^Hello, world!","\n","^Hello?","\n","^Hello, are you there?","\n",["done",{"#n":"g-0"}],null],"done",null],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^Hello, world!","\n","^Hello?","\n","^Hello, are you there?","\n",["done",{"#n":"g-0"}],null],"done",null],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/lists/basic-operations.ink.json b/src/test/resources/inkfiles/lists/basic-operations.ink.json index f87f12f..6977758 100644 --- a/src/test/resources/inkfiles/lists/basic-operations.ink.json +++ b/src/test/resources/inkfiles/lists/basic-operations.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["ev",{"VAR?":"list"},"out","/ev","\n","ev",{"list":{"list.a":1,"list.c":3}},{"list":{"list.b":2,"list.e":5}},"+","out","/ev","\n","ev",{"list":{"list.a":1,"list.b":2,"list.c":3}},{"list":{"list.c":3,"list.b":2,"list.e":5}},"L^","out","/ev","\n","ev",{"VAR?":"list"},{"list":{"list.b":2,"list.d":4,"list.e":5}},"?","out","/ev","\n","ev",{"VAR?":"list"},{"list":{"list.d":4,"list.b":2}},"?","out","/ev","\n","ev",{"VAR?":"list"},{"list":{"list.c":3}},"!?","out","/ev","\n",["done",{"#n":"g-0"}],null],"done",{"global decl":["ev",{"list":{"list.b":2,"list.d":4}},{"VAR=":"list"},"/ev","end",null]}],"listDefs":{"list":{"a":1,"b":2,"c":3,"d":4,"e":5}}} \ No newline at end of file +{"inkVersion":21,"root":[["ev",{"VAR?":"list"},"out","/ev","\n","ev",{"list":{"list.a":1,"list.c":3}},{"list":{"list.b":2,"list.e":5}},"+","out","/ev","\n","ev",{"list":{"list.a":1,"list.b":2,"list.c":3}},{"list":{"list.c":3,"list.b":2,"list.e":5}},"L^","out","/ev","\n","ev",{"VAR?":"list"},{"list":{"list.b":2,"list.d":4,"list.e":5}},"?","out","/ev","\n","ev",{"VAR?":"list"},{"list":{"list.d":4,"list.b":2}},"?","out","/ev","\n","ev",{"VAR?":"list"},{"list":{"list.c":3}},"!?","out","/ev","\n",["done",{"#n":"g-0"}],null],"done",{"global decl":["ev",{"list":{"list.b":2,"list.d":4}},{"VAR=":"list"},"/ev","end",null]}],"listDefs":{"list":{"a":1,"b":2,"c":3,"d":4,"e":5}}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/lists/bug-adding-element.ink.json b/src/test/resources/inkfiles/lists/bug-adding-element.ink.json index a9b5fe0..21b68f8 100644 --- a/src/test/resources/inkfiles/lists/bug-adding-element.ink.json +++ b/src/test/resources/inkfiles/lists/bug-adding-element.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[[["ev",{"^->":"0.init.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":2},{"s":["^a",{"->":"$r","var":true},null]}],["ev",{"^->":"0.init.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",{"VAR?":"gameState"},{"VAR?":"KNOW_ALIEN_REPORT"},"?","/ev",{"*":".^.^.c-1","flg":3},{"s":["^OK",{"->":"$r","var":true},null]}],["ev",{"^->":"0.init.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-2","flg":2},{"s":["^FAIL",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.init.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"gameState"},{"VAR?":"KNOW_ALIEN_REPORT"},"+",{"VAR=":"gameState","re":true},"/ev",{"->":".^.^"},{"->":"0.g-0"},null],"c-1":["ev",{"^->":"0.init.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n",{"->":".^.^"},{"->":"0.g-0"},null],"c-2":["ev",{"^->":"0.init.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n","end",{"->":"0.g-0"},null],"#n":"init"}],{"g-0":["done",null]}],"done",{"global decl":["ev",{"list":{},"origins":["gameState"]},{"VAR=":"gameState"},"/ev","end",null]}],"listDefs":{"gameState":{"KNOW_ALIEN_REPORT":1}}} \ No newline at end of file +{"inkVersion":21,"root":[[[["ev",{"^->":"0.init.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":2},{"s":["^a",{"->":"$r","var":true},null]}],["ev",{"^->":"0.init.1.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str",{"VAR?":"gameState"},{"VAR?":"KNOW_ALIEN_REPORT"},"?","/ev",{"*":".^.^.c-1","flg":3},{"s":["^OK",{"->":"$r","var":true},null]}],["ev",{"^->":"0.init.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-2","flg":2},{"s":["^FAIL",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.init.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"gameState"},{"VAR?":"KNOW_ALIEN_REPORT"},"+",{"VAR=":"gameState","re":true},"/ev",{"->":".^.^"},{"->":"0.g-0"},null],"c-1":["ev",{"^->":"0.init.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n",{"->":".^.^"},{"->":"0.g-0"},null],"c-2":["ev",{"^->":"0.init.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n","end",{"->":"0.g-0"},null],"#n":"init"}],{"g-0":["done",null]}],"done",{"global decl":["ev",{"list":{},"origins":["gameState"]},{"VAR=":"gameState"},"/ev","end",null]}],"listDefs":{"gameState":{"KNOW_ALIEN_REPORT":1}}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/lists/empty-list-origin-after-assignment.ink.json b/src/test/resources/inkfiles/lists/empty-list-origin-after-assignment.ink.json index b308128..ddcf32a 100644 --- a/src/test/resources/inkfiles/lists/empty-list-origin-after-assignment.ink.json +++ b/src/test/resources/inkfiles/lists/empty-list-origin-after-assignment.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["ev",{"list":{}},"/ev",{"VAR=":"x","re":true},"ev",{"VAR?":"x"},"LIST_ALL","out","/ev","\n",["done",{"#n":"g-0"}],null],"done",{"global decl":["ev",{"list":{},"origins":["x"]},{"VAR=":"x"},"/ev","end",null]}],"listDefs":{"x":{"a":1,"b":2,"c":3}}} \ No newline at end of file +{"inkVersion":21,"root":[["ev",{"list":{}},"/ev",{"VAR=":"x","re":true},"ev",{"VAR?":"x"},"LIST_ALL","out","/ev","\n",["done",{"#n":"g-0"}],null],"done",{"global decl":["ev",{"list":{},"origins":["x"]},{"VAR=":"x"},"/ev","end",null]}],"listDefs":{"x":{"a":1,"b":2,"c":3}}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/lists/empty-list-origin.ink.json b/src/test/resources/inkfiles/lists/empty-list-origin.ink.json index ae647db..d054578 100644 --- a/src/test/resources/inkfiles/lists/empty-list-origin.ink.json +++ b/src/test/resources/inkfiles/lists/empty-list-origin.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["ev",{"VAR?":"list"},"LIST_ALL","out","/ev","\n",["done",{"#n":"g-0"}],null],"done",{"global decl":["ev",{"list":{},"origins":["list"]},{"VAR=":"list"},"/ev","end",null]}],"listDefs":{"list":{"a":1,"b":2}}} \ No newline at end of file +{"inkVersion":21,"root":[["ev",{"VAR?":"list"},"LIST_ALL","out","/ev","\n",["done",{"#n":"g-0"}],null],"done",{"global decl":["ev",{"list":{},"origins":["list"]},{"VAR=":"list"},"/ev","end",null]}],"listDefs":{"list":{"a":1,"b":2}}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/lists/list-mixed-items.ink.json b/src/test/resources/inkfiles/lists/list-mixed-items.ink.json index aa17133..9359975 100644 --- a/src/test/resources/inkfiles/lists/list-mixed-items.ink.json +++ b/src/test/resources/inkfiles/lists/list-mixed-items.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["ev",{"VAR?":"list"},{"VAR?":"list2"},"+","out","/ev","\n",["done",{"#n":"g-0"}],null],"done",{"global decl":["ev",{"list":{"list.a":1,"list.c":3}},{"VAR=":"list"},{"list":{"list2.y":2}},{"VAR=":"list2"},"/ev","end",null]}],"listDefs":{"list":{"a":1,"b":2,"c":3,"d":4,"e":5},"list2":{"x":1,"y":2,"z":3}}} \ No newline at end of file +{"inkVersion":21,"root":[["ev",{"VAR?":"list"},{"VAR?":"list2"},"+","out","/ev","\n",["done",{"#n":"g-0"}],null],"done",{"global decl":["ev",{"list":{"list.a":1,"list.c":3}},{"VAR=":"list"},{"list":{"list2.y":2}},{"VAR=":"list2"},"/ev","end",null]}],"listDefs":{"list":{"a":1,"b":2,"c":3,"d":4,"e":5},"list2":{"x":1,"y":2,"z":3}}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/lists/list-range.ink.json b/src/test/resources/inkfiles/lists/list-range.ink.json index 1c014d9..0f17c72 100644 --- a/src/test/resources/inkfiles/lists/list-range.ink.json +++ b/src/test/resources/inkfiles/lists/list-range.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["ev",{"VAR?":"Food"},"LIST_ALL",{"VAR?":"Currency"},"LIST_ALL","+","/ev",{"VAR=":"all","re":true},"\n","ev",{"VAR?":"all"},"out","/ev","\n","ev",{"VAR?":"all"},2,3,"range","out","/ev","\n","ev",{"VAR?":"Numbers"},"LIST_ALL",{"VAR?":"Two"},{"VAR?":"Six"},"range","out","/ev","\n","ev",{"list":{"Food.Pizza":1,"Food.Pasta":2}},-1,100,"range","out","/ev","\n",["done",{"#n":"g-0"}],null],"done",{"global decl":["ev",{"list":{},"origins":["Food"]},{"VAR=":"Food"},{"list":{},"origins":["Currency"]},{"VAR=":"Currency"},{"list":{},"origins":["Numbers"]},{"VAR=":"Numbers"},{"list":{}},{"VAR=":"all"},"/ev","end",null]}],"listDefs":{"Food":{"Pizza":1,"Pasta":2,"Curry":3,"Paella":4},"Currency":{"Pound":1,"Euro":2,"Dollar":3},"Numbers":{"One":1,"Two":2,"Three":3,"Four":4,"Five":5,"Six":6,"Seven":7}}} \ No newline at end of file +{"inkVersion":21,"root":[["ev",{"VAR?":"Food"},"LIST_ALL",{"VAR?":"Currency"},"LIST_ALL","+","/ev",{"VAR=":"all","re":true},"\n","ev",{"VAR?":"all"},"out","/ev","\n","ev",{"VAR?":"all"},2,3,"range","out","/ev","\n","ev",{"VAR?":"Numbers"},"LIST_ALL",{"VAR?":"Two"},{"VAR?":"Six"},"range","out","/ev","\n","ev",{"list":{"Food.Pizza":1,"Food.Pasta":2}},-1,100,"range","out","/ev","\n",["done",{"#n":"g-0"}],null],"done",{"global decl":["ev",{"list":{},"origins":["Food"]},{"VAR=":"Food"},{"list":{},"origins":["Currency"]},{"VAR=":"Currency"},{"list":{},"origins":["Numbers"]},{"VAR=":"Numbers"},{"list":{}},{"VAR=":"all"},"/ev","end",null]}],"listDefs":{"Food":{"Pizza":1,"Pasta":2,"Curry":3,"Paella":4},"Currency":{"Pound":1,"Euro":2,"Dollar":3},"Numbers":{"One":1,"Two":2,"Three":3,"Four":4,"Five":5,"Six":6,"Seven":7}}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/lists/list-save-load.ink.json b/src/test/resources/inkfiles/lists/list-save-load.ink.json index 3080077..b1168b7 100644 --- a/src/test/resources/inkfiles/lists/list-save-load.ink.json +++ b/src/test/resources/inkfiles/lists/list-save-load.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["ev",{"VAR?":"l1"},{"VAR?":"l2"},"+","/ev",{"VAR=":"t","re":true},"ev",{"VAR?":"t"},"out","/ev","\n",["done",{"#n":"g-0"}],null],"done",{"elsewhere":["ev",{"VAR?":"z"},"/ev",{"VAR=":"t","re":true},"ev",{"VAR?":"t"},"out","/ev","\n","end",null],"global decl":["ev",{"list":{"l1.a":1,"l1.c":3}},{"VAR=":"l1"},{"list":{"l2.x":1}},{"VAR=":"l2"},{"list":{}},{"VAR=":"t"},"/ev","end",null]}],"listDefs":{"l1":{"a":1,"b":2,"c":3},"l2":{"x":1,"y":2,"z":3}}} \ No newline at end of file +{"inkVersion":21,"root":[["ev",{"VAR?":"l1"},{"VAR?":"l2"},"+","/ev",{"VAR=":"t","re":true},"ev",{"VAR?":"t"},"out","/ev","\n",["done",{"#n":"g-0"}],null],"done",{"elsewhere":["ev",{"VAR?":"z"},"/ev",{"VAR=":"t","re":true},"ev",{"VAR?":"t"},"out","/ev","\n","end",null],"global decl":["ev",{"list":{"l1.a":1,"l1.c":3}},{"VAR=":"l1"},{"list":{"l2.x":1}},{"VAR=":"l2"},{"list":{}},{"VAR=":"t"},"/ev","end",null]}],"listDefs":{"l1":{"a":1,"b":2,"c":3},"l2":{"x":1,"y":2,"z":3}}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/lists/more-list-operations.ink.json b/src/test/resources/inkfiles/lists/more-list-operations.ink.json index 365045a..fd38f89 100644 --- a/src/test/resources/inkfiles/lists/more-list-operations.ink.json +++ b/src/test/resources/inkfiles/lists/more-list-operations.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["ev",{"VAR?":"l"},"LIST_VALUE","out","/ev","\n","ev","^list",1,"listInt","out","/ev","\n","ev",{"list":{},"origins":["list"]},"/ev",{"temp=":"t"},"\n","ev",{"VAR?":"n"},"/ev",{"VAR=":"t","re":true},"ev",{"VAR?":"t"},"out","/ev","\n","ev",{"VAR?":"t"},"LIST_ALL","/ev",{"VAR=":"t","re":true},"\n","ev",{"VAR?":"t"},{"VAR?":"n"},"-",{"VAR=":"t","re":true},"/ev","ev",{"VAR?":"t"},"out","/ev","\n","ev",{"VAR?":"t"},"LIST_INVERT","/ev",{"VAR=":"t","re":true},"\n","ev",{"VAR?":"t"},"out","/ev","\n",["done",{"#n":"g-0"}],null],"done",{"global decl":["ev",{"list":{},"origins":["list"]},{"VAR=":"list"},"/ev","end",null]}],"listDefs":{"list":{"l":1,"m":5,"n":6}}} \ No newline at end of file +{"inkVersion":21,"root":[["ev",{"VAR?":"l"},"LIST_VALUE","out","/ev","\n","ev","^list",1,"listInt","out","/ev","\n","ev",{"list":{},"origins":["list"]},"/ev",{"temp=":"t"},"\n","ev",{"VAR?":"n"},"/ev",{"VAR=":"t","re":true},"ev",{"VAR?":"t"},"out","/ev","\n","ev",{"VAR?":"t"},"LIST_ALL","/ev",{"VAR=":"t","re":true},"\n","ev",{"VAR?":"t"},{"VAR?":"n"},"-",{"VAR=":"t","re":true},"/ev","ev",{"VAR?":"t"},"out","/ev","\n","ev",{"VAR?":"t"},"LIST_INVERT","/ev",{"VAR=":"t","re":true},"\n","ev",{"VAR?":"t"},"out","/ev","\n",["done",{"#n":"g-0"}],null],"done",{"global decl":["ev",{"list":{},"origins":["list"]},{"VAR=":"list"},"/ev","end",null]}],"listDefs":{"list":{"l":1,"m":5,"n":6}}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/misc/issue15.ink.json b/src/test/resources/inkfiles/misc/issue15.ink.json index 22e21c9..d37a4f3 100644 --- a/src/test/resources/inkfiles/misc/issue15.ink.json +++ b/src/test/resources/inkfiles/misc/issue15.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^This is a test","\n","^SET_X:","\n",["ev",{"VAR?":"x"},"str","^","/str","==","/ev",{"->":".^.b","c":true},{"b":["\n",{"->":"x_not_set"},{"->":"0.6"},null]}],[{"->":".^.b"},{"b":["\n",{"->":"x_is_set"},{"->":"0.6"},null]}],"nop","\n","end",["done",{"#n":"g-0"}],null],"done",{"x_not_set":["^X is not set!","\n","end",null],"x_is_set":["^X is set","\n","end",null],"global decl":["ev","str","^","/str",{"VAR=":"x"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^This is a test","\n","^SET_X:","\n",["ev",{"VAR?":"x"},"str","^","/str","==","/ev",{"->":".^.b","c":true},{"b":["\n",{"->":"x_not_set"},{"->":"0.6"},null]}],[{"->":".^.b"},{"b":["\n",{"->":"x_is_set"},{"->":"0.6"},null]}],"nop","\n","end",["done",{"#n":"g-0"}],null],"done",{"x_not_set":["^X is not set!","\n","end",null],"x_is_set":["^X is set","\n","end",null],"global decl":["ev","str","^","/str",{"VAR=":"x"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/runtime/external-function-0-arg.ink.json b/src/test/resources/inkfiles/runtime/external-function-0-arg.ink.json index 67dd843..5bdfb98 100644 --- a/src/test/resources/inkfiles/runtime/external-function-0-arg.ink.json +++ b/src/test/resources/inkfiles/runtime/external-function-0-arg.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^The value is ","ev",{"x()":"externalFunction"},"out","/ev","^.","\n","end",["done",{"#n":"g-0"}],null],"done",{"externalFunction":["ev","str","^","/str","/ev","~ret",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^The value is ","ev",{"x()":"externalFunction"},"out","/ev","^.","\n","end",["done",{"#n":"g-0"}],null],"done",{"externalFunction":["ev","str","^","/str","/ev","~ret",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/runtime/external-function-1-arg.ink b/src/test/resources/inkfiles/runtime/external-function-1-arg.ink index 791efef..ee4b689 100644 --- a/src/test/resources/inkfiles/runtime/external-function-1-arg.ink +++ b/src/test/resources/inkfiles/runtime/external-function-1-arg.ink @@ -1,9 +1,9 @@ -EXTERNAL externalFunction(boolean) +EXTERNAL externalFunction(integer) -The value is {externalFunction(true)}. +The value is {externalFunction(1)}. -> END -=== function externalFunction(boolean) === +=== function externalFunction(integer) === // Usually external functions can only return placeholder // results, otherwise they'd be defined in ink! ~ return false \ No newline at end of file diff --git a/src/test/resources/inkfiles/runtime/external-function-1-arg.ink.json b/src/test/resources/inkfiles/runtime/external-function-1-arg.ink.json index b99ddc7..7779381 100644 --- a/src/test/resources/inkfiles/runtime/external-function-1-arg.ink.json +++ b/src/test/resources/inkfiles/runtime/external-function-1-arg.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^The value is ","ev",1,{"x()":"externalFunction","exArgs":1},"out","/ev","^.","\n","end",["done",{"#n":"g-0"}],null],"done",{"externalFunction":[{"temp=":"boolean"},"ev",0,"/ev","~ret",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^The value is ","ev",1,{"x()":"externalFunction","exArgs":1},"out","/ev","^.","\n","end",["done",{"#n":"g-0"}],null],"done",{"externalFunction":[{"temp=":"integer"},"ev",false,"/ev","~ret",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/runtime/external-function-2-arg.ink.json b/src/test/resources/inkfiles/runtime/external-function-2-arg.ink.json index f054085..a402fe5 100644 --- a/src/test/resources/inkfiles/runtime/external-function-2-arg.ink.json +++ b/src/test/resources/inkfiles/runtime/external-function-2-arg.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^The value is ","ev",3,4.0,{"x()":"externalFunction","exArgs":2},"out","/ev","^.","\n","end",["done",{"#n":"g-0"}],null],"done",{"externalFunction":[{"temp=":"y"},{"temp=":"x"},"ev",{"VAR?":"x"},{"VAR?":"y"},"+","/ev","~ret",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^The value is ","ev",3,4.0,{"x()":"externalFunction","exArgs":2},"out","/ev","^.","\n","end",["done",{"#n":"g-0"}],null],"done",{"externalFunction":[{"temp=":"y"},{"temp=":"x"},"ev",{"VAR?":"x"},{"VAR?":"y"},"+","/ev","~ret",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/runtime/external-function-3-arg.ink.json b/src/test/resources/inkfiles/runtime/external-function-3-arg.ink.json index 7bcd669..61780c9 100644 --- a/src/test/resources/inkfiles/runtime/external-function-3-arg.ink.json +++ b/src/test/resources/inkfiles/runtime/external-function-3-arg.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^The value is ","ev",1,2,3,{"x()":"externalFunction","exArgs":3},"out","/ev","^.","\n","end",["done",{"#n":"g-0"}],null],"done",{"externalFunction":[{"temp=":"z"},{"temp=":"y"},{"temp=":"x"},"ev",0,"/ev","~ret",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^The value is ","ev",1,2,3,{"x()":"externalFunction","exArgs":3},"out","/ev","^.","\n","end",["done",{"#n":"g-0"}],null],"done",{"externalFunction":[{"temp=":"z"},{"temp=":"y"},{"temp=":"x"},"ev",0,"/ev","~ret",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/runtime/jump-knot.ink.json b/src/test/resources/inkfiles/runtime/jump-knot.ink.json index 71a1da1..ae882fa 100644 --- a/src/test/resources/inkfiles/runtime/jump-knot.ink.json +++ b/src/test/resources/inkfiles/runtime/jump-knot.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[["done",{"#n":"g-0"}],null],"done",{"one":["^One ",{"->":"end"},"\n",null],"two":["^Two ",{"->":"end"},"\n",null],"three":["^Three ",{"->":"end"},"\n",null],"end":["end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"one":["^One ",{"->":"end"},"\n",null],"two":["^Two ",{"->":"end"},"\n",null],"three":["^Three ",{"->":"end"},"\n",null],"end":["end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/runtime/jump-stitch.ink.json b/src/test/resources/inkfiles/runtime/jump-stitch.ink.json index 7f24764..44d0378 100644 --- a/src/test/resources/inkfiles/runtime/jump-stitch.ink.json +++ b/src/test/resources/inkfiles/runtime/jump-stitch.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[["done",{"#n":"g-0"}],null],"done",{"one":[{"->":".^.sone"},{"sone":["^One.1 ",{"->":"end"},"\n",null],"stwo":["^One.2 ",{"->":"end"},"\n",null]}],"two":[{"->":".^.sone"},{"sone":["^Two.1 ",{"->":"end"},"\n",null],"stwo":["^Two.2 ",{"->":"end"},"\n",null],"sthree":["^Two.3 ",{"->":"end"},"\n",null]}],"end":["end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"one":[{"->":".^.sone"},{"sone":["^One.1 ",{"->":"end"},"\n",null],"stwo":["^One.2 ",{"->":"end"},"\n",null]}],"two":[{"->":".^.sone"},{"sone":["^Two.1 ",{"->":"end"},"\n",null],"stwo":["^Two.2 ",{"->":"end"},"\n",null],"sthree":["^Two.3 ",{"->":"end"},"\n",null]}],"end":["end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/runtime/load-save.ink.json b/src/test/resources/inkfiles/runtime/load-save.ink.json index 3b27c56..a5b5767 100644 --- a/src/test/resources/inkfiles/runtime/load-save.ink.json +++ b/src/test/resources/inkfiles/runtime/load-save.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[{"->":"back_in_london"},["done",{"#n":"g-0"}],null],"done",{"back_in_london":[["^We arrived into London at 9.45pm exactly.","\n",["ev",{"^->":"back_in_london.0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"There is not a moment to lose!\"",{"->":"$r","var":true},null]}],["ev",{"^->":"back_in_london.0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^\"Monsieur, let us savour this moment!\"",{"->":"$r","var":true},null]}],"ev","str","^We hurried home","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["ev",{"^->":"back_in_london.0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"^ I declared.","\n",{"->":"hurry_outside"},{"#f":5}],"c-1":["ev",{"^->":"back_in_london.0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"^ I declared.","\n","^My master clouted me firmly around the head and dragged me out of the door.","\n",{"->":"dragged_outside"},{"#f":5}],"c-2":["^ ",{"->":"hurry_outside"},"\n",{"#f":5}]}],null],"hurry_outside":["^We hurried home to Savile Row ",{"->":"as_fast_as_we_could"},"\n",null],"dragged_outside":["^He insisted that we hurried home to Savile Row","\n",{"->":"as_fast_as_we_could"},null],"as_fast_as_we_could":["<>","^ as fast as we could.","\n","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[{"->":"back_in_london"},["done",{"#n":"g-0"}],null],"done",{"back_in_london":[["^We arrived into London at 9.45pm exactly.","\n",["ev",{"^->":"back_in_london.0.2.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":18},{"s":["^\"There is not a moment to lose!\"",{"->":"$r","var":true},null]}],["ev",{"^->":"back_in_london.0.3.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-1","flg":18},{"s":["^\"Monsieur, let us savour this moment!\"",{"->":"$r","var":true},null]}],"ev","str","^We hurried home","/str","/ev",{"*":".^.c-2","flg":20},{"c-0":["ev",{"^->":"back_in_london.0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"^ I declared.","\n",{"->":"hurry_outside"},{"#f":5}],"c-1":["ev",{"^->":"back_in_london.0.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.3.s"},[{"#n":"$r2"}],"^ I declared.","\n","^My master clouted me firmly around the head and dragged me out of the door.","\n",{"->":"dragged_outside"},{"#f":5}],"c-2":["^ ",{"->":"hurry_outside"},"\n",{"#f":5}]}],null],"hurry_outside":["^We hurried home to Savile Row ",{"->":"as_fast_as_we_could"},"\n",null],"dragged_outside":["^He insisted that we hurried home to Savile Row","\n",{"->":"as_fast_as_we_could"},null],"as_fast_as_we_could":["<>","^ as fast as we could.","\n","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/runtime/multiflow-basics.ink.json b/src/test/resources/inkfiles/runtime/multiflow-basics.ink.json index eae7b0b..09d69db 100644 --- a/src/test/resources/inkfiles/runtime/multiflow-basics.ink.json +++ b/src/test/resources/inkfiles/runtime/multiflow-basics.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[["done",{"#n":"g-0"}],null],"done",{"knot1":["^knot 1 line 1","\n","^knot 1 line 2","\n","end",null],"knot2":["^knot 2 line 1","\n","^knot 2 line 2","\n","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"knot1":["^knot 1 line 1","\n","^knot 1 line 2","\n","end",null],"knot2":["^knot 2 line 1","\n","^knot 2 line 2","\n","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/runtime/multiflow-saveloadthreads.ink.json b/src/test/resources/inkfiles/runtime/multiflow-saveloadthreads.ink.json index d1753c8..1a11845 100644 --- a/src/test/resources/inkfiles/runtime/multiflow-saveloadthreads.ink.json +++ b/src/test/resources/inkfiles/runtime/multiflow-saveloadthreads.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^Default line 1","\n","^Default line 2","\n",["done",{"#n":"g-0"}],null],"done",{"red":["^Hello I'm red","\n","ev","str","^red","/str","/ev","thread",{"->":"thread1"},"ev","str","^red","/str","/ev","thread",{"->":"thread2"},"done",null],"blue":["^Hello I'm blue","\n","ev","str","^blue","/str","/ev","thread",{"->":"thread1"},"ev","str","^blue","/str","/ev","thread",{"->":"thread2"},"done",null],"thread1":[{"temp=":"name"},[["ev",{"^->":"thread1.1.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":2},{"s":["^Thread 1 ","ev",{"VAR?":"name"},"out","/ev","^ choice",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"thread1.1.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"name"},"/ev",{"->":"thread1Choice"},null]}],null],"thread2":[{"temp=":"name"},[["ev",{"^->":"thread2.1.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":2},{"s":["^Thread 2 ","ev",{"VAR?":"name"},"out","/ev","^ choice",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"thread2.1.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"name"},"/ev",{"->":"thread2Choice"},null]}],null],"thread1Choice":[{"temp=":"name"},"^After thread 1 choice (","ev",{"VAR?":"name"},"out","/ev","^)","\n","end",null],"thread2Choice":[{"temp=":"name"},"^After thread 2 choice (","ev",{"VAR?":"name"},"out","/ev","^)","\n","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^Default line 1","\n","^Default line 2","\n",["done",{"#n":"g-0"}],null],"done",{"red":["^Hello I'm red","\n","ev","str","^red","/str","/ev","thread",{"->":"thread1"},"ev","str","^red","/str","/ev","thread",{"->":"thread2"},"done",null],"blue":["^Hello I'm blue","\n","ev","str","^blue","/str","/ev","thread",{"->":"thread1"},"ev","str","^blue","/str","/ev","thread",{"->":"thread2"},"done",null],"thread1":[{"temp=":"name"},[["ev",{"^->":"thread1.1.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":2},{"s":["^Thread 1 ","ev",{"VAR?":"name"},"out","/ev","^ choice",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"thread1.1.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"name"},"/ev",{"->":"thread1Choice"},null]}],null],"thread2":[{"temp=":"name"},[["ev",{"^->":"thread2.1.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":2},{"s":["^Thread 2 ","ev",{"VAR?":"name"},"out","/ev","^ choice",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"thread2.1.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"name"},"/ev",{"->":"thread2Choice"},null]}],null],"thread1Choice":[{"temp=":"name"},"^After thread 1 choice (","ev",{"VAR?":"name"},"out","/ev","^)","\n","end",null],"thread2Choice":[{"temp=":"name"},"^After thread 2 choice (","ev",{"VAR?":"name"},"out","/ev","^)","\n","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/runtime/read-visit-counts.ink.json b/src/test/resources/inkfiles/runtime/read-visit-counts.ink.json index 2a73de5..af3b9e3 100644 --- a/src/test/resources/inkfiles/runtime/read-visit-counts.ink.json +++ b/src/test/resources/inkfiles/runtime/read-visit-counts.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[{"->":"one"},["done",{"#f":5,"#n":"g-0"}],null],"done",{"one":["ev",{"VAR?":"x"},4,"<","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"two.s2"},{"->":"one.7"},null]}],[{"->":".^.b"},{"b":["\n",{"->":"two"},{"->":"one.7"},null]}],"nop","\n",{"#f":1}],"two":["end",{"s2":["ev",{"VAR?":"x"},1,"+","/ev",{"VAR=":"x","re":true},{"->":"one"},{"#f":1}],"#f":1}],"global decl":["ev",0,{"VAR=":"x"},"/ev","end",null],"#f":1}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[{"->":"one"},["done",{"#f":5,"#n":"g-0"}],null],"done",{"one":["ev",{"VAR?":"x"},4,"<","/ev",[{"->":".^.b","c":true},{"b":["\n",{"->":"two.s2"},{"->":"one.7"},null]}],[{"->":".^.b"},{"b":["\n",{"->":"two"},{"->":"one.7"},null]}],"nop","\n",{"#f":1}],"two":["end",{"s2":["ev",{"VAR?":"x"},1,"+","/ev",{"VAR=":"x","re":true},{"->":"one"},{"#f":1}],"#f":1}],"global decl":["ev",0,{"VAR=":"x"},"/ev","end",null],"#f":1}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/runtime/saving-loading.ink.json b/src/test/resources/inkfiles/runtime/saving-loading.ink.json index 6870c97..f42774d 100644 --- a/src/test/resources/inkfiles/runtime/saving-loading.ink.json +++ b/src/test/resources/inkfiles/runtime/saving-loading.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[["done",{"#n":"g-0"}],null],"done",{"hurry_home":["^We hurried home ","<>","\n",{"->":"to_savile_row"},null],"to_savile_row":["^to Savile Row","\n",{"->":"as_fast_as_we_could"},null],"as_fast_as_we_could":["<>","^ as fast as we could.","\n","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[["done",{"#n":"g-0"}],null],"done",{"hurry_home":["^We hurried home ","<>","\n",{"->":"to_savile_row"},null],"to_savile_row":["^to Savile Row","\n",{"->":"as_fast_as_we_could"},null],"as_fast_as_we_could":["<>","^ as fast as we could.","\n","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/runtime/set-get-variables.ink.json b/src/test/resources/inkfiles/runtime/set-get-variables.ink.json index 47cd681..4fcf0ce 100644 --- a/src/test/resources/inkfiles/runtime/set-get-variables.ink.json +++ b/src/test/resources/inkfiles/runtime/set-get-variables.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["ev",10,"/ev",{"VAR=":"x","re":true},"ev","str","^Set variable from code to 15","/str","/ev",{"*":"0.c-0","flg":20},{"c-0":["\n","ev",{"VAR?":"x"},15,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","^OK","\n",{"->":"0.c-0.8"},null]}],[{"->":".^.b"},{"b":["\n","^KO","\n",{"->":"0.c-0.8"},null]}],"nop","\n","end",{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",{"global decl":["ev",0,{"VAR=":"x"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["ev",10,"/ev",{"VAR=":"x","re":true},"ev","str","^Set variable from code to 15","/str","/ev",{"*":"0.c-0","flg":20},{"c-0":["\n","ev",{"VAR?":"x"},15,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","^OK","\n",{"->":"0.c-0.8"},null]}],[{"->":".^.b"},{"b":["\n","^KO","\n",{"->":"0.c-0.8"},null]}],"nop","\n","end",{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",{"global decl":["ev",0,{"VAR=":"x"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/runtime/variable-observers.ink.json b/src/test/resources/inkfiles/runtime/variable-observers.ink.json index 76acc5e..5316003 100644 --- a/src/test/resources/inkfiles/runtime/variable-observers.ink.json +++ b/src/test/resources/inkfiles/runtime/variable-observers.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["ev",5,"/ev",{"VAR=":"x","re":true},["ev",{"^->":"0.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-0","flg":18},{"s":["^Sets x = 10",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":"0.4.s"},[{"#n":"$r2"}],"\n","ev",10,"/ev",{"VAR=":"x","re":true},"end",{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",{"global decl":["ev",0,{"VAR=":"x"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["ev",5,"/ev",{"VAR=":"x","re":true},["ev",{"^->":"0.4.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-0","flg":18},{"s":["^Sets x = 10",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":"0.4.s"},[{"#n":"$r2"}],"\n","ev",10,"/ev",{"VAR=":"x","re":true},"end",{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",{"global decl":["ev",0,{"VAR=":"x"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/stitch/auto-stitch.ink.json b/src/test/resources/inkfiles/stitch/auto-stitch.ink.json index 8e56b6c..c6312af 100644 --- a/src/test/resources/inkfiles/stitch/auto-stitch.ink.json +++ b/src/test/resources/inkfiles/stitch/auto-stitch.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[{"->":"the_orient_express"},["done",{"#n":"g-0"}],null],"done",{"the_orient_express":[{"->":".^.in_first_class"},{"in_first_class":[["^I settled my master.","\n","ev","str","^Move to third class","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Are you sure","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n",{"->":".^.^.^.^.in_third_class"},{"#f":5}],"c-1":["^ ",{"->":".^.^.^.^"},"\n",{"#f":5}]}],null],"in_third_class":["^I put myself in third.","\n","end",null]}]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[{"->":"the_orient_express"},["done",{"#n":"g-0"}],null],"done",{"the_orient_express":[{"->":".^.in_first_class"},{"in_first_class":[["^I settled my master.","\n","ev","str","^Move to third class","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Are you sure","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["\n",{"->":".^.^.^.^.in_third_class"},{"#f":5}],"c-1":["^ ",{"->":".^.^.^.^"},"\n",{"#f":5}]}],null],"in_third_class":["^I put myself in third.","\n","end",null]}]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/stitch/manual-stitch.ink.json b/src/test/resources/inkfiles/stitch/manual-stitch.ink.json index 16456a4..ea2ed8f 100644 --- a/src/test/resources/inkfiles/stitch/manual-stitch.ink.json +++ b/src/test/resources/inkfiles/stitch/manual-stitch.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[{"->":"the_orient_express"},["done",{"#n":"g-0"}],null],"done",{"the_orient_express":[["^How shall we travel?","\n","ev","str","^In first class","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^I'll go cheap","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ",{"->":".^.^.^.in_first_class"},"\n",{"#f":5}],"c-1":["^ ",{"->":".^.^.^.in_third_class"},"\n",{"#f":5}]}],{"in_first_class":[["^I settled my master.","\n","ev","str","^Move to third class","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["\n",{"->":".^.^.^.^.in_third_class"},{"#f":5}]}],null],"in_third_class":["^I put myself in third.","\n","end",null]}]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[{"->":"the_orient_express"},["done",{"#n":"g-0"}],null],"done",{"the_orient_express":[["^How shall we travel?","\n","ev","str","^In first class","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^I'll go cheap","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ",{"->":".^.^.^.in_first_class"},"\n",{"#f":5}],"c-1":["^ ",{"->":".^.^.^.in_third_class"},"\n",{"#f":5}]}],{"in_first_class":[["^I settled my master.","\n","ev","str","^Move to third class","/str","/ev",{"*":".^.c-0","flg":20},{"c-0":["\n",{"->":".^.^.^.^.in_third_class"},{"#f":5}]}],null],"in_third_class":["^I put myself in third.","\n","end",null]}]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/tags/tags.ink.json b/src/test/resources/inkfiles/tags/tags.ink.json index cd4096b..5b5fabf 100644 --- a/src/test/resources/inkfiles/tags/tags.ink.json +++ b/src/test/resources/inkfiles/tags/tags.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[{"#":"author: Joe"},{"#":"title: My Great Story"},"^This is the content","\n",["done",{"#n":"g-0"}],null],"done",{"knot":[{"#":"knot tag"},"^Knot content","\n",{"#":"end of knot tag"},"end",{"stitch":[{"#":"stitch tag"},"^Stitch content","\n",{"#":"this tag is below some content so isn't included in the static tags for the stitch"},"end",null]}],"global decl":["ev",2,{"VAR=":"x"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["#","^author: Joe","/#","#","^title: My Great Story","/#","^This is the content","\n",["done",{"#n":"g-0"}],null],"done",{"knot":["#","^knot tag","/#","^Knot content","\n","#","^end of knot tag","/#","end",{"stitch":["#","^stitch tag","/#","^Stitch content","\n","#","^this tag is below some content so isn't included in the static tags for the stitch","/#","end",null]}],"global decl":["ev",2,{"VAR=":"x"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/tags/tagsDynamicContent.ink b/src/test/resources/inkfiles/tags/tagsDynamicContent.ink new file mode 100644 index 0000000..63576b9 --- /dev/null +++ b/src/test/resources/inkfiles/tags/tagsDynamicContent.ink @@ -0,0 +1 @@ +tag # pic{5+3}{red|blue}.jpg \ No newline at end of file diff --git a/src/test/resources/inkfiles/tags/tagsDynamicContent.ink.json b/src/test/resources/inkfiles/tags/tagsDynamicContent.ink.json new file mode 100644 index 0000000..b2ed459 --- /dev/null +++ b/src/test/resources/inkfiles/tags/tagsDynamicContent.ink.json @@ -0,0 +1 @@ +{"inkVersion":21,"root":[["^tag ","#","^pic","ev",5,3,"+","out","/ev",["ev","visit",1,"MIN","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"nop",{"s0":["pop","^red",{"->":"0.9.17"},null],"s1":["pop","^blue",{"->":"0.9.17"},null],"#f":5}],"^.jpg","/#","\n",["done",{"#n":"g-0"}],null],"done",null],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/tags/tagsInChoice.ink b/src/test/resources/inkfiles/tags/tagsInChoice.ink new file mode 100644 index 0000000..77be22b --- /dev/null +++ b/src/test/resources/inkfiles/tags/tagsInChoice.ink @@ -0,0 +1 @@ ++ one #one [two #two] three #three -> END \ No newline at end of file diff --git a/src/test/resources/inkfiles/tags/tagsInChoice.ink.json b/src/test/resources/inkfiles/tags/tagsInChoice.ink.json new file mode 100644 index 0000000..5184748 --- /dev/null +++ b/src/test/resources/inkfiles/tags/tagsInChoice.ink.json @@ -0,0 +1 @@ +{"inkVersion":21,"root":[[["ev",{"^->":"0.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","str","^two ","#","^two","/#","/str","/ev",{"*":"0.c-0","flg":6},{"s":["^one ","#","^one ","/#",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":"0.0.s"},[{"#n":"$r2"}],"^ three ","#","^three ","/#","end","\n",{"->":"0.g-0"},null],"g-0":["done",null]}],"done",null],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/tags/tagsInSeq.ink b/src/test/resources/inkfiles/tags/tagsInSeq.ink new file mode 100644 index 0000000..b39f74d --- /dev/null +++ b/src/test/resources/inkfiles/tags/tagsInSeq.ink @@ -0,0 +1,4 @@ +-> knot -> knot -> +== knot +A {red #red|white #white|blue #blue|green #green} sequence. +->-> \ No newline at end of file diff --git a/src/test/resources/inkfiles/tags/tagsInSeq.ink.json b/src/test/resources/inkfiles/tags/tagsInSeq.ink.json new file mode 100644 index 0000000..5c2dfa2 --- /dev/null +++ b/src/test/resources/inkfiles/tags/tagsInSeq.ink.json @@ -0,0 +1 @@ +{"inkVersion":21,"root":[[{"->t->":"knot"},{"->t->":"knot"},["done",{"#n":"g-0"}],null],"done",{"knot":["^A ",["ev","visit",3,"MIN","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"ev","du",3,"==","/ev",{"->":".^.s3","c":true},"nop",{"s0":["pop","^red ","#","^red",{"->":".^.^.29"},null],"s1":["pop","^white ","/#","#","^white",{"->":".^.^.29"},null],"s2":["pop","^blue ","/#","#","^blue",{"->":".^.^.29"},null],"s3":["pop","^green ","/#","#","^green",{"->":".^.^.29"},null],"#f":5}],"/#","^ sequence.","\n","ev","void","/ev","->->",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/threads/thread-bug.ink.json b/src/test/resources/inkfiles/threads/thread-bug.ink.json index 5c9715c..a8bc091 100644 --- a/src/test/resources/inkfiles/threads/thread-bug.ink.json +++ b/src/test/resources/inkfiles/threads/thread-bug.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[{"->":"start"},["done",{"#n":"g-0"}],null],"done",{"start":[["^Here is some gold. Do you want it?","\n",["ev",{"^->":"start.0.top"},"/ev","thread",{"->":"choices"},["ev",{"^->":"start.0.top.5.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":2},{"s":["^Yes",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"start.0.top.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.5.s"},[{"#n":"$r2"}],"\n","^You win!","\n","end",null],"#f":7,"#n":"top"}],null],null],"choices":[{"temp=":"goback"},[["ev",{"^->":"choices.1.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":2},{"s":["^No",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"choices.1.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n","^Try again!","\n",{"->":"goback","var":true},null]}],null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[{"->":"start"},["done",{"#n":"g-0"}],null],"done",{"start":[["^Here is some gold. Do you want it?","\n",["ev",{"^->":"start.0.top"},"/ev","thread",{"->":"choices"},["ev",{"^->":"start.0.top.5.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":2},{"s":["^Yes",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"start.0.top.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.5.s"},[{"#n":"$r2"}],"\n","^You win!","\n","end",null],"#f":7,"#n":"top"}],null],null],"choices":[{"temp=":"goback"},[["ev",{"^->":"choices.1.0.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":".^.^.c-0","flg":2},{"s":["^No",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"choices.1.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.0.s"},[{"#n":"$r2"}],"\n","^Try again!","\n",{"->":"goback","var":true},null]}],null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/tunnels/tunnel-onwards-divert-override.ink.json b/src/test/resources/inkfiles/tunnels/tunnel-onwards-divert-override.ink.json index e5e5b8e..621c7da 100644 --- a/src/test/resources/inkfiles/tunnels/tunnel-onwards-divert-override.ink.json +++ b/src/test/resources/inkfiles/tunnels/tunnel-onwards-divert-override.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[{"->t->":"A"},"^We will never return to here!","\n",["done",{"#n":"g-0"}],null],"done",{"A":["^This is A","\n","ev",{"^->":"B"},"/ev","->->",null],"B":["^Now in B.","\n","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[{"->t->":"A"},"^We will never return to here!","\n",["done",{"#n":"g-0"}],null],"done",{"A":["^This is A","\n","ev",{"^->":"B"},"/ev","->->",null],"B":["^Now in B.","\n","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/variable/var-divert.ink.json b/src/test/resources/inkfiles/variable/var-divert.ink.json index 9c104e8..f688692 100644 --- a/src/test/resources/inkfiles/variable/var-divert.ink.json +++ b/src/test/resources/inkfiles/variable/var-divert.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^Divert as variable example","\n",{"->":"continue_or_quit"},["done",{"#n":"g-0"}],null],"done",{"continue_or_quit":[["^Give up now, or keep trying to save your Kingdom?","\n","ev","str","^Keep trying!","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Give up","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ",{"->":".^.^.^"},"\n",{"#f":5}],"c-1":["^ ",{"->":"current_epilogue","var":true},"\n",{"#f":5}]}],null],"everybody_dies":["^Everybody dies.","\n","end",{"#f":3}],"global decl":["ev",{"^->":"everybody_dies"},{"VAR=":"current_epilogue"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^Divert as variable example","\n",{"->":"continue_or_quit"},["done",{"#n":"g-0"}],null],"done",{"continue_or_quit":[["^Give up now, or keep trying to save your Kingdom?","\n","ev","str","^Keep trying!","/str","/ev",{"*":".^.c-0","flg":20},"ev","str","^Give up","/str","/ev",{"*":".^.c-1","flg":20},{"c-0":["^ ",{"->":".^.^.^"},"\n",{"#f":5}],"c-1":["^ ",{"->":"current_epilogue","var":true},"\n",{"#f":5}]}],null],"everybody_dies":["^Everybody dies.","\n","end",{"#f":3}],"global decl":["ev",{"^->":"everybody_dies"},{"VAR=":"current_epilogue"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/variable/varcalc.ink.json b/src/test/resources/inkfiles/variable/varcalc.ink.json index 9959113..29ed74c 100644 --- a/src/test/resources/inkfiles/variable/varcalc.ink.json +++ b/src/test/resources/inkfiles/variable/varcalc.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["ev",1,"/ev",{"VAR=":"knows","re":true},"ev",{"VAR?":"x"},{"VAR?":"x"},"*",{"VAR?":"y"},{"VAR?":"y"},"*","-",{"VAR?":"c"},"+","/ev",{"VAR=":"x","re":true},"ev",2,{"VAR?":"x"},"*",{"VAR?":"y"},"*","/ev",{"VAR=":"y","re":true},"ev","str","^a","/str","/ev",{"VAR=":"str","re":true},"ev",{"VAR?":"str"},"str","^a","/str","+",{"VAR=":"str","re":true},"/ev","^The values are ","ev",{"VAR?":"knows"},"out","/ev","^ and ","ev",{"VAR?":"x"},"out","/ev","^ and ","ev",{"VAR?":"y"},"out","/ev","^ and ","ev",{"VAR?":"str"},"out","/ev","^.","\n","end",["done",{"#n":"g-0"}],null],"done",{"global decl":["ev",0,{"VAR=":"knows"},2,{"VAR=":"x"},3,{"VAR=":"y"},4,{"VAR=":"c"},"str","^","/str",{"VAR=":"str"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["ev",true,"/ev",{"VAR=":"knows","re":true},"ev",{"VAR?":"x"},{"VAR?":"x"},"*",{"VAR?":"y"},{"VAR?":"y"},"*","-",{"VAR?":"c"},"+","/ev",{"VAR=":"x","re":true},"ev",2,{"VAR?":"x"},"*",{"VAR?":"y"},"*","/ev",{"VAR=":"y","re":true},"ev","str","^a","/str","/ev",{"VAR=":"str","re":true},"ev",{"VAR?":"str"},"str","^a","/str","+",{"VAR=":"str","re":true},"/ev","^The values are ","ev",{"VAR?":"knows"},"out","/ev","^ and ","ev",{"VAR?":"x"},"out","/ev","^ and ","ev",{"VAR?":"y"},"out","/ev","^ and ","ev",{"VAR?":"str"},"out","/ev","^.","\n","end",["done",{"#n":"g-0"}],null],"done",{"global decl":["ev",false,{"VAR=":"knows"},2,{"VAR=":"x"},3,{"VAR=":"y"},4,{"VAR=":"c"},"str","^","/str",{"VAR=":"str"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/variable/variable-declaration.ink.json b/src/test/resources/inkfiles/variable/variable-declaration.ink.json index 4b486ca..e9a1b2c 100644 --- a/src/test/resources/inkfiles/variable/variable-declaration.ink.json +++ b/src/test/resources/inkfiles/variable/variable-declaration.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["^\"My name is Jean Passepartout, but my friend's call me ","ev",{"VAR?":"friendly_name_of_player"},"out","/ev","^. I'm ","ev",{"VAR?":"age"},"out","/ev","^ years old.\"","\n",["done",{"#n":"g-0"}],null],"done",{"global decl":["ev","str","^Jackie","/str",{"VAR=":"friendly_name_of_player"},23,{"VAR=":"age"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["^\"My name is Jean Passepartout, but my friend's call me ","ev",{"VAR?":"friendly_name_of_player"},"out","/ev","^. I'm ","ev",{"VAR?":"age"},"out","/ev","^ years old.\"","\n",["done",{"#n":"g-0"}],null],"done",{"global decl":["ev","str","^Jackie","/str",{"VAR=":"friendly_name_of_player"},23,{"VAR=":"age"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/variable/varstringinc.ink.json b/src/test/resources/inkfiles/variable/varstringinc.ink.json index 61d27cc..e8c9c52 100644 --- a/src/test/resources/inkfiles/variable/varstringinc.ink.json +++ b/src/test/resources/inkfiles/variable/varstringinc.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[["ev","str","^a","/str","/ev",{"VAR=":"v","re":true},["ev",{"^->":"0.6.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-0","flg":18},{"s":["^inc",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":"0.6.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"v"},"str","^b","/str","+","/ev",{"VAR=":"v","re":true},"ev",{"VAR?":"v"},"out","/ev","^.","\n","end",{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",{"global decl":["ev","str","^","/str",{"VAR=":"v"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[["ev","str","^a","/str","/ev",{"VAR=":"v","re":true},["ev",{"^->":"0.6.$r1"},{"temp=":"$r"},"str",{"->":".^.s"},[{"#n":"$r1"}],"/str","/ev",{"*":"0.c-0","flg":18},{"s":["^inc",{"->":"$r","var":true},null]}],{"c-0":["ev",{"^->":"0.c-0.$r2"},"/ev",{"temp=":"$r"},{"->":"0.6.s"},[{"#n":"$r2"}],"\n","ev",{"VAR?":"v"},"str","^b","/str","+","/ev",{"VAR=":"v","re":true},"ev",{"VAR?":"v"},"out","/ev","^.","\n","end",{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",{"global decl":["ev","str","^","/str",{"VAR=":"v"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/variabletext/cycle.ink.json b/src/test/resources/inkfiles/variabletext/cycle.ink.json index 617364c..5890a29 100644 --- a/src/test/resources/inkfiles/variabletext/cycle.ink.json +++ b/src/test/resources/inkfiles/variabletext/cycle.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[{"->":"test"},["done",{"#n":"g-0"}],null],"done",{"test":[["^The radio hissed into life. ",["ev","visit",3,"%","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"nop",{"s0":["pop","^\"Three!\"",{"->":".^.^.23"},null],"s1":["pop","^\"Two!\"",{"->":".^.^.23"},null],"s2":["pop","^\"One!\"",{"->":".^.^.23"},null],"#f":5}],"\n","ev","str","^Again","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"test"},"\n",null]}],null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[{"->":"test"},["done",{"#n":"g-0"}],null],"done",{"test":[["^The radio hissed into life. ",["ev","visit",3,"%","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"nop",{"s0":["pop","^\"Three!\"",{"->":".^.^.23"},null],"s1":["pop","^\"Two!\"",{"->":".^.^.23"},null],"s2":["pop","^\"One!\"",{"->":".^.^.23"},null],"#f":5}],"\n","ev","str","^Again","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"test"},"\n",null]}],null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/variabletext/empty-elements.ink.json b/src/test/resources/inkfiles/variabletext/empty-elements.ink.json index a66c0f8..2482a03 100644 --- a/src/test/resources/inkfiles/variabletext/empty-elements.ink.json +++ b/src/test/resources/inkfiles/variabletext/empty-elements.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[{"->":"test"},["done",{"#n":"g-0"}],null],"done",{"test":[["^The radio hissed into life. ",["ev","visit",2,"MIN","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"nop",{"s0":["pop",{"->":".^.^.23"},null],"s1":["pop",{"->":".^.^.23"},null],"s2":["pop","^\"One!\"",{"->":".^.^.23"},null],"#f":5}],"\n","ev","str","^Again","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"test"},"\n",null]}],null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[{"->":"test"},["done",{"#n":"g-0"}],null],"done",{"test":[["^The radio hissed into life. ",["ev","visit",2,"MIN","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"nop",{"s0":["pop",{"->":".^.^.23"},null],"s1":["pop",{"->":".^.^.23"},null],"s2":["pop","^\"One!\"",{"->":".^.^.23"},null],"#f":5}],"\n","ev","str","^Again","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"test"},"\n",null]}],null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/variabletext/list-in-choice.ink.json b/src/test/resources/inkfiles/variabletext/list-in-choice.ink.json index f7d4b5f..e9afe37 100644 --- a/src/test/resources/inkfiles/variabletext/list-in-choice.ink.json +++ b/src/test/resources/inkfiles/variabletext/list-in-choice.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[{"->":"test"},["done",{"#n":"g-0"}],null],"done",{"test":[["^He looked at me oddly.","\n","ev","str","^\"Hello, ",["ev","visit",3,"%","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"nop",{"s0":["pop","^Master",{"->":".^.^.23"},null],"s1":["pop","^Monsieur",{"->":".^.^.23"},null],"s2":["pop","^you",{"->":".^.^.23"},null],"#f":5}],"^!\"","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"test"},"\n",null]}],null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[{"->":"test"},["done",{"#n":"g-0"}],null],"done",{"test":[["^He looked at me oddly.","\n","ev","str","^\"Hello, ",["ev","visit",3,"%","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"nop",{"s0":["pop","^Master",{"->":".^.^.23"},null],"s1":["pop","^Monsieur",{"->":".^.^.23"},null],"s2":["pop","^you",{"->":".^.^.23"},null],"#f":5}],"^!\"","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"test"},"\n",null]}],null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/variabletext/once.ink.json b/src/test/resources/inkfiles/variabletext/once.ink.json index 9f2ce32..ae9a77e 100644 --- a/src/test/resources/inkfiles/variabletext/once.ink.json +++ b/src/test/resources/inkfiles/variabletext/once.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[{"->":"test"},["done",{"#n":"g-0"}],null],"done",{"test":[["^The radio hissed into life. ",["ev","visit",3,"MIN","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"ev","du",3,"==","/ev",{"->":".^.s3","c":true},"nop",{"s0":["pop","^\"Three!\"",{"->":".^.^.29"},null],"s1":["pop","^\"Two!\"",{"->":".^.^.29"},null],"s2":["pop","^\"One!\"",{"->":".^.^.29"},null],"s3":["pop",{"->":".^.^.29"},null],"#f":5}],"\n","ev","str","^Again","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"test"},"\n",null]}],null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[{"->":"test"},["done",{"#n":"g-0"}],null],"done",{"test":[["^The radio hissed into life. ",["ev","visit",3,"MIN","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"ev","du",3,"==","/ev",{"->":".^.s3","c":true},"nop",{"s0":["pop","^\"Three!\"",{"->":".^.^.29"},null],"s1":["pop","^\"Two!\"",{"->":".^.^.29"},null],"s2":["pop","^\"One!\"",{"->":".^.^.29"},null],"s3":["pop",{"->":".^.^.29"},null],"#f":5}],"\n","ev","str","^Again","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"test"},"\n",null]}],null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/variabletext/sequence.ink.json b/src/test/resources/inkfiles/variabletext/sequence.ink.json index bb6a847..be4e896 100644 --- a/src/test/resources/inkfiles/variabletext/sequence.ink.json +++ b/src/test/resources/inkfiles/variabletext/sequence.ink.json @@ -1 +1 @@ -{"inkVersion":19,"root":[[{"->":"test"},["done",{"#n":"g-0"}],null],"done",{"test":[["^The radio hissed into life. ",["ev","visit",3,"MIN","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"ev","du",3,"==","/ev",{"->":".^.s3","c":true},"nop",{"s0":["pop","^\"Three!\"",{"->":".^.^.29"},null],"s1":["pop","^\"Two!\"",{"->":".^.^.29"},null],"s2":["pop","^\"One!\"",{"->":".^.^.29"},null],"s3":["pop","^There was the white noise racket of an explosion.",{"->":".^.^.29"},null],"#f":5}],"\n","ev","str","^Again","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"test"},"\n",null]}],null]}],"listDefs":{}} \ No newline at end of file +{"inkVersion":21,"root":[[{"->":"test"},["done",{"#n":"g-0"}],null],"done",{"test":[["^The radio hissed into life. ",["ev","visit",3,"MIN","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"ev","du",3,"==","/ev",{"->":".^.s3","c":true},"nop",{"s0":["pop","^\"Three!\"",{"->":".^.^.29"},null],"s1":["pop","^\"Two!\"",{"->":".^.^.29"},null],"s2":["pop","^\"One!\"",{"->":".^.^.29"},null],"s3":["pop","^There was the white noise racket of an explosion.",{"->":".^.^.29"},null],"#f":5}],"\n","ev","str","^Again","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"test"},"\n",null]}],null]}],"listDefs":{}} \ No newline at end of file