diff --git a/.github/workflows/gradle-build-push.yml b/.github/workflows/gradle-build-push.yml new file mode 100644 index 0000000..c7432ea --- /dev/null +++ b/.github/workflows/gradle-build-push.yml @@ -0,0 +1,34 @@ +name: Run Gradle build on push + +on: [ push ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 21 + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: 21 + - name: Cache Gradle packages + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Build with Gradle + run: ./gradlew build + - name: Pass tests and checks + run: ./gradlew check + - name: Cleanup Gradle Cache + # Remove some files from the Gradle cache, so they aren't cached by GitHub Actions. + # Restoring these files from a GitHub Actions cache might cause problems for future builds. + run: | + rm -f ~/.gradle/caches/modules-2/modules-2.lock + rm -f ~/.gradle/caches/modules-2/gc.properties \ No newline at end of file diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 0000000..df6b257 --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,39 @@ +name: Publish release + +on: + release: + types: [published] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 21 + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: 21 + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build + run: ./gradlew build + - name: Import GPG key + id: import_gpg + uses: crazy-max/ghaction-import-gpg@v5 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.GPG_PASSPHRASE }} + - name: Release build deploy + env: + NEXUS_USERNAME: ${{ secrets.NEXUS_USERNAME }} + NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }} + run: + ./gradlew clean publish -Prelease -Psigning.gnupg.keyId=${{ secrets.GPG_KEYID }} -Psigning.gnupg.passphrase=${{ secrets.GPG_PASSPHRASE }} -Psigning.gnupg.keyName=${{ secrets.GPG_KEYID }} + - name: Trigger manual upload to Central Repository + run: | + curl -X POST \ + -H "Authorization: Bearer $(echo -n '${{ secrets.NEXUS_USERNAME }}:${{ secrets.NEXUS_PASSWORD }}' | base64)" \ + -H "Content-Type: application/json" \ + https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/com.bladecoder \ No newline at end of file diff --git a/.github/workflows/publish-snapshot.yml b/.github/workflows/publish-snapshot.yml new file mode 100644 index 0000000..16e4365 --- /dev/null +++ b/.github/workflows/publish-snapshot.yml @@ -0,0 +1,27 @@ +name: Publish snapshot + +on: + push: + branches: + - master +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 21 + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: 21 + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build + run: ./gradlew build + - name: Publish snapshot + env: + NEXUS_USERNAME: ${{ secrets.NEXUS_USERNAME }} + NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }} + run: ./gradlew publish \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7abcff5..c3b7aab 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,4 @@ build/ *.log - +.DS_Store diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 21cf769..0000000 --- a/.travis.yml +++ /dev/null @@ -1 +0,0 @@ -language: Java diff --git a/README.md b/README.md index afeef0f..e9138dc 100644 --- a/README.md +++ b/README.md @@ -13,23 +13,18 @@ First you need to turn your ink file into a json file [as described here](https: ```java InputStream systemResourceAsStream = ClassLoader.getSystemResourceAsStream(filename); -BufferedReader br = new BufferedReader(new InputStreamReader(systemResourceAsStream, "UTF-8")); - -try { - StringBuilder sb = new StringBuilder(); - String line = br.readLine(); - - while (line != null) { - sb.append(line); - sb.append("\n"); - line = br.readLine(); - } - -} finally { - br.close(); +try (BufferedReader br = + new BufferedReader(new InputStreamReader(systemResourceAsStream, StandardCharsets.UTF_8))) { + StringBuilder sb = new StringBuilder(); + String line = br.readLine(); + + while (line != null) { + sb.append(line); + sb.append("\n"); + line = br.readLine(); + } + return sb.toString(); } - -String json = sb.toSTring().replace('\uFEFF', ' '); ``` ### Starting a story diff --git a/build.gradle b/build.gradle index b593e63..0738be8 100644 --- a/build.gradle +++ b/build.gradle @@ -1,135 +1,148 @@ -apply plugin: 'java-library' -apply plugin: 'java' -apply plugin: 'maven' -apply plugin: 'signing' +plugins { + id 'java-library' + id 'maven-publish' + id 'signing' + id "com.diffplug.spotless" version "7.2.1" +} group = 'com.bladecoder.ink' -sourceCompatibility = 1.7 -targetCompatibility=1.7 +sourceCompatibility = 1.8 +targetCompatibility = 1.8 [compileJava, compileTestJava]*.options*.encoding = 'UTF-8' repositories { - jcenter() + mavenCentral() } dependencies { - testCompile 'junit:junit:4.12' + testImplementation 'junit:junit:4.13' } -jar { - manifest.attributes += [ - 'github': 'https://github.com/bladecoder/blade-ink/', - 'license': 'Apache-2.0', - 'group': project.group, - 'version': project.version, - 'java': targetCompatibility, - 'timestamp': System.currentTimeMillis() - ] +if (!hasProperty("release") && !version.endsWith("-SNAPSHOT")) { + version += "-SNAPSHOT" } -javadoc { - title = "Blade Ink" - options { - memberLevel = JavadocMemberLevel.PUBLIC - author true - setUse true - encoding "UTF-8" - } +// DISABLES JAVADOC ULTRACHECKS IN JDK8 +if (JavaVersion.current().isJava8Compatible()) { + allprojects { + tasks.withType(Javadoc) { + options.addStringOption('Xdoclint:none', '-quiet') + } + } } - -task enginedocJar(type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' - from 'build/docs/javadoc' +spotless { + java { + target fileTree('src') { + include '**/*.java' + } + toggleOffOn() + palantirJavaFormat() + removeUnusedImports() + trimTrailingWhitespace() + endWithNewline() + } } - -task sourcesJar(type: Jar) { - from sourceSets.main.allSource - classifier = 'sources' +jar { + manifest.attributes += [ + 'github' : 'https://github.com/bladecoder/blade-ink/', + 'license' : 'Apache-2.0', + 'group' : project.group, + 'version' : project.version, + 'java' : targetCompatibility, + 'timestamp': System.currentTimeMillis() + ] } -artifacts { - //archives jar - archives enginedocJar - archives sourcesJar +javadoc { + title = "Blade Ink" + options { + memberLevel = JavadocMemberLevel.PUBLIC + author true + setUse true + encoding "UTF-8" + } } -def isDevBuild -def isCiBuild -def isReleaseBuild - -def sonatypeRepositoryUrl - -//set build variables based on build type (release, continuous integration, development) -if(hasProperty("release")) { - isReleaseBuild = true - sonatypeRepositoryUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" -} else if (hasProperty("ci")) { - isCiBuild = true - //version += "-SNAPSHOT" - sonatypeRepositoryUrl = "https://oss.sonatype.org/content/repositories/snapshots/" -} else { - isDevBuild = true +task sourcesJar(type: Jar) { + from sourceSets.main.allJava + archiveClassifier = 'sources' } -//********* artifact signing ********* -if(isReleaseBuild) { - signing { - sign configurations.archives - } -} else { - task signArchives { - // do nothing - } +task javadocJar(type: Jar) { + from javadoc + archiveClassifier = 'javadoc' } -uploadArchives { - repositories { - if (isDevBuild) { - mavenLocal() - } - else { - mavenDeployer { - if(isReleaseBuild) { - beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } +publishing { + publications { + mavenJava(MavenPublication) { + artifactId = 'blade-ink' + from components.java + artifact sourcesJar + artifact javadocJar + + versionMapping { + usage('java-api') { + fromResolutionOf('runtimeClasspath') } - - repository(url: sonatypeRepositoryUrl) { - authentication(userName: sonatypeUsername, password: sonatypePassword) + usage('java-runtime') { + fromResolutionResult() } + } + + pom { + name = 'blade-ink' + description = "This is a Java port of inkle's ink, a scripting language for writing interactive narrative." + url = 'https://github.com/bladecoder/blade-ink' - pom.project { - name 'blade-ink' - packaging 'jar' - description "This is a Java port of inkle's ink, a scripting language for writing interactive narrative." - url 'https://github.com/bladecoder/blade-ink' - - scm { - url 'scm:git@github.com:bladecoder/blade-ink.git' - connection 'scm:git@github.com:bladecoder/blade-ink.git' - developerConnection 'scm:git@github.com:bladecoder/blade-ink.git' - } - - licenses { - license { - name 'The Apache Software License, Version 2.0' - url 'http://www.apache.org/licenses/LICENSE-2.0.txt' - distribution 'repo' - } - } - - developers { - developer { - id 'rgarcia' - name 'Rafael Garcia' - } - } - } + licenses { + license { + name = 'The Apache License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + developers { + developer { + id = 'bladecoder' + name = 'Rafael Garcia' + email = 'bladecoder@gmail.com' + } + } + scm { + connection = 'scm:git@github.com:bladecoder/blade-ink.git' + developerConnection = 'scm:git@github.com:bladecoder/blade-ink.git' + url = 'scm:git@github.com:bladecoder/blade-ink.git' + } } } } + repositories { + maven { + def releasesRepoUrl = "https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/" + def snapshotsRepoUrl = "https://central.sonatype.com/repository/maven-snapshots/" + url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl + credentials { + username project.hasProperty('NEXUS_USERNAME') ? NEXUS_USERNAME : "$System.env.NEXUS_USERNAME" + password project.hasProperty('NEXUS_PASSWORD') ? NEXUS_PASSWORD : "$System.env.NEXUS_PASSWORD" + } + } + } +} + +signing { + if (!version.endsWith('SNAPSHOT')) { + useGpgCmd() + sign publishing.publications.mavenJava + } +} + +javadoc { + if (JavaVersion.current().isJava9Compatible()) { + options.addBooleanOption('html5', true) + } } diff --git a/gradle.properties b/gradle.properties index 8549450..4fb0ceb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,2 @@ -# Matching Ink v0.9.0 -version=0.7.4 - +# Matching Ink v1.2.0 +version=1.2.2 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f1bef0f..fd73fc2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip diff --git a/src/main/java/com/bladecoder/ink/runtime/AbstractValue.java b/src/main/java/com/bladecoder/ink/runtime/AbstractValue.java index 0842ce5..2212098 100644 --- a/src/main/java/com/bladecoder/ink/runtime/AbstractValue.java +++ b/src/main/java/com/bladecoder/ink/runtime/AbstractValue.java @@ -1,52 +1,49 @@ -package com.bladecoder.ink.runtime; - -public abstract class AbstractValue extends RTObject { - public abstract ValueType getValueType(); - - public abstract boolean isTruthy() throws Exception; - - public abstract AbstractValue cast(ValueType newType) throws Exception; - - public abstract Object getValueObject(); - - public static AbstractValue create(Object val) { - // Implicitly lose precision from any doubles we get passed in - if (val instanceof Double) { - double doub = (Double) val; - val = (float) doub; - } - - // Implicitly convert bools into ints - if (val instanceof Boolean) { - boolean b = (Boolean) val; - val = (int) (b ? 1 : 0); - } - - if (val instanceof Integer) { - return new IntValue((Integer) val); - } else if (val instanceof Long) { - return new IntValue(((Long) val).intValue()); - } else if (val instanceof Float) { - return new FloatValue((Float) val); - } else if (val instanceof Double) { - return new FloatValue((((Double) val).floatValue())); - } else if (val instanceof String) { - return new StringValue((String) val); - } else if (val instanceof Path) { - return new DivertTargetValue((Path) val); - } else if (val instanceof InkList) { - return new ListValue((InkList) val); - } - - return null; - } - - @Override - RTObject copy() { - return create(getValueObject()); - } - - protected StoryException BadCastException(ValueType targetType) throws Exception { - return new StoryException("Can't cast " + this.getValueObject() + " from " + this.getValueType() + " to " + targetType); - } -} +package com.bladecoder.ink.runtime; + +public abstract class AbstractValue extends RTObject { + public abstract ValueType getValueType(); + + public abstract boolean isTruthy() throws Exception; + + public abstract AbstractValue cast(ValueType newType) throws Exception; + + public abstract Object getValueObject(); + + public static AbstractValue create(Object val) { + // Implicitly lose precision from any doubles we get passed in + if (val instanceof Double) { + double doub = (Double) val; + val = (float) doub; + } + + if (val instanceof Boolean) { + return new BoolValue((Boolean) val); + } else if (val instanceof Integer) { + return new IntValue((Integer) val); + } else if (val instanceof Long) { + return new IntValue(((Long) val).intValue()); + } else if (val instanceof Float) { + return new FloatValue((Float) val); + } else if (val instanceof Double) { + return new FloatValue((((Double) val).floatValue())); + } else if (val instanceof String) { + return new StringValue((String) val); + } else if (val instanceof Path) { + return new DivertTargetValue((Path) val); + } else if (val instanceof InkList) { + return new ListValue((InkList) val); + } + + return null; + } + + @Override + RTObject copy() { + return create(getValueObject()); + } + + protected StoryException BadCastException(ValueType targetType) throws Exception { + return new StoryException( + "Can't cast " + this.getValueObject() + " from " + this.getValueType() + " to " + targetType); + } +} diff --git a/src/main/java/com/bladecoder/ink/runtime/BoolValue.java b/src/main/java/com/bladecoder/ink/runtime/BoolValue.java new file mode 100644 index 0000000..0542f6c --- /dev/null +++ b/src/main/java/com/bladecoder/ink/runtime/BoolValue.java @@ -0,0 +1,42 @@ +package com.bladecoder.ink.runtime; + +class BoolValue extends Value { + public BoolValue() { + this(false); + } + + public BoolValue(boolean boolVal) { + super(boolVal); + } + + @Override + public AbstractValue cast(ValueType newType) throws Exception { + if (newType == getValueType()) { + return this; + } + + if (newType == ValueType.Int) { + return new IntValue(value ? 1 : 0); + } + + if (newType == ValueType.Float) { + return new FloatValue(value ? 1f : 0f); + } + + if (newType == ValueType.String) { + return new StringValue(value.toString()); + } + + throw BadCastException(newType); + } + + @Override + public boolean isTruthy() { + return value; + } + + @Override + public ValueType getValueType() { + return ValueType.Bool; + } +} diff --git a/src/main/java/com/bladecoder/ink/runtime/CallStack.java b/src/main/java/com/bladecoder/ink/runtime/CallStack.java index 77c0036..6be1591 100644 --- a/src/main/java/com/bladecoder/ink/runtime/CallStack.java +++ b/src/main/java/com/bladecoder/ink/runtime/CallStack.java @@ -1,439 +1,440 @@ package com.bladecoder.ink.runtime; +import com.bladecoder.ink.runtime.SimpleJson.InnerWriter; +import com.bladecoder.ink.runtime.SimpleJson.Writer; import java.util.ArrayList; import java.util.HashMap; import java.util.List; -import com.bladecoder.ink.runtime.SimpleJson.InnerWriter; -import com.bladecoder.ink.runtime.SimpleJson.Writer; +public class CallStack { + public static class Element { + public final Pointer currentPointer = new Pointer(); + + public boolean inExpressionEvaluation; + public HashMap temporaryVariables; + + public PushPopType type; + + // When this callstack element is actually a function evaluation called from the + // game, + // we need to keep track of the size of the evaluation stack when it was called + // so that we know whether there was any return value. + public int evaluationStackHeightWhenPushed; + + // When functions are called, we trim whitespace from the start and end of what + // they generate, so we make sure know where the function's start and end are. + public int functionStartInOuputStream; + + public Element(PushPopType type, Pointer pointer) { + this(type, pointer, false); + } + + public Element(PushPopType type, Pointer pointer, boolean inExpressionEvaluation) { + this.currentPointer.assign(pointer); + + this.inExpressionEvaluation = inExpressionEvaluation; + this.temporaryVariables = new HashMap<>(); + this.type = type; + } + + public Element copy() { + Element copy = new Element(this.type, currentPointer, this.inExpressionEvaluation); + copy.temporaryVariables = new HashMap<>(this.temporaryVariables); + copy.evaluationStackHeightWhenPushed = evaluationStackHeightWhenPushed; + copy.functionStartInOuputStream = functionStartInOuputStream; + return copy; + } + } + + public static class Thread { + public List callstack; + public final Pointer previousPointer = new Pointer(); + public int threadIndex; + + public Thread() { + callstack = new ArrayList<>(); + } + + @SuppressWarnings("unchecked") + public Thread(HashMap jThreadObj, Story storyContext) throws Exception { + this(); + threadIndex = (int) jThreadObj.get("threadIndex"); + + List jThreadCallstack = (List) jThreadObj.get("callstack"); + + for (Object jElTok : jThreadCallstack) { + + HashMap jElementObj = (HashMap) jElTok; + + PushPopType pushPopType = PushPopType.values()[(Integer) jElementObj.get("type")]; + + final Pointer pointer = new Pointer(Pointer.Null); + + String currentContainerPathStr = null; + Object currentContainerPathStrToken = jElementObj.get("cPath"); + if (currentContainerPathStrToken != null) { + currentContainerPathStr = currentContainerPathStrToken.toString(); + final SearchResult threadPointerResult = + storyContext.contentAtPath(new Path(currentContainerPathStr)); + pointer.container = threadPointerResult.getContainer(); + pointer.index = (int) jElementObj.get("idx"); + + if (threadPointerResult.obj == null) { + throw new Exception("When loading state, internal story location couldn't be found: " + + currentContainerPathStr + + ". Has the story changed since this save data was created?"); + } else if (threadPointerResult.approximate) { + if (pointer.container != null) { + storyContext.warning( + "When loading state, exact internal story location couldn't be found: '" + + currentContainerPathStr + "', so it was approximated to '" + + pointer.container.getPath().toString() + + "' to recover. Has the story changed since this save data was created?"); + } else { + storyContext.warning( + "When loading state, exact internal story location couldn't be found: '" + + currentContainerPathStr + + "' and it may not be recoverable. Has the story changed since this save data was created?"); + } + } + } + + boolean inExpressionEvaluation = (boolean) jElementObj.get("exp"); + + Element el = new Element(pushPopType, pointer, inExpressionEvaluation); + + Object temps = jElementObj.get("temp"); + if (temps != null) { + el.temporaryVariables = Json.jObjectToHashMapRuntimeObjs((HashMap) temps); + } else { + el.temporaryVariables.clear(); + } + + callstack.add(el); + } + + Object prevContentObjPath = jThreadObj.get("previousContentObject"); + if (prevContentObjPath != null) { + Path prevPath = new Path((String) prevContentObjPath); + previousPointer.assign(storyContext.pointerAtPath(prevPath)); + } + } + + public Thread copy() { + Thread copy = new Thread(); + copy.threadIndex = threadIndex; + for (Element e : callstack) { + copy.callstack.add(e.copy()); + } + copy.previousPointer.assign(previousPointer); + return copy; + } + + public void writeJson(SimpleJson.Writer writer) throws Exception { + writer.writeObjectStart(); + + // callstack + writer.writePropertyStart("callstack"); + writer.writeArrayStart(); + for (CallStack.Element el : callstack) { + writer.writeObjectStart(); + if (!el.currentPointer.isNull()) { + writer.writeProperty( + "cPath", el.currentPointer.container.getPath().getComponentsString()); + writer.writeProperty("idx", el.currentPointer.index); + } + + writer.writeProperty("exp", el.inExpressionEvaluation); + writer.writeProperty("type", el.type.ordinal()); + + if (el.temporaryVariables.size() > 0) { + writer.writePropertyStart("temp"); + Json.writeDictionaryRuntimeObjs(writer, el.temporaryVariables); + writer.writePropertyEnd(); + } + + writer.writeObjectEnd(); + } + writer.writeArrayEnd(); + writer.writePropertyEnd(); + + // threadIndex + writer.writeProperty("threadIndex", threadIndex); + + if (!previousPointer.isNull()) { + writer.writeProperty( + "previousContentObject", + previousPointer.resolve().getPath().toString()); + } + + writer.writeObjectEnd(); + } + } + + private int threadCounter; + private final Pointer startOfRoot = new Pointer(); + + private List threads; + + public CallStack(CallStack toCopy) { + threads = new ArrayList<>(); + for (Thread otherThread : toCopy.threads) { + threads.add(otherThread.copy()); + } + + threadCounter = toCopy.threadCounter; + startOfRoot.assign(toCopy.startOfRoot); + } + + public CallStack(Story storyContext) { + startOfRoot.assign(Pointer.startOf(storyContext.getMainContentContainer())); + + reset(); + } + + public void reset() { + threads = new ArrayList<>(); + threads.add(new Thread()); + + threads.get(0).callstack.add(new Element(PushPopType.Tunnel, startOfRoot)); + } + + public boolean canPop() { + return getCallStack().size() > 1; + } + + public boolean canPop(PushPopType type) { + + if (!canPop()) return false; + + if (type == null) return true; + + return getCurrentElement().type == type; + } + + public boolean canPopThread() { + return threads.size() > 1 && !elementIsEvaluateFromGame(); + } + + public boolean elementIsEvaluateFromGame() { + return getCurrentElement().type == PushPopType.FunctionEvaluationFromGame; + } + + // Find the most appropriate context for this variable. + // Are we referencing a temporary or global variable? + // Note that the compiler will have warned us about possible conflicts, + // so anything that happens here should be safe! + public int contextForVariableNamed(String name) { + // Current temporary context? + // (Shouldn't attempt to access contexts higher in the callstack.) + if (getCurrentElement().temporaryVariables.containsKey(name)) { + return getCurrentElementIndex() + 1; + } + + // Global + else { + return 0; + } + } + + public int getDepth() { + return getElements().size(); + } + + public Element getCurrentElement() { + Thread thread = threads.get(threads.size() - 1); + List cs = thread.callstack; + return cs.get(cs.size() - 1); + } + + public int getCurrentElementIndex() { + return getCallStack().size() - 1; + } + + private List getCallStack() { + return getcurrentThread().callstack; + } + + public Thread getcurrentThread() { + return threads.get(threads.size() - 1); + } + + // + public List getElements() { + return getCallStack(); + } + + public void writeJson(SimpleJson.Writer w) throws Exception { + w.writeObject(new InnerWriter() { + + @Override + public void write(Writer writer) throws Exception { + writer.writePropertyStart("threads"); + writer.writeArrayStart(); + + for (CallStack.Thread thread : threads) { + thread.writeJson(writer); + } + writer.writeArrayEnd(); + writer.writePropertyEnd(); + + writer.writePropertyStart("threadCounter"); + writer.write(threadCounter); + writer.writePropertyEnd(); + } + }); + } + + public RTObject getTemporaryVariableWithName(String name) { + return getTemporaryVariableWithName(name, -1); + } + + // Get variable value, dereferencing a variable pointer if necessary + public RTObject getTemporaryVariableWithName(String name, int contextIndex) { + // contextIndex 0 means global, so index is actually 1-based + if (contextIndex == -1) contextIndex = getCurrentElementIndex() + 1; + + Element contextElement = getCallStack().get(contextIndex - 1); + RTObject varValue = contextElement.temporaryVariables.get(name); + + return varValue; + } + + public void pop() throws Exception { + pop(null); + } + + public void pop(PushPopType type) throws Exception { + if (canPop(type)) { + getCallStack().remove(getCallStack().size() - 1); + return; + } else { + throw new Exception("Mismatched push/pop in Callstack"); + } + } + + public void popThread() throws Exception { + if (canPopThread()) { + threads.remove(getcurrentThread()); + } else { + throw new Exception("Can't pop thread"); + } + } + + public void push(PushPopType type) { + push(type, 0, 0); + } + + public void push(PushPopType type, int externalEvaluationStackHeight) { + push(type, externalEvaluationStackHeight, 0); + } + + public void push(PushPopType type, int externalEvaluationStackHeight, int outputStreamLengthWithPushed) { + // When pushing to callstack, maintain the current content path, but + // jump + // out of expressions by default + Element element = new Element(type, getCurrentElement().currentPointer, false); + + element.evaluationStackHeightWhenPushed = externalEvaluationStackHeight; + element.functionStartInOuputStream = outputStreamLengthWithPushed; + + getCallStack().add(element); + } + + public void pushThread() { + Thread newThread = getcurrentThread().copy(); + threadCounter++; + newThread.threadIndex = threadCounter; + threads.add(newThread); + } + + public void setCurrentThread(Thread value) { + // Debug.Assert (threads.Count == 1, "Shouldn't be directly setting the + // current thread when we have a stack of them"); + threads.clear(); + threads.add(value); + } + + // Unfortunately it's not possible to implement jsonToken since + // the setter needs to take a Story as a context in order to + // look up RTObjects from paths for currentContainer within elements. + @SuppressWarnings("unchecked") + public void setJsonToken(HashMap jRTObject, Story storyContext) throws Exception { + threads.clear(); + + List jThreads = (List) jRTObject.get("threads"); + + for (Object jThreadTok : jThreads) { + HashMap jThreadObj = (HashMap) jThreadTok; + Thread thread = new Thread(jThreadObj, storyContext); + threads.add(thread); + } + + threadCounter = (int) jRTObject.get("threadCounter"); + startOfRoot.assign(Pointer.startOf(storyContext.getMainContentContainer())); + } + + public Thread forkThread() { + Thread forkedThread = getcurrentThread().copy(); + threadCounter++; + forkedThread.threadIndex = threadCounter; + return forkedThread; + } + + public void setTemporaryVariable(String name, RTObject value, boolean declareNew) { + setTemporaryVariable(name, value, declareNew); + } + + public void setTemporaryVariable(String name, RTObject value, boolean declareNew, int contextIndex) + throws Exception { + if (contextIndex == -1) contextIndex = getCurrentElementIndex() + 1; + + Element contextElement = getCallStack().get(contextIndex - 1); + + if (!declareNew && !contextElement.temporaryVariables.containsKey(name)) { + throw new Exception("Could not find temporary variable to set: " + name); + } + + RTObject oldValue = contextElement.temporaryVariables.get(name); + + if (oldValue != null) ListValue.retainListOriginsForAssignment(oldValue, value); + + contextElement.temporaryVariables.put(name, value); + } + + public Thread getThreadWithIndex(int index) { + // return threads.Find (t => t.threadIndex == index); + + for (Thread t : threads) if (t.threadIndex == index) return t; + + return null; + } + + String getCallStackTrace() { + StringBuilder sb = new StringBuilder(); + + for (int t = 0; t < threads.size(); t++) { + + Thread thread = threads.get(t); + boolean isCurrent = (t == threads.size() - 1); + sb.append(String.format( + "=== THREAD %d/%d %s===\n", (t + 1), threads.size(), (isCurrent ? "(current) " : ""))); + + for (int i = 0; i < thread.callstack.size(); i++) { + + if (thread.callstack.get(i).type == PushPopType.Function) sb.append(" [FUNCTION] "); + else sb.append(" [TUNNEL] "); + + final Pointer pointer = new Pointer(); + pointer.assign(thread.callstack.get(i).currentPointer); + if (!pointer.isNull()) { + sb.append("\n"); + } + } + } -class CallStack { - static class Element { - public final Pointer currentPointer = new Pointer(); - - public boolean inExpressionEvaluation; - public HashMap temporaryVariables; - - public PushPopType type; - - // When this callstack element is actually a function evaluation called from the - // game, - // we need to keep track of the size of the evaluation stack when it was called - // so that we know whether there was any return value. - public int evaluationStackHeightWhenPushed; - - // When functions are called, we trim whitespace from the start and end of what - // they generate, so we make sure know where the function's start and end are. - public int functionStartInOuputStream; - - public Element(PushPopType type, Pointer pointer) { - this(type, pointer, false); - } - - public Element(PushPopType type, Pointer pointer, boolean inExpressionEvaluation) { - this.currentPointer.assign(pointer); - - this.inExpressionEvaluation = inExpressionEvaluation; - this.temporaryVariables = new HashMap<>(); - this.type = type; - } - - public Element copy() { - Element copy = new Element(this.type, currentPointer, this.inExpressionEvaluation); - copy.temporaryVariables = new HashMap<>(this.temporaryVariables); - copy.evaluationStackHeightWhenPushed = evaluationStackHeightWhenPushed; - copy.functionStartInOuputStream = functionStartInOuputStream; - return copy; - } - } - - static class Thread { - public List callstack; - public final Pointer previousPointer = new Pointer(); - public int threadIndex; - - public Thread() { - callstack = new ArrayList<>(); - } - - @SuppressWarnings("unchecked") - public Thread(HashMap jThreadObj, Story storyContext) throws Exception { - this(); - threadIndex = (int) jThreadObj.get("threadIndex"); - - List jThreadCallstack = (List) jThreadObj.get("callstack"); - - for (Object jElTok : jThreadCallstack) { - - HashMap jElementObj = (HashMap) jElTok; - - PushPopType pushPopType = PushPopType.values()[(Integer) jElementObj.get("type")]; - - final Pointer pointer = new Pointer(Pointer.Null); - - String currentContainerPathStr = null; - Object currentContainerPathStrToken = jElementObj.get("cPath"); - if (currentContainerPathStrToken != null) { - currentContainerPathStr = currentContainerPathStrToken.toString(); - final SearchResult threadPointerResult = storyContext - .contentAtPath(new Path(currentContainerPathStr)); - pointer.container = threadPointerResult.getContainer(); - pointer.index = (int) jElementObj.get("idx"); - - if (threadPointerResult.obj == null) - throw new Exception("When loading state, internal story location couldn't be found: " - + currentContainerPathStr - + ". Has the story changed since this save data was created?"); - else if (threadPointerResult.approximate) - storyContext.warning("When loading state, exact internal story location couldn't be found: '" - + currentContainerPathStr + "', so it was approximated to '" - + pointer.container.getPath().toString() - + "' to recover. Has the story changed since this save data was created?"); - } - - boolean inExpressionEvaluation = (boolean) jElementObj.get("exp"); - - Element el = new Element(pushPopType, pointer, inExpressionEvaluation); - - Object temps = jElementObj.get("temp"); - if (temps != null) { - el.temporaryVariables = Json.jObjectToHashMapRuntimeObjs((HashMap) temps); - } else { - el.temporaryVariables.clear(); - } - - callstack.add(el); - } - - Object prevContentObjPath = jThreadObj.get("previousContentObject"); - if (prevContentObjPath != null) { - Path prevPath = new Path((String) prevContentObjPath); - previousPointer.assign(storyContext.pointerAtPath(prevPath)); - } - } - - public Thread copy() { - Thread copy = new Thread(); - copy.threadIndex = threadIndex; - for (Element e : callstack) { - copy.callstack.add(e.copy()); - } - copy.previousPointer.assign(previousPointer); - return copy; - } - - public void writeJson(SimpleJson.Writer writer) throws Exception { - writer.writeObjectStart(); - - // callstack - writer.writePropertyStart("callstack"); - writer.writeArrayStart(); - for (CallStack.Element el : callstack) { - writer.writeObjectStart(); - if (!el.currentPointer.isNull()) { - writer.writeProperty("cPath", el.currentPointer.container.getPath().getComponentsString()); - writer.writeProperty("idx", el.currentPointer.index); - } - - writer.writeProperty("exp", el.inExpressionEvaluation); - writer.writeProperty("type", el.type.ordinal()); - - if (el.temporaryVariables.size() > 0) { - writer.writePropertyStart("temp"); - Json.writeDictionaryRuntimeObjs(writer, el.temporaryVariables); - writer.writePropertyEnd(); - } - - writer.writeObjectEnd(); - } - writer.writeArrayEnd(); - writer.writePropertyEnd(); - - // threadIndex - writer.writeProperty("threadIndex", threadIndex); - - if (!previousPointer.isNull()) { - writer.writeProperty("previousContentObject", previousPointer.resolve().getPath().toString()); - } - - writer.writeObjectEnd(); - } - } - - private int threadCounter; - final private Pointer startOfRoot = new Pointer(); - - private List threads; - - public CallStack(CallStack toCopy) { - threads = new ArrayList<>(); - for (Thread otherThread : toCopy.threads) { - threads.add(otherThread.copy()); - } - - threadCounter = toCopy.threadCounter; - startOfRoot.assign(toCopy.startOfRoot); - } - - public CallStack(Story storyContext) { - startOfRoot.assign(Pointer.startOf(storyContext.getRootContentContainer())); - - reset(); - } - - public void reset() { - threads = new ArrayList<>(); - threads.add(new Thread()); - - threads.get(0).callstack.add(new Element(PushPopType.Tunnel, startOfRoot)); - } - - public boolean canPop() { - return getCallStack().size() > 1; - } - - public boolean canPop(PushPopType type) { - - if (!canPop()) - return false; - - if (type == null) - return true; - - return getCurrentElement().type == type; - } - - public boolean canPopThread() { - return threads.size() > 1 && !elementIsEvaluateFromGame(); - } - - public boolean elementIsEvaluateFromGame() { - return getCurrentElement().type == PushPopType.FunctionEvaluationFromGame; - } - - // Find the most appropriate context for this variable. - // Are we referencing a temporary or global variable? - // Note that the compiler will have warned us about possible conflicts, - // so anything that happens here should be safe! - public int contextForVariableNamed(String name) { - // Current temporary context? - // (Shouldn't attempt to access contexts higher in the callstack.) - if (getCurrentElement().temporaryVariables.containsKey(name)) { - return getCurrentElementIndex() + 1; - } - - // Global - else { - return 0; - } - } - - public int getDepth() { - return getElements().size(); - } - - public Element getCurrentElement() { - Thread thread = threads.get(threads.size() - 1); - List cs = thread.callstack; - return cs.get(cs.size() - 1); - } - - public int getCurrentElementIndex() { - return getCallStack().size() - 1; - } - - private List getCallStack() { - return getcurrentThread().callstack; - } - - public Thread getcurrentThread() { - return threads.get(threads.size() - 1); - } - - // - public List getElements() { - return getCallStack(); - } - - public void writeJson(SimpleJson.Writer w) throws Exception { - w.writeObject(new InnerWriter() { - - @Override - public void write(Writer writer) throws Exception { - writer.writePropertyStart("threads"); - writer.writeArrayStart(); - - for (CallStack.Thread thread : threads) { - thread.writeJson(writer); - } - writer.writeArrayEnd(); - writer.writePropertyEnd(); - - writer.writePropertyStart("threadCounter"); - writer.write(threadCounter); - writer.writePropertyEnd(); - } - - }); - - } - - public RTObject getTemporaryVariableWithName(String name) { - return getTemporaryVariableWithName(name, -1); - } - - // Get variable value, dereferencing a variable pointer if necessary - public RTObject getTemporaryVariableWithName(String name, int contextIndex) { - if (contextIndex == -1) - contextIndex = getCurrentElementIndex() + 1; - - Element contextElement = getCallStack().get(contextIndex - 1); - RTObject varValue = contextElement.temporaryVariables.get(name); - - return varValue; - } - - public void pop() throws Exception { - pop(null); - } - - public void pop(PushPopType type) throws Exception { - if (canPop(type)) { - getCallStack().remove(getCallStack().size() - 1); - return; - } else { - throw new Exception("Mismatched push/pop in Callstack"); - } - } - - public void popThread() throws Exception { - if (canPopThread()) { - threads.remove(getcurrentThread()); - } else { - throw new Exception("Can't pop thread"); - } - } - - public void push(PushPopType type) { - push(type, 0, 0); - } - - public void push(PushPopType type, int externalEvaluationStackHeight) { - push(type, externalEvaluationStackHeight, 0); - } - - public void push(PushPopType type, int externalEvaluationStackHeight, int outputStreamLengthWithPushed) { - // When pushing to callstack, maintain the current content path, but - // jump - // out of expressions by default - Element element = new Element(type, getCurrentElement().currentPointer, false); - - element.evaluationStackHeightWhenPushed = externalEvaluationStackHeight; - element.functionStartInOuputStream = outputStreamLengthWithPushed; - - getCallStack().add(element); - } - - public void pushThread() { - Thread newThread = getcurrentThread().copy(); - threadCounter++; - newThread.threadIndex = threadCounter; - threads.add(newThread); - } - - public void setCurrentThread(Thread value) { - // Debug.Assert (threads.Count == 1, "Shouldn't be directly setting the - // current thread when we have a stack of them"); - threads.clear(); - threads.add(value); - } - - // Unfortunately it's not possible to implement jsonToken since - // the setter needs to take a Story as a context in order to - // look up RTObjects from paths for currentContainer within elements. - @SuppressWarnings("unchecked") - public void setJsonToken(HashMap jRTObject, Story storyContext) throws Exception { - threads.clear(); - - List jThreads = (List) jRTObject.get("threads"); - - for (Object jThreadTok : jThreads) { - HashMap jThreadObj = (HashMap) jThreadTok; - Thread thread = new Thread(jThreadObj, storyContext); - threads.add(thread); - } - - threadCounter = (int) jRTObject.get("threadCounter"); - startOfRoot.assign(Pointer.startOf(storyContext.getRootContentContainer())); - } - - public Thread forkThread() { - Thread forkedThread = getcurrentThread().copy(); - threadCounter++; - forkedThread.threadIndex = threadCounter; - return forkedThread; - } - - public void setTemporaryVariable(String name, RTObject value, boolean declareNew) { - setTemporaryVariable(name, value, declareNew); - } - - public void setTemporaryVariable(String name, RTObject value, boolean declareNew, int contextIndex) - throws StoryException, Exception { - if (contextIndex == -1) - contextIndex = getCurrentElementIndex() + 1; - - Element contextElement = getCallStack().get(contextIndex - 1); - - if (!declareNew && !contextElement.temporaryVariables.containsKey(name)) { - throw new StoryException("Could not find temporary variable to set: " + name); - } - - RTObject oldValue = contextElement.temporaryVariables.get(name); - - if (oldValue != null) - ListValue.retainListOriginsForAssignment(oldValue, value); - - contextElement.temporaryVariables.put(name, value); - } - - public Thread getThreadWithIndex(int index) { - // return threads.Find (t => t.threadIndex == index); - - for (Thread t : threads) - if (t.threadIndex == index) - return t; - - return null; - } - - String getCallStackTrace() { - StringBuilder sb = new StringBuilder(); - - for (int t = 0; t < threads.size(); t++) { - - Thread thread = threads.get(t); - boolean isCurrent = (t == threads.size() - 1); - sb.append(String.format("=== THREAD %d/%d %s===\n", (t + 1), threads.size(), - (isCurrent ? "(current) " : ""))); - - for (int i = 0; i < thread.callstack.size(); i++) { - - if (thread.callstack.get(i).type == PushPopType.Function) - sb.append(" [FUNCTION] "); - else - sb.append(" [TUNNEL] "); - - final Pointer pointer = new Pointer(); - pointer.assign(thread.callstack.get(i).currentPointer); - if (!pointer.isNull()) { - sb.append("\n"); - } - } - } - - return sb.toString(); - } + return sb.toString(); + } } diff --git a/src/main/java/com/bladecoder/ink/runtime/Choice.java b/src/main/java/com/bladecoder/ink/runtime/Choice.java index 18d09b1..2297283 100644 --- a/src/main/java/com/bladecoder/ink/runtime/Choice.java +++ b/src/main/java/com/bladecoder/ink/runtime/Choice.java @@ -1,68 +1,87 @@ -package com.bladecoder.ink.runtime; - -/** - * 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; - } - -} +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; + + 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() {} + + 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; + } + + public Choice clone() { + Choice copy = new Choice(); + copy.text = text; + copy.sourcePath = sourcePath; + copy.index = index; + copy.targetPath = targetPath; + copy.originalThreadIndex = originalThreadIndex; + copy.isInvisibleDefault = isInvisibleDefault; + if (threadAtGeneration != null) copy.threadAtGeneration = threadAtGeneration.copy(); + return copy; + } +} diff --git a/src/main/java/com/bladecoder/ink/runtime/ChoicePoint.java b/src/main/java/com/bladecoder/ink/runtime/ChoicePoint.java index 0dd4007..7924de5 100644 --- a/src/main/java/com/bladecoder/ink/runtime/ChoicePoint.java +++ b/src/main/java/com/bladecoder/ink/runtime/ChoicePoint.java @@ -1,142 +1,135 @@ -package com.bladecoder.ink.runtime; - -/** - * The ChoicePoint represents the point within the Story where a Choice instance - * gets generated. The distinction is made because the text of the Choice can be - * dynamically generated. - */ -public class ChoicePoint extends RTObject { - private boolean hasChoiceOnlyContent; - - private boolean hasStartContent; - - private boolean isInvisibleDefault; - - private boolean onceOnly; - - private boolean hasCondition; - - private Path pathOnChoice; - - public ChoicePoint() throws Exception { - this(true); - } - - public ChoicePoint(boolean onceOnly) { - this.setOnceOnly(onceOnly); - } - - public Container getChoiceTarget() throws Exception { - return resolvePath(pathOnChoice).getContainer(); - } - - public int getFlags() { - int flags = 0; - if (hasCondition()) - flags |= 1; - - if (hasStartContent()) - flags |= 2; - - if (hasChoiceOnlyContent()) - flags |= 4; - - if (isInvisibleDefault()) - flags |= 8; - - if (isOnceOnly()) - flags |= 16; - - return flags; - } - - public boolean hasChoiceOnlyContent() { - return hasChoiceOnlyContent; - } - - public boolean hasCondition() { - return hasCondition; - } - - public boolean hasStartContent() { - return hasStartContent; - } - - public boolean isInvisibleDefault() { - return isInvisibleDefault; - } - - public boolean isOnceOnly() { - return onceOnly; - } - - public Path getPathOnChoice() throws Exception { - // Resolve any relative paths to global ones as we come across them - if (pathOnChoice != null && pathOnChoice.isRelative()) { - Container choiceTargetObj = getChoiceTarget(); - if (choiceTargetObj != null) { - pathOnChoice = choiceTargetObj.getPath(); - } - } - return pathOnChoice; - } - - public String getPathStringOnChoice() throws Exception { - return compactPathString(getPathOnChoice()); - } - - public void setFlags(int value) { - setHasCondition((value & 1) > 0); - setHasStartContent((value & 2) > 0); - setHasChoiceOnlyContent((value & 4) > 0); - setIsInvisibleDefault((value & 8) > 0); - setOnceOnly((value & 16) > 0); - } - - public void setHasChoiceOnlyContent(boolean value) { - hasChoiceOnlyContent = value; - } - - public void setHasCondition(boolean value) { - hasCondition = value; - } - - public void setHasStartContent(boolean value) { - hasStartContent = value; - } - - public void setIsInvisibleDefault(boolean value) { - isInvisibleDefault = value; - } - - public void setOnceOnly(boolean value) { - onceOnly = value; - } - - public void setPathOnChoice(Path value) { - pathOnChoice = value; - } - - public void setPathStringOnChoice(String value) { - setPathOnChoice(new Path(value)); - } - - @Override - public String toString() { - try { - Integer targetLineNum = debugLineNumberOfPath(getPathOnChoice()); - - String targetString = getPathOnChoice().toString(); - - if (targetLineNum != null) { - targetString = " line " + targetLineNum + "("+targetString+")"; - } - - return "Choice: -> " + targetString; - } catch (Exception e) { - throw new RuntimeException(e); - } - - } - -} +package com.bladecoder.ink.runtime; + +/** + * The ChoicePoint represents the point within the Story where a Choice instance + * gets generated. The distinction is made because the text of the Choice can be + * dynamically generated. + */ +public class ChoicePoint extends RTObject { + private boolean hasChoiceOnlyContent; + + private boolean hasStartContent; + + private boolean isInvisibleDefault; + + private boolean onceOnly; + + private boolean hasCondition; + + private Path pathOnChoice; + + public ChoicePoint() throws Exception { + this(true); + } + + public ChoicePoint(boolean onceOnly) { + this.setOnceOnly(onceOnly); + } + + public Container getChoiceTarget() throws Exception { + return resolvePath(pathOnChoice).getContainer(); + } + + public int getFlags() { + int flags = 0; + if (hasCondition()) flags |= 1; + + if (hasStartContent()) flags |= 2; + + if (hasChoiceOnlyContent()) flags |= 4; + + if (isInvisibleDefault()) flags |= 8; + + if (isOnceOnly()) flags |= 16; + + return flags; + } + + public boolean hasChoiceOnlyContent() { + return hasChoiceOnlyContent; + } + + public boolean hasCondition() { + return hasCondition; + } + + public boolean hasStartContent() { + return hasStartContent; + } + + public boolean isInvisibleDefault() { + return isInvisibleDefault; + } + + public boolean isOnceOnly() { + return onceOnly; + } + + public Path getPathOnChoice() throws Exception { + // Resolve any relative paths to global ones as we come across them + if (pathOnChoice != null && pathOnChoice.isRelative()) { + Container choiceTargetObj = getChoiceTarget(); + if (choiceTargetObj != null) { + pathOnChoice = choiceTargetObj.getPath(); + } + } + return pathOnChoice; + } + + public String getPathStringOnChoice() throws Exception { + return compactPathString(getPathOnChoice()); + } + + public void setFlags(int value) { + setHasCondition((value & 1) > 0); + setHasStartContent((value & 2) > 0); + setHasChoiceOnlyContent((value & 4) > 0); + setIsInvisibleDefault((value & 8) > 0); + setOnceOnly((value & 16) > 0); + } + + public void setHasChoiceOnlyContent(boolean value) { + hasChoiceOnlyContent = value; + } + + public void setHasCondition(boolean value) { + hasCondition = value; + } + + public void setHasStartContent(boolean value) { + hasStartContent = value; + } + + public void setIsInvisibleDefault(boolean value) { + isInvisibleDefault = value; + } + + public void setOnceOnly(boolean value) { + onceOnly = value; + } + + public void setPathOnChoice(Path value) { + pathOnChoice = value; + } + + public void setPathStringOnChoice(String value) { + setPathOnChoice(new Path(value)); + } + + @Override + public String toString() { + try { + Integer targetLineNum = debugLineNumberOfPath(getPathOnChoice()); + + String targetString = getPathOnChoice().toString(); + + if (targetLineNum != null) { + targetString = " line " + targetLineNum + "(" + targetString + ")"; + } + + return "Choice: -> " + targetString; + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/bladecoder/ink/runtime/Container.java b/src/main/java/com/bladecoder/ink/runtime/Container.java index 5fe6e06..431cd5a 100644 --- a/src/main/java/com/bladecoder/ink/runtime/Container.java +++ b/src/main/java/com/bladecoder/ink/runtime/Container.java @@ -1,395 +1,330 @@ -package com.bladecoder.ink.runtime; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map.Entry; - -import com.bladecoder.ink.runtime.Path.Component; - -public class Container extends RTObject implements INamedContent { - private String name; - - private List content; - private HashMap namedContent; - - private boolean visitsShouldBeCounted; - private boolean turnIndexShouldBeCounted; - private boolean countingAtStartOnly; - - public Container() { - content = new ArrayList(); - setNamedContent(new HashMap()); - } - - @Override - public String getName() { - return name; - } - - public void setName(String value) { - name = value; - } - - public List getContent() { - return content; - } - - public void setContent(List value) throws Exception { - addContent(value); - } - - public HashMap getNamedContent() { - return namedContent; - } - - public void setNamedContent(HashMap value) { - namedContent = value; - } - - public HashMap getNamedOnlyContent() { - - HashMap namedOnlyContentDict = new HashMap(); - - for (Entry kvPair : getNamedContent().entrySet()) { - namedOnlyContentDict.put(kvPair.getKey(), (RTObject) kvPair.getValue()); - } - - for (RTObject c : getContent()) { - INamedContent named = c instanceof INamedContent ? (INamedContent) c : (INamedContent) null; - if (named != null && named.hasValidName()) { - namedOnlyContentDict.remove(named.getName()); - } - - } - - if (namedOnlyContentDict.size() == 0) - namedOnlyContentDict = null; - - return namedOnlyContentDict; - } - - public void setNamedOnlyContent(HashMap value) { - HashMap existingNamedOnly = getNamedOnlyContent(); - if (existingNamedOnly != null) { - for (Entry kvPair : existingNamedOnly.entrySet()) { - getNamedContent().remove(kvPair.getKey()); - } - } - - if (value == null) - return; - - for (Entry kvPair : value.entrySet()) { - INamedContent named = kvPair.getValue() instanceof INamedContent ? (INamedContent) kvPair.getValue() - : (INamedContent) null; - if (named != null) - addToNamedContentOnly(named); - - } - } - - public boolean getVisitsShouldBeCounted() { - return visitsShouldBeCounted; - } - - public void setVisitsShouldBeCounted(boolean value) { - visitsShouldBeCounted = value; - } - - public boolean getTurnIndexShouldBeCounted() { - return turnIndexShouldBeCounted; - } - - public void setTurnIndexShouldBeCounted(boolean value) { - turnIndexShouldBeCounted = value; - } - - public boolean getCountingAtStartOnly() { - return countingAtStartOnly; - } - - public void setCountingAtStartOnly(boolean value) { - countingAtStartOnly = value; - } - - public static final int COUNTFLAGS_VISITS = 1; - public static final int COUNTFLAGS_TURNS = 2; - public static final int COUNTFLAGS_COUNTSTARTONLY = 4; - - public int getCountFlags() { - int flags = 0; - - if (getVisitsShouldBeCounted()) - flags |= COUNTFLAGS_VISITS; - - if (getTurnIndexShouldBeCounted()) - flags |= COUNTFLAGS_TURNS; - - if (getCountingAtStartOnly()) - flags |= COUNTFLAGS_COUNTSTARTONLY; - - // If we're only storing CountStartOnly, it serves no purpose, - // since it's dependent on the other two to be used at all. - // (e.g. for setting the fact that *if* a gather or choice's - // content is counted, then is should only be counter at the start) - // So this is just an optimisation for storage. - if (flags == COUNTFLAGS_COUNTSTARTONLY) { - flags = 0; - } - - return flags; - } - - public void setCountFlags(int value) { - int flag = value; - - if ((flag & COUNTFLAGS_VISITS) > 0) - setVisitsShouldBeCounted(true); - - if ((flag & COUNTFLAGS_TURNS) > 0) - setTurnIndexShouldBeCounted(true); - - if ((flag & COUNTFLAGS_COUNTSTARTONLY) > 0) - setCountingAtStartOnly(true); - - } - - @Override - public boolean hasValidName() { - return getName() != null && getName().length() > 0; - } - - public Path getPathToFirstLeafContent() { - if (_pathToFirstLeafContent == null) - _pathToFirstLeafContent = getPath().pathByAppendingPath(getInternalPathToFirstLeafContent()); - - return _pathToFirstLeafContent; - } - - Path _pathToFirstLeafContent; - - Path getInternalPathToFirstLeafContent() { - List components = new ArrayList(); - - Container container = this; - while (container != null) { - if (container.getContent().size() > 0) { - components.add(new Path.Component(0)); - container = container.getContent().get(0) instanceof Container - ? (Container) container.getContent().get(0) - : (Container) null; - } - - } - - return new Path(components); - } - - public void addContent(RTObject contentObj) throws Exception { - getContent().add(contentObj); - - if (contentObj.getParent() != null) { - throw new Exception("content is already in " + contentObj.getParent()); - } - - contentObj.setParent(this); - - tryAddNamedContent(contentObj); - } - - public void addContent(List contentList) throws Exception { - for (RTObject c : contentList) { - addContent(c); - } - } - - public void insertContent(RTObject contentObj, int index) throws Exception { - getContent().add(index, contentObj); - if (contentObj.getParent() != null) { - throw new Exception("content is already in " + contentObj.getParent()); - } - - contentObj.setParent(this); - tryAddNamedContent(contentObj); - } - - public void tryAddNamedContent(RTObject contentObj) throws Exception { - INamedContent namedContentObj = contentObj instanceof INamedContent ? (INamedContent) contentObj - : (INamedContent) null; - if (namedContentObj != null && namedContentObj.hasValidName()) { - addToNamedContentOnly(namedContentObj); - } - - } - - public void addToNamedContentOnly(INamedContent namedContentObj) { - // Debug.Assert(namedContentObj instanceof RTObject, "Can only add - // Runtime.RTObjects to a Runtime.Container"); - RTObject runtimeObj = (RTObject) namedContentObj; - - runtimeObj.setParent(this); - - getNamedContent().put(namedContentObj.getName(), namedContentObj); - } - - public void addContentsOfContainer(Container otherContainer) throws Exception { - getContent().addAll(otherContainer.getContent()); - - for (RTObject obj : otherContainer.getContent()) { - obj.setParent(this); - - tryAddNamedContent(obj); - } - } - - protected RTObject contentWithPathComponent(Path.Component component) throws StoryException, Exception { - - if (component.isIndex()) { - if (component.getIndex() >= 0 && component.getIndex() < getContent().size()) { - return getContent().get(component.getIndex()); - } else { - return null; - } - } else if (component.isParent()) { - // When path is out of range, quietly return nil - // (useful as we step/increment forwards through content) - return this.getParent(); - } else { - INamedContent foundContent = getNamedContent().get(component.getName()); - - if (foundContent != null) { - return (RTObject) foundContent; - } else { - return null; - } - } - } - - public SearchResult contentAtPath(Path path) throws Exception { - return contentAtPath(path, 0, -1); - } - - public SearchResult contentAtPath(Path path, int partialPathStart, int partialPathLength) throws Exception { - if (partialPathLength == -1) - partialPathLength = path.getLength(); - - SearchResult result = new SearchResult(); - result.approximate = false; - - Container currentContainer = this; - RTObject currentObj = this; - - for (int i = partialPathStart; i < partialPathLength; ++i) { - Component comp = path.getComponent(i); - // Path component was wrong type - if (currentContainer == null) { - result.approximate = true; - break; - } - - RTObject foundObj = currentContainer.contentWithPathComponent(comp); - - // Couldn't resolve entire path? - if (foundObj == null) { - result.approximate = true; - break; - } - - currentObj = foundObj; - currentContainer = foundObj instanceof Container ? (Container) foundObj : null; - } - - result.obj = currentObj; - - return result; - } - - private final static int spacesPerIndent = 4; - - private void appendIndentation(StringBuilder sb, int indentation) { - for (int i = 0; i < spacesPerIndent * indentation; ++i) { - sb.append(" "); - } - } - - public void buildStringOfHierarchy(StringBuilder sb, int indentation, RTObject pointedObj) { - - appendIndentation(sb, indentation); - - sb.append("["); - if (this.hasValidName()) { - sb.append(" ({"); - sb.append(this.getName()); - sb.append("})"); - } - - if (this == pointedObj) { - sb.append(" <---"); - } - - sb.append("\n"); - indentation++; - for (int i = 0; i < getContent().size(); ++i) { - RTObject obj = getContent().get(i); - - if (obj instanceof Container) { - Container container = (Container) obj; - container.buildStringOfHierarchy(sb, indentation, pointedObj); - } else { - appendIndentation(sb, indentation); - if (obj instanceof StringValue) { - sb.append("\""); - sb.append(obj.toString().replace("\n", "\\n")); - sb.append("\""); - } else { - sb.append(obj.toString()); - } - } - if (i != getContent().size() - 1) { - sb.append(","); - } - - if (!(obj instanceof Container) && obj == pointedObj) { - sb.append(" <---"); - } - - sb.append("\n"); - } - - HashMap onlyNamed = new HashMap(); - - for (Entry objKV : getNamedContent().entrySet()) { - if (getContent().contains(objKV.getValue())) { - continue; - } else { - onlyNamed.put(objKV.getKey(), objKV.getValue()); - } - } - - if (onlyNamed.size() > 0) { - appendIndentation(sb, indentation); - - sb.append("-- named: --\n"); - - for (Entry objKV : onlyNamed.entrySet()) { - // Debug.Assert(objKV.Value instanceof Container, "Can only - // print out named Containers"); - Container container = (Container) objKV.getValue(); - container.buildStringOfHierarchy(sb, indentation, pointedObj); - sb.append("\n"); - } - } - - indentation--; - appendIndentation(sb, indentation); - sb.append("]"); - } - - public String buildStringOfHierarchy() { - StringBuilder sb = new StringBuilder(); - buildStringOfHierarchy(sb, 0, null); - return sb.toString(); - } - -} +package com.bladecoder.ink.runtime; + +import com.bladecoder.ink.runtime.Path.Component; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map.Entry; + +public class Container extends RTObject implements INamedContent { + private String name; + + private final List content; + private HashMap namedContent; + + private boolean visitsShouldBeCounted; + private boolean turnIndexShouldBeCounted; + private boolean countingAtStartOnly; + + public Container() { + content = new ArrayList<>(); + setNamedContent(new HashMap<>()); + } + + @Override + public String getName() { + return name; + } + + public void setName(String value) { + name = value; + } + + public List getContent() { + return content; + } + + public HashMap getNamedContent() { + return namedContent; + } + + public void setNamedContent(HashMap value) { + namedContent = value; + } + + public HashMap getNamedOnlyContent() { + + HashMap namedOnlyContentDict = new HashMap(); + + for (Entry kvPair : getNamedContent().entrySet()) { + namedOnlyContentDict.put(kvPair.getKey(), (RTObject) kvPair.getValue()); + } + + for (RTObject c : getContent()) { + INamedContent named = c instanceof INamedContent ? (INamedContent) c : null; + if (named != null && named.hasValidName()) { + namedOnlyContentDict.remove(named.getName()); + } + } + + return namedOnlyContentDict; + } + + public void setNamedOnlyContent(HashMap value) { + HashMap existingNamedOnly = getNamedOnlyContent(); + for (Entry kvPair : existingNamedOnly.entrySet()) { + getNamedContent().remove(kvPair.getKey()); + } + + for (Entry kvPair : value.entrySet()) { + INamedContent named = kvPair.getValue() instanceof INamedContent + ? (INamedContent) kvPair.getValue() + : (INamedContent) null; + if (named != null) addToNamedContentOnly(named); + } + } + + public boolean getVisitsShouldBeCounted() { + return visitsShouldBeCounted; + } + + public void setVisitsShouldBeCounted(boolean value) { + visitsShouldBeCounted = value; + } + + public boolean getTurnIndexShouldBeCounted() { + return turnIndexShouldBeCounted; + } + + public void setTurnIndexShouldBeCounted(boolean value) { + turnIndexShouldBeCounted = value; + } + + public boolean getCountingAtStartOnly() { + return countingAtStartOnly; + } + + public void setCountingAtStartOnly(boolean value) { + countingAtStartOnly = value; + } + + public static final int COUNTFLAGS_VISITS = 1; + public static final int COUNTFLAGS_TURNS = 2; + public static final int COUNTFLAGS_COUNTSTARTONLY = 4; + + public int getCountFlags() { + int flags = 0; + + if (getVisitsShouldBeCounted()) flags |= COUNTFLAGS_VISITS; + + if (getTurnIndexShouldBeCounted()) flags |= COUNTFLAGS_TURNS; + + if (getCountingAtStartOnly()) flags |= COUNTFLAGS_COUNTSTARTONLY; + + // If we're only storing CountStartOnly, it serves no purpose, + // since it's dependent on the other two to be used at all. + // (e.g. for setting the fact that *if* a gather or choice's + // content is counted, then is should only be counter at the start) + // So this is just an optimisation for storage. + if (flags == COUNTFLAGS_COUNTSTARTONLY) { + flags = 0; + } + + return flags; + } + + public void setCountFlags(int value) { + + if ((value & COUNTFLAGS_VISITS) > 0) setVisitsShouldBeCounted(true); + + if ((value & COUNTFLAGS_TURNS) > 0) setTurnIndexShouldBeCounted(true); + + if ((value & COUNTFLAGS_COUNTSTARTONLY) > 0) setCountingAtStartOnly(true); + } + + @Override + public boolean hasValidName() { + return getName() != null && !getName().isEmpty(); + } + + public void addContents(List contentList) throws Exception { + for (RTObject c : contentList) { + addContent(c); + } + } + + private void addContent(RTObject contentObj) throws Exception { + getContent().add(contentObj); + + if (contentObj.getParent() != null) { + throw new Exception("content is already in " + contentObj.getParent()); + } + + contentObj.setParent(this); + + tryAddNamedContent(contentObj); + } + + private void tryAddNamedContent(RTObject contentObj) { + INamedContent namedContentObj = contentObj instanceof INamedContent ? (INamedContent) contentObj : null; + if (namedContentObj != null && namedContentObj.hasValidName()) { + addToNamedContentOnly(namedContentObj); + } + } + + public void addToNamedContentOnly(INamedContent namedContentObj) { + // Debug.Assert(namedContentObj instanceof RTObject, "Can only add + // Runtime.RTObjects to a Runtime.Container"); + RTObject runtimeObj = (RTObject) namedContentObj; + + runtimeObj.setParent(this); + + getNamedContent().put(namedContentObj.getName(), namedContentObj); + } + + private RTObject contentWithPathComponent(Path.Component component) { + + if (component.isIndex()) { + if (component.getIndex() >= 0 && component.getIndex() < getContent().size()) { + return getContent().get(component.getIndex()); + } else { + return null; + } + } else if (component.isParent()) { + // When path is out of range, quietly return nil + // (useful as we step/increment forwards through content) + return this.getParent(); + } else { + INamedContent foundContent = getNamedContent().get(component.getName()); + + if (foundContent != null) { + return (RTObject) foundContent; + } else { + return null; + } + } + } + + public SearchResult contentAtPath(Path path) { + return contentAtPath(path, 0, -1); + } + + public SearchResult contentAtPath(Path path, int partialPathStart, int partialPathLength) { + if (partialPathLength == -1) partialPathLength = path.getLength(); + + SearchResult result = new SearchResult(); + result.approximate = false; + + Container currentContainer = this; + RTObject currentObj = this; + + for (int i = partialPathStart; i < partialPathLength; ++i) { + Component comp = path.getComponent(i); + // Path component was wrong type + if (currentContainer == null) { + result.approximate = true; + break; + } + + RTObject foundObj = currentContainer.contentWithPathComponent(comp); + + // Couldn't resolve entire path? + if (foundObj == null) { + result.approximate = true; + break; + } + + // Are we about to loop into another container? + // Is the object a container as expected? It might + // no longer be if the content has shuffled around, so what + // was originally a container no longer is. + Container nextContainer = foundObj instanceof Container ? (Container) foundObj : null; + if (i < partialPathLength - 1 && nextContainer == null) { + result.approximate = true; + break; + } + + currentObj = foundObj; + currentContainer = nextContainer; + } + + result.obj = currentObj; + + return result; + } + + private static final int spacesPerIndent = 4; + + private void appendIndentation(StringBuilder sb, int indentation) { + for (int i = 0; i < spacesPerIndent * indentation; ++i) { + sb.append(" "); + } + } + + public void buildStringOfHierarchy(StringBuilder sb, int indentation, RTObject pointedObj) { + + appendIndentation(sb, indentation); + + sb.append("["); + if (this.hasValidName()) { + sb.append(" ({"); + sb.append(this.getName()); + sb.append("})"); + } + + if (this == pointedObj) { + sb.append(" <---"); + } + + sb.append("\n"); + indentation++; + for (int i = 0; i < getContent().size(); ++i) { + RTObject obj = getContent().get(i); + + if (obj instanceof Container) { + Container container = (Container) obj; + container.buildStringOfHierarchy(sb, indentation, pointedObj); + } else { + appendIndentation(sb, indentation); + if (obj instanceof StringValue) { + sb.append("\""); + sb.append(obj.toString().replace("\n", "\\n")); + sb.append("\""); + } else { + sb.append(obj.toString()); + } + } + if (i != getContent().size() - 1) { + sb.append(","); + } + + if (!(obj instanceof Container) && obj == pointedObj) { + sb.append(" <---"); + } + + sb.append("\n"); + } + + HashMap onlyNamed = new HashMap<>(); + + for (Entry objKV : getNamedContent().entrySet()) { + if (!getContent().contains(objKV.getValue())) { + onlyNamed.put(objKV.getKey(), objKV.getValue()); + } + } + + if (!onlyNamed.isEmpty()) { + appendIndentation(sb, indentation); + + sb.append("-- named: --\n"); + + for (Entry objKV : onlyNamed.entrySet()) { + // Debug.Assert(objKV.Value instanceof Container, "Can only + // print out named Containers"); + Container container = (Container) objKV.getValue(); + container.buildStringOfHierarchy(sb, indentation, pointedObj); + sb.append("\n"); + } + } + + indentation--; + appendIndentation(sb, indentation); + sb.append("]"); + } + + public String buildStringOfHierarchy() { + StringBuilder sb = new StringBuilder(); + buildStringOfHierarchy(sb, 0, null); + return sb.toString(); + } +} diff --git a/src/main/java/com/bladecoder/ink/runtime/ControlCommand.java b/src/main/java/com/bladecoder/ink/runtime/ControlCommand.java index d213026..79b0a37 100644 --- a/src/main/java/com/bladecoder/ink/runtime/ControlCommand.java +++ b/src/main/java/com/bladecoder/ink/runtime/ControlCommand.java @@ -1,146 +1,62 @@ -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(); - } - -} +package com.bladecoder.ink.runtime; + +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, + 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()); + } + + @Override + public String toString() { + return getCommandType().toString(); + } +} diff --git a/src/main/java/com/bladecoder/ink/runtime/DebugMetadata.java b/src/main/java/com/bladecoder/ink/runtime/DebugMetadata.java index d9ef6cb..a418082 100644 --- a/src/main/java/com/bladecoder/ink/runtime/DebugMetadata.java +++ b/src/main/java/com/bladecoder/ink/runtime/DebugMetadata.java @@ -1,21 +1,56 @@ -package com.bladecoder.ink.runtime; - -public class DebugMetadata { - public int startLineNumber = 0; - public int endLineNumber = 0; - public String fileName = null; - public String sourceName = null; - - public DebugMetadata() { - } - - @Override - public String toString() { - if (fileName != null) { - return String.format("line %d of %s", startLineNumber, fileName); - } else { - return "line " + startLineNumber; - } - } - -} +package com.bladecoder.ink.runtime; + +public class DebugMetadata { + public int startLineNumber = 0; + public int endLineNumber = 0; + public int startCharacterNumber = 0; + public int endCharacterNumber = 0; + public String fileName = null; + public String sourceName = null; + + public DebugMetadata() {} + + // Currently only used in VariableReference in order to + // merge the debug metadata of a Path.Of.Indentifiers into + // one single range. + public DebugMetadata merge(DebugMetadata dm) { + DebugMetadata newDebugMetadata = new DebugMetadata(); + + // These are not supposed to be differ between 'this' and 'dm'. + newDebugMetadata.fileName = fileName; + newDebugMetadata.sourceName = sourceName; + + if (startLineNumber < dm.startLineNumber) { + newDebugMetadata.startLineNumber = startLineNumber; + newDebugMetadata.startCharacterNumber = startCharacterNumber; + } else if (startLineNumber > dm.startLineNumber) { + newDebugMetadata.startLineNumber = dm.startLineNumber; + newDebugMetadata.startCharacterNumber = dm.startCharacterNumber; + } else { + newDebugMetadata.startLineNumber = startLineNumber; + newDebugMetadata.startCharacterNumber = Math.min(startCharacterNumber, dm.startCharacterNumber); + } + + if (endLineNumber > dm.endLineNumber) { + newDebugMetadata.endLineNumber = endLineNumber; + newDebugMetadata.endCharacterNumber = endCharacterNumber; + } else if (endLineNumber < dm.endLineNumber) { + newDebugMetadata.endLineNumber = dm.endLineNumber; + newDebugMetadata.endCharacterNumber = dm.endCharacterNumber; + } else { + newDebugMetadata.endLineNumber = endLineNumber; + newDebugMetadata.endCharacterNumber = Math.max(endCharacterNumber, dm.endCharacterNumber); + } + + return newDebugMetadata; + } + + @Override + public String toString() { + if (fileName != null) { + return String.format("line %d of %s", startLineNumber, fileName); + } else { + return "line " + startLineNumber; + } + } +} diff --git a/src/main/java/com/bladecoder/ink/runtime/Divert.java b/src/main/java/com/bladecoder/ink/runtime/Divert.java index 5ac0aae..915c681 100644 --- a/src/main/java/com/bladecoder/ink/runtime/Divert.java +++ b/src/main/java/com/bladecoder/ink/runtime/Divert.java @@ -1,212 +1,204 @@ -package com.bladecoder.ink.runtime; - -public class Divert extends RTObject { - private int externalArgs; - - private boolean isConditional; - - private boolean isExternal; - - private boolean pushesToStack; - - private PushPopType stackPushType = PushPopType.Tunnel; - - private final Pointer targetPointer = new Pointer(); - - private Path targetPath; - - private String variableDivertName; - - public Divert() { - setPushesToStack(false); - } - - public Divert(PushPopType stackPushType) { - setPushesToStack(true); - this.setStackPushType(stackPushType); - } - - public boolean equals(RTObject obj) { - try { - Divert otherDivert = obj instanceof Divert ? (Divert) obj : (Divert) null; - if (otherDivert != null) { - if (this.hasVariableTarget() == otherDivert.hasVariableTarget()) { - if (this.hasVariableTarget()) { - return this.getVariableDivertName().equals(otherDivert.getVariableDivertName()); - } else { - return this.getTargetPath().equals(otherDivert.getTargetPath()); - } - } - - } - - return false; - } catch (Exception e) { - throw new RuntimeException(e); - } - - } - - public int getExternalArgs() { - return externalArgs; - } - - public boolean getPushesToStack() { - return pushesToStack; - } - - public PushPopType getStackPushType() { - return stackPushType; - } - - public Pointer getTargetPointer() throws Exception { - if (targetPointer.isNull()) { - RTObject targetObj = resolvePath(targetPath).obj; - - if (targetPath.getLastComponent().isIndex()) { - targetPointer.container = (Container) targetObj.getParent(); - targetPointer.index = targetPath.getLastComponent().getIndex(); - } else { - targetPointer.assign(Pointer.startOf((Container) targetObj)); - } - } - return targetPointer; - } - - public Path getTargetPath() throws Exception { - // Resolve any relative paths to global ones as we come across them - if (targetPath != null && targetPath.isRelative()) { - RTObject targetObj = getTargetPointer().resolve(); - - if (targetObj != null) { - targetPath = targetObj.getPath(); - } - - } - - return targetPath; - } - - public String getTargetPathString() throws Exception { - if (getTargetPath() == null) - return null; - - return compactPathString(getTargetPath()); - } - - public String getVariableDivertName() { - return variableDivertName; - } - - @Override - public int hashCode() { - try { - if (hasVariableTarget()) { - int variableTargetSalt = 12345; - return getVariableDivertName().hashCode() + variableTargetSalt; - } else { - int pathTargetSalt = 54321; - return getTargetPath().hashCode() + pathTargetSalt; - } - } catch (RuntimeException __dummyCatchVar1) { - throw __dummyCatchVar1; - } catch (Exception __dummyCatchVar1) { - throw new RuntimeException(__dummyCatchVar1); - } - - } - - public boolean hasVariableTarget() { - return getVariableDivertName() != null; - } - - public boolean isConditional() { - return isConditional; - } - - public boolean isExternal() { - return isExternal; - } - - public void setConditional(boolean value) { - isConditional = value; - } - - public void setExternal(boolean value) { - isExternal = value; - } - - public void setExternalArgs(int value) { - externalArgs = value; - } - - public void setPushesToStack(boolean value) { - pushesToStack = value; - } - - public void setStackPushType(PushPopType stackPushType) { - this.stackPushType = stackPushType; - } - - public void setTargetPath(Path value) { - targetPath = value; - targetPointer.assign(Pointer.Null); - } - - public void setTargetPathString(String value) { - if (value == null) { - setTargetPath(null); - } else { - setTargetPath(new Path(value)); - } - } - - public void setVariableDivertName(String value) { - variableDivertName = value; - } - - @Override - public String toString() { - try { - if (hasVariableTarget()) { - return "Divert(variable: " + getVariableDivertName() + ")"; - } else if (getTargetPath() == null) { - return "Divert(null)"; - } else { - StringBuilder sb = new StringBuilder(); - String targetStr = getTargetPath().toString(); - Integer targetLineNum = debugLineNumberOfPath(getTargetPath()); - if (targetLineNum != null) { - targetStr = "line " + targetLineNum; - } - - sb.append("Divert"); - - if (isConditional) - sb.append('?'); - - if (getPushesToStack()) { - if (getStackPushType() == PushPopType.Function) { - sb.append(" function"); - } else { - sb.append(" tunnel"); - } - } - - sb.append(" -> "); - sb.append(getTargetPathString()); - - sb.append(" ("); - sb.append(targetStr); - sb.append(")"); - return sb.toString(); - } - } catch (RuntimeException __dummyCatchVar2) { - throw __dummyCatchVar2; - } catch (Exception __dummyCatchVar2) { - throw new RuntimeException(__dummyCatchVar2); - } - - } - -} +package com.bladecoder.ink.runtime; + +public class Divert extends RTObject { + private int externalArgs; + + private boolean isConditional; + + private boolean isExternal; + + private boolean pushesToStack; + + private PushPopType stackPushType = PushPopType.Tunnel; + + private final Pointer targetPointer = new Pointer(); + + private Path targetPath; + + private String variableDivertName; + + public Divert() { + setPushesToStack(false); + } + + public Divert(PushPopType stackPushType) { + setPushesToStack(true); + this.setStackPushType(stackPushType); + } + + public boolean equals(RTObject obj) { + try { + Divert otherDivert = obj instanceof Divert ? (Divert) obj : (Divert) null; + if (otherDivert != null) { + if (this.hasVariableTarget() == otherDivert.hasVariableTarget()) { + if (this.hasVariableTarget()) { + return this.getVariableDivertName().equals(otherDivert.getVariableDivertName()); + } else { + return this.getTargetPath().equals(otherDivert.getTargetPath()); + } + } + } + + return false; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public int getExternalArgs() { + return externalArgs; + } + + public boolean getPushesToStack() { + return pushesToStack; + } + + public PushPopType getStackPushType() { + return stackPushType; + } + + public Pointer getTargetPointer() throws Exception { + if (targetPointer.isNull()) { + RTObject targetObj = resolvePath(targetPath).obj; + + if (targetPath.getLastComponent().isIndex()) { + targetPointer.container = targetObj.getParent(); + targetPointer.index = targetPath.getLastComponent().getIndex(); + } else { + targetPointer.assign(Pointer.startOf((Container) targetObj)); + } + } + return targetPointer; + } + + public Path getTargetPath() throws Exception { + // Resolve any relative paths to global ones as we come across them + if (targetPath != null && targetPath.isRelative()) { + RTObject targetObj = getTargetPointer().resolve(); + + if (targetObj != null) { + targetPath = targetObj.getPath(); + } + } + + return targetPath; + } + + public String getTargetPathString() throws Exception { + if (getTargetPath() == null) return null; + + return compactPathString(getTargetPath()); + } + + public String getVariableDivertName() { + return variableDivertName; + } + + @Override + public int hashCode() { + try { + if (hasVariableTarget()) { + int variableTargetSalt = 12345; + return getVariableDivertName().hashCode() + variableTargetSalt; + } else { + int pathTargetSalt = 54321; + return getTargetPath().hashCode() + pathTargetSalt; + } + } catch (RuntimeException __dummyCatchVar1) { + throw __dummyCatchVar1; + } catch (Exception __dummyCatchVar1) { + throw new RuntimeException(__dummyCatchVar1); + } + } + + public boolean hasVariableTarget() { + return getVariableDivertName() != null; + } + + public boolean isConditional() { + return isConditional; + } + + public boolean isExternal() { + return isExternal; + } + + public void setConditional(boolean value) { + isConditional = value; + } + + public void setExternal(boolean value) { + isExternal = value; + } + + public void setExternalArgs(int value) { + externalArgs = value; + } + + public void setPushesToStack(boolean value) { + pushesToStack = value; + } + + public void setStackPushType(PushPopType stackPushType) { + this.stackPushType = stackPushType; + } + + public void setTargetPath(Path value) { + targetPath = value; + targetPointer.assign(Pointer.Null); + } + + public void setTargetPathString(String value) { + if (value == null) { + setTargetPath(null); + } else { + setTargetPath(new Path(value)); + } + } + + public void setVariableDivertName(String value) { + variableDivertName = value; + } + + @Override + public String toString() { + try { + if (hasVariableTarget()) { + return "Divert(variable: " + getVariableDivertName() + ")"; + } else if (getTargetPath() == null) { + return "Divert(null)"; + } else { + StringBuilder sb = new StringBuilder(); + String targetStr = getTargetPath().toString(); + Integer targetLineNum = debugLineNumberOfPath(getTargetPath()); + if (targetLineNum != null) { + targetStr = "line " + targetLineNum; + } + + sb.append("Divert"); + + if (isConditional) sb.append('?'); + + if (getPushesToStack()) { + if (getStackPushType() == PushPopType.Function) { + sb.append(" function"); + } else { + sb.append(" tunnel"); + } + } + + sb.append(" -> "); + sb.append(getTargetPathString()); + + sb.append(" ("); + sb.append(targetStr); + sb.append(")"); + return sb.toString(); + } + } catch (RuntimeException __dummyCatchVar2) { + throw __dummyCatchVar2; + } catch (Exception __dummyCatchVar2) { + throw new RuntimeException(__dummyCatchVar2); + } + } +} diff --git a/src/main/java/com/bladecoder/ink/runtime/DivertTargetValue.java b/src/main/java/com/bladecoder/ink/runtime/DivertTargetValue.java index 4cdedd4..438321b 100644 --- a/src/main/java/com/bladecoder/ink/runtime/DivertTargetValue.java +++ b/src/main/java/com/bladecoder/ink/runtime/DivertTargetValue.java @@ -1,43 +1,41 @@ -package com.bladecoder.ink.runtime; - -class DivertTargetValue extends Value { - public DivertTargetValue() { - super(null); - } - - public DivertTargetValue(Path targetPath) { - super(targetPath); - } - - @Override - public AbstractValue cast(ValueType newType) throws Exception { - if (newType == getValueType()) - return this; - - throw BadCastException (newType); - } - - @Override - public boolean isTruthy() throws Exception { - throw new Exception("Shouldn't be checking the truthiness of a divert target"); - } - - public Path getTargetPath() { - return this.getValue(); - } - - @Override - public ValueType getValueType() { - return ValueType.DivertTarget; - } - - public void setTargetPath(Path value) { - this.setValue(value); - } - - @Override - public String toString() { - return "DivertTargetValue(" + getTargetPath() + ")"; - } - -} +package com.bladecoder.ink.runtime; + +class DivertTargetValue extends Value { + public DivertTargetValue() { + super(null); + } + + public DivertTargetValue(Path targetPath) { + super(targetPath); + } + + @Override + public AbstractValue cast(ValueType newType) throws Exception { + if (newType == getValueType()) return this; + + throw BadCastException(newType); + } + + @Override + public boolean isTruthy() throws Exception { + throw new Exception("Shouldn't be checking the truthiness of a divert target"); + } + + public Path getTargetPath() { + return this.getValue(); + } + + @Override + public ValueType getValueType() { + return ValueType.DivertTarget; + } + + public void setTargetPath(Path value) { + this.setValue(value); + } + + @Override + public String toString() { + return "DivertTargetValue(" + getTargetPath() + ")"; + } +} diff --git a/src/main/java/com/bladecoder/ink/runtime/Error.java b/src/main/java/com/bladecoder/ink/runtime/Error.java new file mode 100644 index 0000000..00eb49b --- /dev/null +++ b/src/main/java/com/bladecoder/ink/runtime/Error.java @@ -0,0 +1,24 @@ +package com.bladecoder.ink.runtime; + +public class Error { + /// Callback for errors throughout both the ink runtime and compiler. + + public interface ErrorHandler { + void error(String message, ErrorType type); + } + + /// Author errors will only ever come from the compiler so don't need to be + /// handled + /// by your Story error handler. The "Error" ErrorType is by far the most common + /// for a runtime story error (rather than compiler error), though the Warning + /// type + /// is also possible. + public enum ErrorType { + /// Generated by a "TODO" note in the ink source + Author, + /// You should probably fix this, but it's not critical + Warning, + /// Critical error that can't be recovered from + Error + } +} diff --git a/src/main/java/com/bladecoder/ink/runtime/FloatValue.java b/src/main/java/com/bladecoder/ink/runtime/FloatValue.java index 8408876..0e61ad2 100644 --- a/src/main/java/com/bladecoder/ink/runtime/FloatValue.java +++ b/src/main/java/com/bladecoder/ink/runtime/FloatValue.java @@ -1,39 +1,42 @@ -package com.bladecoder.ink.runtime; - -class FloatValue extends Value { - public FloatValue() { - this(0.0f); - } - - public FloatValue(float val) { - super(val); - } - - @Override - public AbstractValue cast(ValueType newType) throws Exception { - if (newType == getValueType()) { - return this; - } - - if (newType == ValueType.Int) { - return new IntValue(this.getValue().intValue()); - } - - if (newType == ValueType.String) { - return new StringValue(this.getValue().toString()); - } - - throw BadCastException (newType); - } - - @Override - public boolean isTruthy() { - return getValue() != 0.0f; - } - - @Override - public ValueType getValueType() { - return ValueType.Float; - } - -} +package com.bladecoder.ink.runtime; + +class FloatValue extends Value { + public FloatValue() { + this(0.0f); + } + + public FloatValue(float val) { + super(val); + } + + @Override + public AbstractValue cast(ValueType newType) throws Exception { + if (newType == getValueType()) { + return this; + } + + if (newType == ValueType.Bool) { + return new BoolValue(this.value == 0.0f ? false : true); + } + + if (newType == ValueType.Int) { + return new IntValue(this.getValue().intValue()); + } + + if (newType == ValueType.String) { + return new StringValue(this.getValue().toString()); + } + + throw BadCastException(newType); + } + + @Override + public boolean isTruthy() { + return getValue() != 0.0f; + } + + @Override + public ValueType getValueType() { + return ValueType.Float; + } +} diff --git a/src/main/java/com/bladecoder/ink/runtime/Flow.java b/src/main/java/com/bladecoder/ink/runtime/Flow.java new file mode 100644 index 0000000..dd6684a --- /dev/null +++ b/src/main/java/com/bladecoder/ink/runtime/Flow.java @@ -0,0 +1,103 @@ +package com.bladecoder.ink.runtime; + +import com.bladecoder.ink.runtime.SimpleJson.Writer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +public class Flow { + public String name; + public CallStack callStack; + public List outputStream; + public List currentChoices; + + public Flow(String name, Story story) { + this.name = name; + this.callStack = new CallStack(story); + this.outputStream = new ArrayList<>(); + this.currentChoices = new ArrayList<>(); + } + + @SuppressWarnings("unchecked") + public Flow(String name, Story story, HashMap jObject) throws Exception { + this.name = name; + this.callStack = new CallStack(story); + this.callStack.setJsonToken((HashMap) jObject.get("callstack"), story); + this.outputStream = Json.jArrayToRuntimeObjList((List) jObject.get("outputStream")); + this.currentChoices = Json.jArrayToRuntimeObjList((List) jObject.get("currentChoices")); + + // choiceThreads is optional + Object jChoiceThreadsObj = jObject.get("choiceThreads"); + + loadFlowChoiceThreads((HashMap) jChoiceThreadsObj, story); + } + + public void writeJson(SimpleJson.Writer writer) throws Exception { + writer.writeObjectStart(); + + writer.writeProperty("callstack", new SimpleJson.InnerWriter() { + @Override + public void write(Writer w) throws Exception { + callStack.writeJson(w); + } + }); + + writer.writeProperty("outputStream", new SimpleJson.InnerWriter() { + @Override + public void write(Writer w) throws Exception { + Json.writeListRuntimeObjs(w, outputStream); + } + }); + + // choiceThreads: optional + // Has to come BEFORE the choices themselves are written out + // since the originalThreadIndex of each choice needs to be set + boolean hasChoiceThreads = false; + for (Choice c : currentChoices) { + c.originalThreadIndex = c.getThreadAtGeneration().threadIndex; + + if (callStack.getThreadWithIndex(c.originalThreadIndex) == null) { + if (!hasChoiceThreads) { + hasChoiceThreads = true; + writer.writePropertyStart("choiceThreads"); + writer.writeObjectStart(); + } + + writer.writePropertyStart(c.originalThreadIndex); + c.getThreadAtGeneration().writeJson(writer); + writer.writePropertyEnd(); + } + } + + if (hasChoiceThreads) { + writer.writeObjectEnd(); + writer.writePropertyEnd(); + } + + writer.writeProperty("currentChoices", new SimpleJson.InnerWriter() { + @Override + public void write(Writer w) throws Exception { + w.writeArrayStart(); + for (Choice c : currentChoices) Json.writeChoice(w, c); + w.writeArrayEnd(); + } + }); + + writer.writeObjectEnd(); + } + + // Used both to load old format and current + @SuppressWarnings("unchecked") + public void loadFlowChoiceThreads(HashMap jChoiceThreads, Story story) throws Exception { + for (Choice choice : currentChoices) { + CallStack.Thread foundActiveThread = callStack.getThreadWithIndex(choice.originalThreadIndex); + if (foundActiveThread != null) { + choice.setThreadAtGeneration(foundActiveThread.copy()); + } else { + HashMap jSavedChoiceThread = + (HashMap) jChoiceThreads.get(Integer.toString(choice.originalThreadIndex)); + choice.setThreadAtGeneration(new CallStack.Thread(jSavedChoiceThread, story)); + } + } + } +} diff --git a/src/main/java/com/bladecoder/ink/runtime/Glue.java b/src/main/java/com/bladecoder/ink/runtime/Glue.java index 957bbc1..2b697b1 100644 --- a/src/main/java/com/bladecoder/ink/runtime/Glue.java +++ b/src/main/java/com/bladecoder/ink/runtime/Glue.java @@ -1,11 +1,10 @@ -package com.bladecoder.ink.runtime; - -public class Glue extends RTObject { - public Glue() { } - - @Override - public String toString() { - return "Glue"; - } - -} +package com.bladecoder.ink.runtime; + +public class Glue extends RTObject { + public Glue() {} + + @Override + public String toString() { + return "Glue"; + } +} diff --git a/src/main/java/com/bladecoder/ink/runtime/INamedContent.java b/src/main/java/com/bladecoder/ink/runtime/INamedContent.java index 42a8b41..2cc76de 100644 --- a/src/main/java/com/bladecoder/ink/runtime/INamedContent.java +++ b/src/main/java/com/bladecoder/ink/runtime/INamedContent.java @@ -1,7 +1,7 @@ -package com.bladecoder.ink.runtime; - -public interface INamedContent { - String getName(); - - boolean hasValidName(); -} +package com.bladecoder.ink.runtime; + +public interface INamedContent { + String getName(); + + boolean hasValidName(); +} diff --git a/src/main/java/com/bladecoder/ink/runtime/InkList.java b/src/main/java/com/bladecoder/ink/runtime/InkList.java index bf114dd..88c860e 100644 --- a/src/main/java/com/bladecoder/ink/runtime/InkList.java +++ b/src/main/java/com/bladecoder/ink/runtime/InkList.java @@ -3,9 +3,10 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; -import java.util.Map; import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; /** * The InkList is the underlying type that's used to store an instance of a list @@ -15,574 +16,606 @@ */ @SuppressWarnings("serial") public class InkList extends HashMap { - // Story has to set this so that the value knows its origin, - // necessary for certain operations (e.g. interacting with ints). - // Only the story has access to the full set of lists, so that - // the origin can be resolved from the originListName. - private List origins; - - // Origin name needs to be serialised when content is empty, - // assuming a name is availble, for list definitions with variable - // that is currently empty. - private List originNames; - - /** - * Create a new empty ink list. - */ - public InkList() { - } - - public InkList(Map.Entry singleElement) { - put(singleElement.getKey(), singleElement.getValue()); - } - - /** - * Create a new empty ink list that's intended to hold items from a particular - * origin list definition. The origin Story is needed in order to be able to - * look up that definition. - */ - public InkList(String singleOriginListName, Story originStory) throws Exception { - setInitialOriginName(singleOriginListName); - - ListDefinition def = originStory.getListDefinitions().getListDefinition(singleOriginListName); - - if (def != null) { - origins = new ArrayList(); - origins.add(def); - } else - throw new Exception( - "InkList origin could not be found in story when constructing new list: " + singleOriginListName); - } - - /** - * Create a new ink list that contains the same contents as another list. - */ - public InkList(InkList otherList) { - super(otherList); - this.originNames = otherList.originNames; - } - - ListDefinition getOriginOfMaxItem() { - if (origins == null) - return null; - - String maxOriginName = getMaxItem().getKey().getOriginName(); - for (ListDefinition origin : origins) { - if (origin.getName().equals(maxOriginName)) - return origin; - } - - return null; - } - - public void setOrigins(List origins) { - this.origins = origins; - } - - public List getOrigins() { - return origins; - } - - public List getOriginNames() { - if (this.size() > 0) { - if (originNames == null && this.size() > 0) - originNames = new ArrayList(); - else - originNames.clear(); - - for (InkListItem itemAndValue : keySet()) - originNames.add(itemAndValue.getOriginName()); - } - - return originNames; - } - - void setInitialOriginName(String initialOriginName) { - originNames = new ArrayList(); - originNames.add(initialOriginName); - } - - void setInitialOriginNames(List initialOriginNames) { - if (initialOriginNames == null) - originNames = null; - else { - originNames = new ArrayList(); - originNames.addAll(initialOriginNames); - } - } - - /** - * Returns a new list that is the combination of the current list and one that's - * passed in. Equivalent to calling (list1 + list2) in ink. - */ - public InkList union(InkList otherList) { - InkList union = new InkList(this); - for (InkListItem key : otherList.keySet()) - union.put(key, otherList.get(key)); - - return union; - } - - /** - * Returns a new list that's the same as the current one, except with the given - * items removed that are in the passed in list. Equivalent to calling (list1 - - * list2) in ink. - * - * @param listToRemove - * List to remove. - */ - - public InkList without(InkList listToRemove) { - InkList result = new InkList(this); - for (InkListItem kv : listToRemove.keySet()) - result.remove(kv); - - return result; - } - - /** - * Returns a new list that is the intersection of the current list with another - * list that's passed in - i.e. a list of the items that are shared between the - * two other lists. Equivalent to calling (list1 ^ list2) in ink. - */ - public InkList intersect(InkList otherList) { - InkList intersection = new InkList(); - - for (Map.Entry kv : this.entrySet()) { - if (otherList.containsKey(kv.getKey())) - intersection.put(kv.getKey(), kv.getValue()); - } - - return intersection; - } - - /** - * Get the maximum item in the list, equivalent to calling LIST_MAX(list) in - * ink. - */ - public Map.Entry getMaxItem() { - CustomEntry max = new CustomEntry(null, 0); - - for (Map.Entry kv : this.entrySet()) { - if (max.getKey() == null || kv.getValue() > max.getValue()) { - max.set(kv); - } - } - - return max; - } - - /** - * Get the minimum item in the list, equivalent to calling LIST_MIN(list) in - * ink. - */ - public Map.Entry getMinItem() { - CustomEntry min = new CustomEntry(null, 0); - - for (Map.Entry kv : this.entrySet()) { - if (min.getKey() == null || kv.getValue() < min.getValue()) - min.set(kv); - } - - return min; - } - - /** - * Returns true if the current list contains all the items that are in the list - * that is passed in. Equivalent to calling (list1 ? list2) in ink. - * - * @param otherList - * Other list. - */ - public boolean contains(InkList otherList) { - for (Map.Entry kv : otherList.entrySet()) { - if (!this.containsKey(kv.getKey())) - return false; - } - - return true; - } - - /** - * Returns true if all the item values in the current list are greater than all - * the item values in the passed in list. Equivalent to calling (list1 > - * list2) in ink. - */ - public boolean greaterThan(InkList otherList) { - if (size() == 0) - return false; - if (otherList.size() == 0) - return true; - - // All greater - return getMinItem().getValue() > otherList.getMaxItem().getValue(); - } - - /** - * Returns true if the item values in the current list overlap or are all - * greater than the item values in the passed in list. None of the item values - * in the current list must fall below the item values in the passed in list. - * Equivalent to (list1 >= list2) in ink, or LIST_MIN(list1) >= - * LIST_MIN(list2) && LIST_MAX(list1) >= LIST_MAX(list2). - */ - public boolean greaterThanOrEquals(InkList otherList) { - if (size() == 0) - return false; - if (otherList.size() == 0) - return true; - - // All greater - return getMinItem().getValue() >= otherList.getMinItem().getValue() - && getMaxItem().getValue() >= otherList.getMaxItem().getValue(); - } - - /** - * Returns true if all the item values in the current list are less than all the - * item values in the passed in list. Equivalent to calling (list1 < list2) - * in ink. - */ - public boolean lessThan(InkList otherList) { - if (otherList.size() == 0) - return false; - if (size() == 0) - return true; - - return getMaxItem().getValue() < otherList.getMinItem().getValue(); - } - - /** - * Returns true if the item values in the current list overlap or are all less - * than the item values in the passed in list. None of the item values in the - * current list must go above the item values in the passed in list. Equivalent - * to (list1 <= list2) in ink, or LIST_MAX(list1) <= LIST_MAX(list2) - * && LIST_MIN(list1) <= LIST_MIN(list2). - */ - public boolean lessThanOrEquals(InkList otherList) { - if (otherList.size() == 0) - return false; - if (size() == 0) - return true; - - return getMaxItem().getValue() <= otherList.getMaxItem().getValue() - && getMinItem().getValue() <= otherList.getMinItem().getValue(); - } - - InkList maxAsList() { - if (size() > 0) - return new InkList(getMaxItem()); - else - return new InkList(); - } - - InkList minAsList() { - if (size() > 0) - return new InkList(getMinItem()); - else - return new InkList(); - } - - /** - * Returns a sublist with the elements given the minimum and maxmimum bounds. - * The bounds can either be ints which are indices into the entire (sorted) - * list, or they can be InkLists themselves. These are intended to be - * single-item lists so you can specify the upper and lower bounds. If you pass - * in multi-item lists, it'll use the minimum and maximum items in those lists - * respectively. WARNING: Calling this method requires a full sort of all the - * elements in the list. - * - * @throws Exception - * @throws StoryException - */ - public InkList listWithSubRange(Object minBound, Object maxBound) throws StoryException, Exception { - if (this.size() == 0) - return new InkList(); - - List> ordered = getOrderedItems(); - int minValue = 0; - int maxValue = Integer.MAX_VALUE; - - if (minBound instanceof Integer) { - minValue = (int) minBound; - } else { - if (minBound instanceof InkList && ((InkList) minBound).size() > 0) - minValue = ((InkList) minBound).getMinItem().getValue(); - } - - if (maxBound instanceof Integer) - maxValue = (int) maxBound; - else { - if (minBound instanceof InkList && ((InkList) minBound).size() > 0) - maxValue = ((InkList) maxBound).getMaxItem().getValue(); - } - - InkList subList = new InkList(); - subList.setInitialOriginNames(originNames); - - for (Entry item : ordered) { - if (item.getValue() >= minValue && item.getValue() <= maxValue) { - subList.put(item.getKey(), item.getValue()); - } - } - - return subList; - } - - // Runtime sets may reference items from different origin sets - public String getSingleOriginListName() { - String name = null; - - for (Map.Entry itemAndValue : entrySet()) { - String originName = itemAndValue.getKey().getOriginName(); - - // First name - take it as the assumed single origin name - if (name == null) - name = originName; - - // A different one than one we've already had? No longer - // single origin. - else if (name != originName) - return null; - } - - return name; - } - - /** - * The inverse of the list, equivalent to calling LIST_INVERSE(list) in ink - */ - public InkList getInverse() { - - InkList rawList = new InkList(); - - if (origins != null) { - for (ListDefinition origin : origins) { - for (Map.Entry itemAndValue : origin.getItems().entrySet()) { - - if (!this.containsKey(itemAndValue.getKey())) - rawList.put(itemAndValue.getKey(), itemAndValue.getValue()); - } - } - } - - return rawList; - - } - - /** - * The list of all items from the original list definition, equivalent to - * calling LIST_ALL(list) in ink. - */ - public InkList getAll() { - - InkList list = new InkList(); - - if (origins != null) { - for (ListDefinition origin : origins) { - for (Map.Entry kv : origin.getItems().entrySet()) { - list.put(kv.getKey(), kv.getValue()); - } - } - } - - return list; - } - - /** - * Adds the given item to the ink list. Note that the item must come from a list - * definition that is already "known" to this list, so that the item's value can - * be looked up. By "known", we mean that it already has items in it from that - * source, or it did at one point - it can't be a completely fresh empty list, - * or a list that only contains items from a different list definition. - * - * @throws Exception - */ - public void addItem(InkListItem item) throws Exception { - if (item.getOriginName() == null) { - addItem(item.getItemName()); - return; - } - - for (ListDefinition origin : origins) { - if (origin.getName().equals(item.getOriginName())) { - Integer intVal = origin.getValueForItem(item); - - if (intVal != null) { - this.put(item, intVal); - return; - } else { - throw new Exception("Could not add the item " + item - + " to this list because it doesn't exist in the original list definition in ink."); - } - } - } - - throw new Exception( - "Failed to add item to list because the item was from a new list definition that wasn't previously known to this list. Only items from previously known lists can be used, so that the int value can be found."); - } - - /** - * Adds the given item to the ink list, attempting to find the origin list - * definition that it belongs to. The item must therefore come from a list - * definition that is already "known" to this list, so that the item's value can - * be looked up. By "known", we mean that it already has items in it from that - * source, or it did at one point - it can't be a completely fresh empty list, - * or a list that only contains items from a different list definition. - * - * @throws Exception - */ - public void addItem(String itemName) throws Exception { - ListDefinition foundListDef = null; - - for (ListDefinition origin : origins) { - if (origin.containsItemWithName(itemName)) { - if (foundListDef != null) { - throw new Exception( - "Could not add the item " + itemName + " to this list because it could come from either " - + origin.getName() + " or " + foundListDef.getName()); - } else { - foundListDef = origin; - } - } - } - - if (foundListDef == null) - throw new Exception("Could not add the item " + itemName - + " to this list because it isn't known to any list definitions previously associated with this list."); - - InkListItem item = new InkListItem(foundListDef.getName(), itemName); - Integer itemVal = foundListDef.getValueForItem(item); - this.put(item, itemVal != null ? itemVal : 0); - } - - /** - * Returns true if this ink list contains an item with the given short name - * (ignoring the original list where it was defined). - */ - public boolean ContainsItemNamed(String itemName) { - for (Map.Entry itemWithValue : this.entrySet()) { - if (itemWithValue.getKey().getItemName().equals(itemName)) - return true; - } - return false; - } - - /** - * Returns true if the passed object is also an ink list that contains the same - * items as the current list, false otherwise. - */ - @Override - public boolean equals(Object other) { - InkList otherRawList = null; - - if (other instanceof InkList) - otherRawList = (InkList) other; - - if (otherRawList == null) - return false; - if (otherRawList.size() != size()) - return false; - - for (InkListItem key : keySet()) { - if (!otherRawList.containsKey(key)) - return false; - } - - return true; - } - - /** - * Return the hashcode for this object, used for comparisons and inserting into - * dictionaries. - */ - @Override - public int hashCode() { - int ownHash = 0; - - for (InkListItem key : keySet()) - ownHash += key.hashCode(); - - return ownHash; - } - - List> getOrderedItems() { - List> ordered = new ArrayList>(entrySet()); - - Collections.sort(ordered, new Comparator>() { - @Override - public int compare(Entry o1, Entry o2) { - if (o1.getValue() == o2.getValue()) { - return o1.getKey().getOriginName().compareTo(o2.getKey().getOriginName()); - } else { - return o1.getValue() - o2.getValue(); - } - } - }); - - return ordered; - } - - /** - * Returns a string in the form "a, b, c" with the names of the items in the - * list, without the origin list definition names. Equivalent to writing {list} - * in ink. - */ - @Override - public String toString() { - List> ordered = getOrderedItems(); - - StringBuilder sb = new StringBuilder(); - - for (int i = 0; i < ordered.size(); i++) { - if (i > 0) - sb.append(", "); - - InkListItem item = ordered.get(i).getKey(); - - sb.append(item.getItemName()); - } - - return sb.toString(); - } - - public class CustomEntry implements Map.Entry { - - private InkListItem key; - private Integer value; - - CustomEntry(InkListItem key, Integer value) { - set(key, value); - } - - public void set(InkListItem key, Integer value) { - this.key = key; - this.value = value; - } - - public void set(Map.Entry e) { - key = e.getKey(); - value = e.getValue(); - } - - @Override - public InkListItem getKey() { - return key; - } - - @Override - public Integer getValue() { - return value; - } - - @Override - public Integer setValue(Integer value) { - Integer old = this.value; - this.value = value; - - return old; - } - - public void setKey(InkListItem key) { - this.key = key; - } - } + // Story has to set this so that the value knows its origin, + // necessary for certain operations (e.g. interacting with ints). + // Only the story has access to the full set of lists, so that + // the origin can be resolved from the originListName. + private List origins; + + // Origin name needs to be serialised when content is empty, + // assuming a name is availble, for list definitions with variable + // that is currently empty. + private List originNames; + + /** + * Create a new empty ink list. + */ + public InkList() {} + + public InkList(Map.Entry singleElement) { + put(singleElement.getKey(), singleElement.getValue()); + } + + /** + * Create a new empty ink list that's intended to hold items from a particular + * origin list definition. The origin Story is needed in order to be able to + * look up that definition. + */ + public InkList(String singleOriginListName, Story originStory) throws Exception { + setInitialOriginName(singleOriginListName); + + ListDefinition def = originStory.getListDefinitions().getListDefinition(singleOriginListName); + + if (def != null) { + origins = new ArrayList<>(); + origins.add(def); + } else + throw new Exception( + "InkList origin could not be found in story when constructing new list: " + singleOriginListName); + } + + /** + * Create a new ink list that contains the same contents as another list. + */ + public InkList(InkList otherList) { + super(otherList); + + if (otherList.originNames != null) this.originNames = new ArrayList<>(otherList.originNames); + + if (otherList.origins != null) { + origins = new ArrayList<>(otherList.origins); + } + } + + /** + * Converts a string to an ink list and returns for use in the story. + */ + public static InkList fromString(String myListItem, Story originStory) throws Exception { + if (myListItem == null || myListItem.isEmpty()) return new InkList(); + + ListValue listValue = originStory.getListDefinitions().findSingleItemListWithName(myListItem); + if (listValue != null) return new InkList(listValue.value); + else + throw new Exception("Could not find the InkListItem from the string '" + myListItem + + "' to create an InkList because it doesn't exist in the original list definition in ink."); + } + + ListDefinition getOriginOfMaxItem() { + if (origins == null) return null; + + String maxOriginName = getMaxItem().getKey().getOriginName(); + for (ListDefinition origin : origins) { + if (origin.getName().equals(maxOriginName)) return origin; + } + + return null; + } + + public void setOrigins(List origins) { + this.origins = origins; + } + + public List getOrigins() { + return origins; + } + + public List getOriginNames() { + if (this.size() > 0) { + if (originNames == null && this.size() > 0) originNames = new ArrayList<>(); + else originNames.clear(); + + for (InkListItem itemAndValue : keySet()) originNames.add(itemAndValue.getOriginName()); + } + + return originNames; + } + + void setInitialOriginName(String initialOriginName) { + originNames = new ArrayList<>(); + originNames.add(initialOriginName); + } + + void setInitialOriginNames(List initialOriginNames) { + if (initialOriginNames == null) originNames = null; + else { + originNames = new ArrayList<>(); + originNames.addAll(initialOriginNames); + } + } + + /** + * Returns a new list that is the combination of the current list and one that's + * passed in. Equivalent to calling (list1 + list2) in ink. + */ + public InkList union(InkList otherList) { + InkList union = new InkList(this); + for (InkListItem key : otherList.keySet()) union.put(key, otherList.get(key)); + + return union; + } + + /** + * Returns a new list that's the same as the current one, except with the given + * items removed that are in the passed in list. Equivalent to calling (list1 - + * list2) in ink. + * + * @param listToRemove List to remove. + */ + public InkList without(InkList listToRemove) { + InkList result = new InkList(this); + for (InkListItem kv : listToRemove.keySet()) result.remove(kv); + + return result; + } + + /** + * Returns a new list that is the intersection of the current list with another + * list that's passed in - i.e. a list of the items that are shared between the + * two other lists. Equivalent to calling (list1 ^ list2) in ink. + */ + public InkList intersect(InkList otherList) { + InkList intersection = new InkList(); + + for (Map.Entry kv : this.entrySet()) { + if (otherList.containsKey(kv.getKey())) intersection.put(kv.getKey(), kv.getValue()); + } + + return intersection; + } + + /** + * Fast test for the existence of any intersection between the current list and another + */ + public boolean hasIntersection(InkList otherList) { + for (Map.Entry kv : this.entrySet()) { + if (otherList.containsKey(kv.getKey())) return true; + } + return false; + } + + /** + * Get the maximum item in the list, equivalent to calling LIST_MAX(list) in + * ink. + */ + public Map.Entry getMaxItem() { + CustomEntry max = new CustomEntry(null, 0); + + for (Map.Entry kv : this.entrySet()) { + if (max.getKey() == null || kv.getValue() > max.getValue()) { + max.set(kv); + } + } + + return max; + } + + /** + * Get the minimum item in the list, equivalent to calling LIST_MIN(list) in + * ink. + */ + public Map.Entry getMinItem() { + CustomEntry min = new CustomEntry(null, 0); + + for (Map.Entry kv : this.entrySet()) { + if (min.getKey() == null || kv.getValue() < min.getValue()) min.set(kv); + } + + return min; + } + + /** + * Returns true if the current list contains all the items that are in the list + * that is passed in. Equivalent to calling (list1 ? list2) in ink. + * + * @param otherList Other list. + */ + public boolean contains(InkList otherList) { + if (otherList.size() == 0 || this.size() == 0) return false; + + for (Map.Entry kv : otherList.entrySet()) { + if (!this.containsKey(kv.getKey())) return false; + } + + return true; + } + + /** + * Returns true if the current list contains an item matching the given name. + */ + public boolean contains(String listItemName) { + for (Map.Entry kv : this.entrySet()) { + if (Objects.equals(kv.getKey().getItemName(), listItemName)) return true; + } + return false; + } + + /** + * Returns true if all the item values in the current list are greater than all + * the item values in the passed in list. Equivalent to calling (list1 > + * list2) in ink. + */ + public boolean greaterThan(InkList otherList) { + if (size() == 0) return false; + if (otherList.size() == 0) return true; + + // All greater + return getMinItem().getValue() > otherList.getMaxItem().getValue(); + } + + /** + * Returns true if the item values in the current list overlap or are all + * greater than the item values in the passed in list. None of the item values + * in the current list must fall below the item values in the passed in list. + * Equivalent to (list1 >= list2) in ink, or LIST_MIN(list1) >= + * LIST_MIN(list2) && LIST_MAX(list1) >= LIST_MAX(list2). + */ + public boolean greaterThanOrEquals(InkList otherList) { + if (size() == 0) return false; + if (otherList.size() == 0) return true; + + // All greater + return getMinItem().getValue() >= otherList.getMinItem().getValue() + && getMaxItem().getValue() >= otherList.getMaxItem().getValue(); + } + + /** + * Returns true if all the item values in the current list are less than all the + * item values in the passed in list. Equivalent to calling (list1 < list2) + * in ink. + */ + public boolean lessThan(InkList otherList) { + if (otherList.size() == 0) return false; + if (size() == 0) return true; + + return getMaxItem().getValue() < otherList.getMinItem().getValue(); + } + + /** + * Returns true if the item values in the current list overlap or are all less + * than the item values in the passed in list. None of the item values in the + * current list must go above the item values in the passed in list. Equivalent + * to (list1 <= list2) in ink, or LIST_MAX(list1) <= LIST_MAX(list2) + * && LIST_MIN(list1) <= LIST_MIN(list2). + */ + public boolean lessThanOrEquals(InkList otherList) { + if (otherList.size() == 0) return false; + if (size() == 0) return true; + + return getMaxItem().getValue() <= otherList.getMaxItem().getValue() + && getMinItem().getValue() <= otherList.getMinItem().getValue(); + } + + InkList maxAsList() { + if (size() > 0) return new InkList(getMaxItem()); + else return new InkList(); + } + + InkList minAsList() { + if (size() > 0) return new InkList(getMinItem()); + else return new InkList(); + } + + /** + * Returns a sublist with the elements given the minimum and maxmimum bounds. + * The bounds can either be ints which are indices into the entire (sorted) + * list, or they can be InkLists themselves. These are intended to be + * single-item lists so you can specify the upper and lower bounds. If you pass + * in multi-item lists, it'll use the minimum and maximum items in those lists + * respectively. WARNING: Calling this method requires a full sort of all the + * elements in the list. + * + * @throws Exception + * @throws StoryException + */ + public InkList listWithSubRange(Object minBound, Object maxBound) throws StoryException, Exception { + if (this.size() == 0) return new InkList(); + + List> ordered = getOrderedItems(); + int minValue = 0; + int maxValue = Integer.MAX_VALUE; + + if (minBound instanceof Integer) { + minValue = (int) minBound; + } else { + if (minBound instanceof InkList && ((InkList) minBound).size() > 0) + minValue = ((InkList) minBound).getMinItem().getValue(); + } + + if (maxBound instanceof Integer) maxValue = (int) maxBound; + else { + if (maxBound instanceof InkList && ((InkList) maxBound).size() > 0) + maxValue = ((InkList) maxBound).getMaxItem().getValue(); + } + + InkList subList = new InkList(); + subList.setInitialOriginNames(originNames); + + for (Entry item : ordered) { + if (item.getValue() >= minValue && item.getValue() <= maxValue) { + subList.put(item.getKey(), item.getValue()); + } + } + + return subList; + } + + // Runtime sets may reference items from different origin sets + public String getSingleOriginListName() { + String name = null; + + for (Map.Entry itemAndValue : entrySet()) { + String originName = itemAndValue.getKey().getOriginName(); + + // First name - take it as the assumed single origin name + if (name == null) name = originName; + + // A different one than one we've already had? No longer + // single origin. + else if (name != originName) return null; + } + + return name; + } + + /** + * If you have an InkList that's known to have one single item, this is a convenient way to get it. + */ + public InkListItem getSingleItem() { + for (Map.Entry item : this.entrySet()) { + return item.getKey(); + } + + return null; + } + + /** + * The inverse of the list, equivalent to calling LIST_INVERSE(list) in ink + */ + public InkList getInverse() { + + InkList rawList = new InkList(); + + if (origins != null) { + for (ListDefinition origin : origins) { + for (Map.Entry itemAndValue : + origin.getItems().entrySet()) { + + if (!this.containsKey(itemAndValue.getKey())) + rawList.put(itemAndValue.getKey(), itemAndValue.getValue()); + } + } + } + + return rawList; + } + + /** + * The list of all items from the original list definition, equivalent to + * calling LIST_ALL(list) in ink. + */ + public InkList getAll() { + + InkList list = new InkList(); + + if (origins != null) { + for (ListDefinition origin : origins) { + for (Map.Entry kv : origin.getItems().entrySet()) { + list.put(kv.getKey(), kv.getValue()); + } + } + } + + return list; + } + + /** + * Adds the given item to the ink list. Note that the item must come from a list + * definition that is already "known" to this list, so that the item's value can + * be looked up. + * By "known", we mean that it already has items in it from that + * source, or it did at one point - it can't be a completely fresh empty list, + * or a list that only contains items from a different list definition. + * + * @throws Exception + */ + public void addItem(InkListItem item) throws Exception { + if (item.getOriginName() == null) { + addItem(item.getItemName()); + return; + } + + for (ListDefinition origin : origins) { + if (origin.getName().equals(item.getOriginName())) { + Integer intVal = origin.getValueForItem(item); + + if (intVal != null) { + this.put(item, intVal); + return; + } else { + throw new Exception("Could not add the item " + item + + " to this list because it doesn't exist in the original list definition in ink."); + } + } + } + + throw new Exception( + "Failed to add item to list because the item was from a new list definition that wasn't previously " + + "known to this list. Only items from previously known lists can be used, so that the int " + + "value" + + " can be found."); + } + + /** + * Adds the given item to the ink list, attempting to find the origin list + * definition that it belongs to. The item must therefore come from a list + * definition that is already "known" to this list, so that the item's value can + * be looked up. By "known", we mean that it already has items in it from that + * source, or it did at one point - it can't be a completely fresh empty list, + * or a list that only contains items from a different list definition. + * You can also provide the Story object, so in the case of an unknown element, it can be created fresh. + * + * @throws Exception + */ + public void addItem(String itemName, Story storyObject) throws Exception { + ListDefinition foundListDef = null; + + if (origins != null) { + for (ListDefinition origin : origins) { + if (origin.containsItemWithName(itemName)) { + if (foundListDef != null) { + throw new Exception("Could not add the item " + itemName + + " to this list because it could come from either " + origin.getName() + " or " + + foundListDef.getName()); + } else { + foundListDef = origin; + } + } + } + } + + if (foundListDef == null) { + if (storyObject == null) { + throw new Exception("Could not add the item " + itemName + + " to this list because it isn't known to any list definitions previously associated with this " + + "list."); + } else { + Entry newItem = + fromString(itemName, storyObject).getOrderedItems().get(0); + this.put(newItem.getKey(), newItem.getValue()); + } + } else { + InkListItem item = new InkListItem(foundListDef.getName(), itemName); + Integer itemVal = foundListDef.getValueForItem(item); + this.put(item, itemVal != null ? itemVal : 0); + } + } + + public void addItem(String itemName) throws Exception { + addItem(itemName, null); + } + + /** + * Returns true if this ink list contains an item with the given short name + * (ignoring the original list where it was defined). + */ + public boolean containsItemNamed(String itemName) { + for (Map.Entry itemWithValue : this.entrySet()) { + if (itemWithValue.getKey().getItemName().equals(itemName)) return true; + } + return false; + } + + /** + * Returns true if the passed object is also an ink list that contains the same + * items as the current list, false otherwise. + */ + @Override + public boolean equals(Object other) { + InkList otherRawList = null; + + if (other instanceof InkList) otherRawList = (InkList) other; + + if (otherRawList == null) return false; + if (otherRawList.size() != size()) return false; + + for (InkListItem key : keySet()) { + if (!otherRawList.containsKey(key)) return false; + } + + return true; + } + + /** + * Return the hashcode for this object, used for comparisons and inserting into + * dictionaries. + */ + @Override + public int hashCode() { + int ownHash = 0; + + for (InkListItem key : keySet()) ownHash += key.hashCode(); + + return ownHash; + } + + List> getOrderedItems() { + List> ordered = new ArrayList<>(entrySet()); + + Collections.sort(ordered, new Comparator>() { + @Override + public int compare(Entry o1, Entry o2) { + if (o1.getValue() == o2.getValue()) { + return o1.getKey().getOriginName().compareTo(o2.getKey().getOriginName()); + } else { + return o1.getValue() - o2.getValue(); + } + } + }); + + return ordered; + } + + /** + * Returns a string in the form "a, b, c" with the names of the items in the + * list, without the origin list definition names. Equivalent to writing {list} + * in ink. + */ + @Override + public String toString() { + List> ordered = getOrderedItems(); + + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < ordered.size(); i++) { + if (i > 0) sb.append(", "); + + InkListItem item = ordered.get(i).getKey(); + + sb.append(item.getItemName()); + } + + return sb.toString(); + } + + public static class CustomEntry implements Map.Entry { + + private InkListItem key; + private Integer value; + + CustomEntry(InkListItem key, Integer value) { + set(key, value); + } + + public void set(InkListItem key, Integer value) { + this.key = key; + this.value = value; + } + + public void set(Map.Entry e) { + key = e.getKey(); + value = e.getValue(); + } + + @Override + public InkListItem getKey() { + return key; + } + + @Override + public Integer getValue() { + return value; + } + + @Override + public Integer setValue(Integer value) { + Integer old = this.value; + this.value = value; + + return old; + } + + public void setKey(InkListItem key) { + this.key = key; + } + } } diff --git a/src/main/java/com/bladecoder/ink/runtime/InkListItem.java b/src/main/java/com/bladecoder/ink/runtime/InkListItem.java index 6aa07d7..fd23c0d 100644 --- a/src/main/java/com/bladecoder/ink/runtime/InkListItem.java +++ b/src/main/java/com/bladecoder/ink/runtime/InkListItem.java @@ -7,91 +7,90 @@ * int. */ public class InkListItem { - /** - * The name of the list where the item was originally defined. - */ - private String originName; + /** + * The name of the list where the item was originally defined. + */ + private String originName; - /** - * The main name of the item as defined in ink. - */ - private String itemName; + /** + * The main name of the item as defined in ink. + */ + private String itemName; - /** - * Create an item with the given original list definition name, and the name - * of this item. - */ - public InkListItem(String originName, String itemName) { - this.originName = originName; - this.itemName = itemName; - } + /** + * Create an item with the given original list definition name, and the name + * of this item. + */ + public InkListItem(String originName, String itemName) { + this.originName = originName; + this.itemName = itemName; + } - /** - * Create an item from a dot-separted string of the form - * "listDefinitionName.listItemName". - */ - public InkListItem(String fullName) { - String[] nameParts = fullName.split("\\."); - this.originName = nameParts[0]; - this.itemName = nameParts[1]; - } + /** + * Create an item from a dot-separted string of the form + * "listDefinitionName.listItemName". + */ + public InkListItem(String fullName) { + String[] nameParts = fullName.split("\\."); + this.originName = nameParts[0]; + this.itemName = nameParts[1]; + } - static InkListItem getNull() { - return new InkListItem(null, null); - } + static InkListItem getNull() { + return new InkListItem(null, null); + } - public String getOriginName() { - return originName; - } + public String getOriginName() { + return originName; + } - public String getItemName() { - return itemName; - } + public String getItemName() { + return itemName; + } - /** - * Get the full dot-separated name of the item, in the form - * "listDefinitionName.itemName". - */ - public String getFullName() { - return (originName != null ? originName : "?") + "." + itemName; - } + /** + * Get the full dot-separated name of the item, in the form + * "listDefinitionName.itemName". + */ + public String getFullName() { + return (originName != null ? originName : "?") + "." + itemName; + } - boolean isNull() { - return originName == null && itemName == null; - } + boolean isNull() { + return originName == null && itemName == null; + } - /** - * Get the full dot-separated name of the item, in the form - * "listDefinitionName.itemName". Calls fullName internally. - */ - @Override - public String toString() { - return getFullName(); - } + /** + * Get the full dot-separated name of the item, in the form + * "listDefinitionName.itemName". Calls fullName internally. + */ + @Override + public String toString() { + return getFullName(); + } - /** - * Is this item the same as another item? - */ - @Override - public boolean equals(Object obj) { - if (obj instanceof InkListItem) { - InkListItem otherItem = (InkListItem) obj; - return otherItem.itemName.equals(itemName) && otherItem.originName.equals(originName); - } + /** + * Is this item the same as another item? + */ + @Override + public boolean equals(Object obj) { + if (obj instanceof InkListItem) { + InkListItem otherItem = (InkListItem) obj; + return otherItem.itemName.equals(itemName) && otherItem.originName.equals(originName); + } - return false; - } + return false; + } - /** - * Get the hashcode for an item. - */ - @Override - public int hashCode() { - int originCode = 0; - int itemCode = itemName.hashCode(); - if (originName != null) - originCode = originName.hashCode(); + /** + * Get the hashcode for an item. + */ + @Override + public int hashCode() { + int originCode = 0; + int itemCode = itemName.hashCode(); + if (originName != null) originCode = originName.hashCode(); - return originCode + itemCode; - } + return originCode + itemCode; + } } diff --git a/src/main/java/com/bladecoder/ink/runtime/IntValue.java b/src/main/java/com/bladecoder/ink/runtime/IntValue.java index bf03272..478a98f 100644 --- a/src/main/java/com/bladecoder/ink/runtime/IntValue.java +++ b/src/main/java/com/bladecoder/ink/runtime/IntValue.java @@ -1,39 +1,42 @@ -package com.bladecoder.ink.runtime; - -class IntValue extends Value { - public IntValue() { - this(0); - } - - public IntValue(int intVal) { - super(intVal); - } - - @Override - public AbstractValue cast(ValueType newType) throws Exception { - if (newType == getValueType()) { - return this; - } - - if (newType == ValueType.Float) { - return new FloatValue(this.getValue()); - } - - if (newType == ValueType.String) { - return new StringValue(this.getValue().toString()); - } - - throw BadCastException (newType); - } - - @Override - public boolean isTruthy() { - return getValue() != 0; - } - - @Override - public ValueType getValueType() { - return ValueType.Int; - } - -} +package com.bladecoder.ink.runtime; + +class IntValue extends Value { + public IntValue() { + this(0); + } + + public IntValue(int intVal) { + super(intVal); + } + + @Override + public AbstractValue cast(ValueType newType) throws Exception { + if (newType == getValueType()) { + return this; + } + + if (newType == ValueType.Bool) { + return new BoolValue(this.value == 0 ? false : true); + } + + if (newType == ValueType.Float) { + return new FloatValue(this.getValue()); + } + + if (newType == ValueType.String) { + return new StringValue(this.getValue().toString()); + } + + throw BadCastException(newType); + } + + @Override + public boolean isTruthy() { + return getValue() != 0; + } + + @Override + public ValueType getValueType() { + return ValueType.Int; + } +} diff --git a/src/main/java/com/bladecoder/ink/runtime/Json.java b/src/main/java/com/bladecoder/ink/runtime/Json.java index e84253e..d5198cc 100644 --- a/src/main/java/com/bladecoder/ink/runtime/Json.java +++ b/src/main/java/com/bladecoder/ink/runtime/Json.java @@ -1,765 +1,751 @@ -package com.bladecoder.ink.runtime; - -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 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) { - 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"); - } - - } -} +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; + +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() - 1]); + 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); + + // 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.isEmpty() || countFlags > 0 || hasNameProperty; + + if (hasTerminator) writer.writeObjectStart(); + + 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.addContents(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 : 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()); + choice.tags = jArrayToTags(jObj, choice); + return choice; + } + + private static List jArrayToTags(HashMap jObj, Choice choice) { + Object jArray = jObj.get("tags"); + if (jArray == null) return null; + + List tags = new ArrayList<>(); + for (Object stringValue : (List) jArray) { + tags.add(stringValue.toString()); + } + + return tags; + } + + 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()); + writeChoiceTags(writer, choice); + writer.writeObjectEnd(); + } + + private static void writeChoiceTags(SimpleJson.Writer writer, Choice choice) throws Exception { + if (choice.tags == null || choice.tags.isEmpty()) return; + writer.writePropertyStart("tags"); + writer.writeArrayStart(); + for (String tag : choice.tags) { + writer.write(tag); + } + + writer.writeArrayEnd(); + writer.writePropertyEnd(); + } + + 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 static final 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 RuntimeException("Control command (index " + i + ") not accounted for in serialisation"); + } + } +} diff --git a/src/main/java/com/bladecoder/ink/runtime/ListDefinition.java b/src/main/java/com/bladecoder/ink/runtime/ListDefinition.java index ad60926..d9075c2 100644 --- a/src/main/java/com/bladecoder/ink/runtime/ListDefinition.java +++ b/src/main/java/com/bladecoder/ink/runtime/ListDefinition.java @@ -4,61 +4,60 @@ import java.util.Map.Entry; public class ListDefinition { - private String name; - private HashMap items; - - // The main representation should be simple item names rather than a - // RawListItem, - // since we mainly want to access items based on their simple name, since - // that's - // how they'll be most commonly requested from ink. - private HashMap itemNameToValues; - - public ListDefinition(String name, HashMap items) { - this.name = name; - this.itemNameToValues = items; - } - - public HashMap getItems() { - if (items == null) { - items = new HashMap(); - for (Entry itemNameAndValue : itemNameToValues.entrySet()) { - InkListItem item = new InkListItem(name, itemNameAndValue.getKey()); - items.put(item, itemNameAndValue.getValue()); - } - } - - return items; - } - - public String getName() { - return name; - } - - public Integer getValueForItem(InkListItem item) { - return itemNameToValues.get(item.getItemName()); - } - - public boolean containsItem(InkListItem item) { - if (!item.getOriginName().equals(name)) - return false; - - return itemNameToValues.containsKey(item.getItemName()); - } - - public boolean containsItemWithName(String itemName) { - return itemNameToValues.containsKey(itemName); - } - - public InkListItem getItemWithValue(int val) { - InkListItem item = null; - - for (Entry namedItem : itemNameToValues.entrySet()) { - if (namedItem.getValue() == val) { - return new InkListItem(name, namedItem.getKey()); - } - } - - return item; - } + private String name; + private HashMap items; + + // The main representation should be simple item names rather than a + // RawListItem, + // since we mainly want to access items based on their simple name, since + // that's + // how they'll be most commonly requested from ink. + private HashMap itemNameToValues; + + public ListDefinition(String name, HashMap items) { + this.name = name; + this.itemNameToValues = items; + } + + public HashMap getItems() { + if (items == null) { + items = new HashMap(); + for (Entry itemNameAndValue : itemNameToValues.entrySet()) { + InkListItem item = new InkListItem(name, itemNameAndValue.getKey()); + items.put(item, itemNameAndValue.getValue()); + } + } + + return items; + } + + public String getName() { + return name; + } + + public Integer getValueForItem(InkListItem item) { + return itemNameToValues.get(item.getItemName()); + } + + public boolean containsItem(InkListItem item) { + if (!item.getOriginName().equals(name)) return false; + + return itemNameToValues.containsKey(item.getItemName()); + } + + public boolean containsItemWithName(String itemName) { + return itemNameToValues.containsKey(itemName); + } + + public InkListItem getItemWithValue(int val) { + InkListItem item = null; + + for (Entry namedItem : itemNameToValues.entrySet()) { + if (namedItem.getValue() == val) { + return new InkListItem(name, namedItem.getKey()); + } + } + + return item; + } } diff --git a/src/main/java/com/bladecoder/ink/runtime/ListDefinitionsOrigin.java b/src/main/java/com/bladecoder/ink/runtime/ListDefinitionsOrigin.java index dc9b57b..c00239f 100644 --- a/src/main/java/com/bladecoder/ink/runtime/ListDefinitionsOrigin.java +++ b/src/main/java/com/bladecoder/ink/runtime/ListDefinitionsOrigin.java @@ -6,48 +6,47 @@ import java.util.Map.Entry; public class ListDefinitionsOrigin { - private HashMap lists; - private HashMap allUnambiguousListValueCache; - - public ListDefinitionsOrigin(List lists) { - this.lists = new HashMap(); - allUnambiguousListValueCache = new HashMap(); - - for (ListDefinition list : lists) { - this.lists.put(list.getName(), list); - - for (Entry itemWithValue : list.getItems().entrySet()) { - InkListItem item = itemWithValue.getKey(); - Integer val = itemWithValue.getValue(); - ListValue listValue = new ListValue(item, val); - - // May be ambiguous, but compiler should've caught that, - // so we may be doing some replacement here, but that's okay. - allUnambiguousListValueCache.put(item.getItemName(), listValue); - allUnambiguousListValueCache.put(item.getFullName(), listValue); - } - } - } - - public ListDefinition getListDefinition(String name) { - return lists.get(name); - } - - public List getLists() { - List listOfLists = new ArrayList(); - for (ListDefinition namedList : lists.values()) { - listOfLists.add(namedList); - } - - return listOfLists; - } - - ListValue findSingleItemListWithName(String name) { - ListValue val = null; - - val = allUnambiguousListValueCache.get(name); - - return val; - } + private HashMap lists; + private HashMap allUnambiguousListValueCache; + public ListDefinitionsOrigin(List lists) { + this.lists = new HashMap(); + allUnambiguousListValueCache = new HashMap(); + + for (ListDefinition list : lists) { + this.lists.put(list.getName(), list); + + for (Entry itemWithValue : list.getItems().entrySet()) { + InkListItem item = itemWithValue.getKey(); + Integer val = itemWithValue.getValue(); + ListValue listValue = new ListValue(item, val); + + // May be ambiguous, but compiler should've caught that, + // so we may be doing some replacement here, but that's okay. + allUnambiguousListValueCache.put(item.getItemName(), listValue); + allUnambiguousListValueCache.put(item.getFullName(), listValue); + } + } + } + + public ListDefinition getListDefinition(String name) { + return lists.get(name); + } + + public List getLists() { + List listOfLists = new ArrayList(); + for (ListDefinition namedList : lists.values()) { + listOfLists.add(namedList); + } + + return listOfLists; + } + + ListValue findSingleItemListWithName(String name) { + ListValue val = null; + + if (name != null && !name.trim().isEmpty()) val = allUnambiguousListValueCache.get(name); + + return val; + } } diff --git a/src/main/java/com/bladecoder/ink/runtime/ListValue.java b/src/main/java/com/bladecoder/ink/runtime/ListValue.java index b0e6482..169da1d 100644 --- a/src/main/java/com/bladecoder/ink/runtime/ListValue.java +++ b/src/main/java/com/bladecoder/ink/runtime/ListValue.java @@ -4,77 +4,64 @@ class ListValue extends Value { - public ListValue(InkList list) { - super(list); - } - - public ListValue() { - super(new InkList()); - } - - public ListValue(InkListItem singleItem, int singleValue) { - super(new InkList()); - value.put(singleItem, singleValue); - } - - @Override - public ValueType getValueType() { - return ValueType.List; - } - - // Truthy if it is non-empty - @Override - public boolean isTruthy() { - return value.size() > 0; - } - - @Override - public AbstractValue cast(ValueType newType) throws Exception { - if (newType == ValueType.Int) { - Entry max = value.getMaxItem(); - if (max.getKey().isNull()) - return new IntValue(0); - else - return new IntValue(max.getValue()); - } - - else if (newType == ValueType.Float) { - Entry max = value.getMaxItem(); - if (max.getKey().isNull()) - return new FloatValue(0.0f); - else - return new FloatValue((float) max.getValue()); - } - - else if (newType == ValueType.String) { - Entry max = value.getMaxItem(); - if (max.getKey().isNull()) - return new StringValue(""); - else { - return new StringValue(max.getKey().toString()); - } - } - - if (newType == getValueType()) - return this; - - throw BadCastException (newType); - } - - public static void retainListOriginsForAssignment(RTObject oldValue, RTObject newValue) { - ListValue oldList = null; - - if (oldValue instanceof ListValue) - oldList = (ListValue) oldValue; - - ListValue newList = null; - - if (newValue instanceof ListValue) - newList = (ListValue) newValue; - - // When assigning the emtpy list, try to retain any initial origin names - if (oldList != null && newList != null && newList.value.size() == 0) - newList.value.setInitialOriginNames(oldList.value.getOriginNames()); - } - + public ListValue(InkList list) { + super(list); + } + + public ListValue() { + super(new InkList()); + } + + public ListValue(InkListItem singleItem, int singleValue) { + super(new InkList()); + value.put(singleItem, singleValue); + } + + @Override + public ValueType getValueType() { + return ValueType.List; + } + + // Truthy if it is non-empty + @Override + public boolean isTruthy() { + return value.size() > 0; + } + + @Override + public AbstractValue cast(ValueType newType) throws Exception { + if (newType == ValueType.Int) { + Entry max = value.getMaxItem(); + if (max.getKey().isNull()) return new IntValue(0); + else return new IntValue(max.getValue()); + } else if (newType == ValueType.Float) { + Entry max = value.getMaxItem(); + if (max.getKey().isNull()) return new FloatValue(0.0f); + else return new FloatValue((float) max.getValue()); + } else if (newType == ValueType.String) { + Entry max = value.getMaxItem(); + if (max.getKey().isNull()) return new StringValue(""); + else { + return new StringValue(max.getKey().toString()); + } + } + + if (newType == getValueType()) return this; + + throw BadCastException(newType); + } + + public static void retainListOriginsForAssignment(RTObject oldValue, RTObject newValue) { + ListValue oldList = null; + + if (oldValue instanceof ListValue) oldList = (ListValue) oldValue; + + ListValue newList = null; + + if (newValue instanceof ListValue) newList = (ListValue) newValue; + + // When assigning the emtpy list, try to retain any initial origin names + if (oldList != null && newList != null && newList.value.size() == 0) + newList.value.setInitialOriginNames(oldList.value.getOriginNames()); + } } diff --git a/src/main/java/com/bladecoder/ink/runtime/NativeFunctionCall.java b/src/main/java/com/bladecoder/ink/runtime/NativeFunctionCall.java index aa6a839..f91af2c 100644 --- a/src/main/java/com/bladecoder/ink/runtime/NativeFunctionCall.java +++ b/src/main/java/com/bladecoder/ink/runtime/NativeFunctionCall.java @@ -1,906 +1,896 @@ -package com.bladecoder.ink.runtime; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map.Entry; - -public class NativeFunctionCall extends RTObject { - static interface BinaryOp { - Object invoke(Object left, Object right); - } - - static interface UnaryOp { - Object invoke(Object val); - } - - public static final String Add = "+"; - public static final String And = "&&"; - public static final String Divide = "/"; - public static final String Equal = "=="; - public static final String Greater = ">"; - public static final String GreaterThanOrEquals = ">="; - public static final String Less = "<"; - public static final String LessThanOrEquals = "<="; - public static final String Max = "MAX"; - public static final String Min = "MIN"; - - public static final String Pow = "POW"; - public static final String Floor = "FLOOR"; - public static final String Ceiling = "CEILING"; - public static final String Int = "INT"; - public static final String Float = "FLOAT"; - - public static final String Mod = "%"; - public static final String Multiply = "*"; - private static HashMap nativeFunctions; - public static final String Negate = "_"; // distinguish from "-" for - // subtraction - public static final String Not = "!"; - - public static final String Has = "?"; - public static final String Hasnt = "!?"; - public static final String Intersect = "^"; - - public static final String ListMax = "LIST_MAX"; - public static final String ListMin = "LIST_MIN"; - public static final String All = "LIST_ALL"; - public static final String Count = "LIST_COUNT"; - public static final String ValueOfList = "LIST_VALUE"; - public static final String Invert = "LIST_INVERT"; - - public static final String NotEquals = "!="; - - public static final String Or = "||"; - - public static final String Subtract = "-"; - - static void addListBinaryOp(String name, BinaryOp op) { - addOpToNativeFunc(name, 2, ValueType.List, op); - } - - static void addListUnaryOp(String name, UnaryOp op) { - addOpToNativeFunc(name, 1, ValueType.List, op); - } - - static void addFloatBinaryOp(String name, BinaryOp op) { - addOpToNativeFunc(name, 2, ValueType.Float, op); - } - - static void addFloatUnaryOp(String name, UnaryOp op) { - addOpToNativeFunc(name, 1, ValueType.Float, op); - } - - static void addIntBinaryOp(String name, BinaryOp op) { - addOpToNativeFunc(name, 2, ValueType.Int, op); - } - - static void addIntUnaryOp(String name, UnaryOp op) { - addOpToNativeFunc(name, 1, ValueType.Int, op); - } - - static void addOpToNativeFunc(String name, int args, ValueType valType, Object op) { - NativeFunctionCall nativeFunc = nativeFunctions.get(name); - - // Operations for each data type, for a single operation (e.g. "+") - - if (nativeFunc == null) { - nativeFunc = new NativeFunctionCall(name, args); - nativeFunctions.put(name, nativeFunc); - } - - nativeFunc.addOpFuncForType(valType, op); - } - - static void addStringBinaryOp(String name, BinaryOp op) { - addOpToNativeFunc(name, 2, ValueType.String, op); - } - - public static boolean callExistsWithName(String functionName) { - generateNativeFunctionsIfNecessary(); - return nativeFunctions.containsKey(functionName); - } - - public static NativeFunctionCall callWithName(String functionName) { - return new NativeFunctionCall(functionName); - } - - static void generateNativeFunctionsIfNecessary() { - if (nativeFunctions == null) { - nativeFunctions = new HashMap(); - - // Int operations - addIntBinaryOp(Add, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (Integer) left + (Integer) right; - } - }); - - addIntBinaryOp(Subtract, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (Integer) left - (Integer) right; - } - }); - - addIntBinaryOp(Multiply, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (Integer) left * (Integer) right; - } - }); - - addIntBinaryOp(Divide, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (Integer) left / (Integer) right; - } - }); - - addIntBinaryOp(Mod, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (Integer) left % (Integer) right; - } - }); - - addIntUnaryOp(Negate, new UnaryOp() { - - @Override - public Object invoke(Object val) { - return -(Integer) val; - } - }); - - addIntBinaryOp(Equal, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (Integer) left == (Integer) right ? 1 : 0; - } - }); - - addIntBinaryOp(Greater, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (Integer) left > (Integer) right ? 1 : 0; - } - }); - - addIntBinaryOp(Less, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (Integer) left < (Integer) right ? 1 : 0; - } - }); - addIntBinaryOp(GreaterThanOrEquals, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (Integer) left >= (Integer) right ? 1 : 0; - } - }); - addIntBinaryOp(LessThanOrEquals, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (Integer) left <= (Integer) right ? 1 : 0; - } - }); - addIntBinaryOp(NotEquals, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (Integer) left != (Integer) right ? 1 : 0; - } - }); - - addIntUnaryOp(Not, new UnaryOp() { - - @Override - public Object invoke(Object val) { - return (Integer) val == 0 ? 1 : 0; - } - }); - - addIntBinaryOp(And, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (Integer) left != 0 && (Integer) right != 0 ? 1 : 0; - } - }); - addIntBinaryOp(Or, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (Integer) left != 0 || (Integer) right != 0 ? 1 : 0; - } - }); - addIntBinaryOp(Max, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return Math.max((Integer) left, (Integer) right); - } - }); - addIntBinaryOp(Min, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return Math.min((Integer) left, (Integer) right); - } - }); - addIntBinaryOp(Pow, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (float)Math.pow((Integer) left, (Integer) right); - } - }); - - addIntUnaryOp(Floor, new UnaryOp() { - - @Override - public Object invoke(Object val) { - return val; - } - }); - addIntUnaryOp(Ceiling, new UnaryOp() { - - @Override - public Object invoke(Object val) { - return val; - } - }); - addIntUnaryOp(Int, new UnaryOp() { - - @Override - public Object invoke(Object val) { - return val; - } - }); - addIntUnaryOp(Float, new UnaryOp() { - - @Override - public Object invoke(Object val) { - return (Float)val; - } - }); - - // Float operations - addFloatBinaryOp(Add, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (Float) left + (Float) right; - } - }); - - addFloatBinaryOp(Subtract, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (Float) left - (Float) right; - } - }); - - addFloatBinaryOp(Multiply, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (Float) left * (Float) right; - } - }); - - addFloatBinaryOp(Divide, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (Float) left / (Float) right; - } - }); - - addFloatBinaryOp(Mod, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (Float) left % (Float) right; - } - }); - - addFloatUnaryOp(Negate, new UnaryOp() { - - @Override - public Object invoke(Object val) { - return -(Float) val; - } - }); - - addFloatBinaryOp(Equal, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (Float) left == (Float) right ? (Integer) 1 : (Integer) 0; - } - }); - - addFloatBinaryOp(Greater, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (Float) left > (Float) right ? (Integer) 1 : (Integer) 0; - } - }); - - addFloatBinaryOp(Less, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (Float) left < (Float) right ? (Integer) 1 : (Integer) 0; - } - }); - addFloatBinaryOp(GreaterThanOrEquals, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (Float) left >= (Float) right ? (Integer) 1 : (Integer) 0; - } - }); - addFloatBinaryOp(LessThanOrEquals, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (Float) left <= (Float) right ? (Integer) 1 : (Integer) 0; - } - }); - addFloatBinaryOp(NotEquals, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (Float) left != (Float) right ? (Integer) 1 : (Integer) 0; - } - }); - - addFloatUnaryOp(Not, new UnaryOp() { - - @Override - public Object invoke(Object val) { - return (Float) val == 0 ? (Integer) 1 : (Integer) 0; - } - }); - - addFloatBinaryOp(And, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (Float) left != 0 && (Float) right != 0 ? (Integer) 1 : (Integer) 0; - } - }); - addFloatBinaryOp(Or, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (Float) left != 0 || (Float) right != 0 ? (Integer) 1 : (Integer) 0; - } - }); - addFloatBinaryOp(Max, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return Math.max((Float) left, (Float) right); - } - }); - addFloatBinaryOp(Min, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return Math.min((Float) left, (Float) right); - } - }); - - addFloatBinaryOp(Pow, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (float)Math.pow((Float) left, (Float) right); - } - }); - - addFloatUnaryOp(Floor, new UnaryOp() { - - @Override - public Object invoke(Object val) { - return (float)Math.floor((double)val); - } - }); - addFloatUnaryOp(Ceiling, new UnaryOp() { - - @Override - public Object invoke(Object val) { - return (float)Math.ceil((double)val); - } - }); - addFloatUnaryOp(Int, new UnaryOp() { - - @Override - public Object invoke(Object val) { - return (int) val; - } - }); - addFloatUnaryOp(Float, new UnaryOp() { - - @Override - public Object invoke(Object val) { - return val; - } - }); - - // String operations - addStringBinaryOp(Add, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (String) left + (String) right; - } - }); - // concat - addStringBinaryOp(Equal, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return ((String) left).equals(right) ? (Integer) 1 : (Integer) 0; - } - }); - - addStringBinaryOp(NotEquals, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (!((String) left).equals(right)) ? (Integer) 1 : (Integer) 0; - } - }); - - addStringBinaryOp(Has, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (((String) left).contains(right.toString())) ? (Integer) 1 : (Integer) 0; - } - }); - - addStringBinaryOp(Hasnt, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (((String) left).contains(right.toString())) ? (Integer) 0 : (Integer) 1; - } - }); - - // List operations - addListBinaryOp(Add, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return ((InkList) left).union((InkList) right); - } - }); - - addListBinaryOp(Subtract, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return ((InkList) left).without((InkList) right); - } - }); - - addListBinaryOp(Has, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return ((InkList) left).contains((InkList) right) ? (Integer) 1 : (Integer) 0; - } - }); - - addListBinaryOp(Hasnt, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return ((InkList) left).contains((InkList) right) ? (Integer) 0 : (Integer) 1; - } - }); - - addListBinaryOp(Intersect, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return ((InkList) left).intersect((InkList) right); - } - }); - - addListBinaryOp(Equal, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return ((InkList) left).equals(right) ? (Integer) 1 : (Integer) 0; - } - }); - - addListBinaryOp(Greater, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return ((InkList) left).size() > 0 && ((InkList) left).greaterThan((InkList) right) ? (Integer) 1 - : (Integer) 0; - } - }); - - addListBinaryOp(Less, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return ((InkList) left).lessThan((InkList) right) ? (Integer) 1 : (Integer) 0; - } - }); - - addListBinaryOp(GreaterThanOrEquals, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return ((InkList) left).size() > 0 && ((InkList) left).greaterThanOrEquals((InkList) right) - ? (Integer) 1 : (Integer) 0; - } - }); - - addListBinaryOp(LessThanOrEquals, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return ((InkList) left).size() > 0 && ((InkList) left).lessThanOrEquals((InkList) right) - ? (Integer) 1 : (Integer) 0; - } - }); - - addListBinaryOp(NotEquals, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (!((InkList) left).equals(right) ? (Integer) 1 : (Integer) 0); - } - }); - - addListBinaryOp(And, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (((InkList) left).size() > 0 && ((InkList) right).size() > 0? (Integer) 1 : (Integer) 0); - } - }); - - addListBinaryOp(Or, new BinaryOp() { - @Override - public Object invoke(Object left, Object right) { - return (((InkList) left).size() > 0 || ((InkList) right).size() > 0? (Integer) 1 : (Integer) 0); - } - }); - - addListUnaryOp(Not, new UnaryOp() { - @Override - public Object invoke(Object val) { - return ((InkList) val).size() == 0 ? (int) 1 : (int) 0; - } - }); - - // Placeholder to ensure that Invert gets created at all, - // since this function is never actually run, and is special cased - // in Call - addListUnaryOp(Invert, new UnaryOp() { - @Override - public Object invoke(Object val) { - return ((InkList) val).getInverse(); - } - }); - - addListUnaryOp(All, new UnaryOp() { - @Override - public Object invoke(Object val) { - return ((InkList) val).getAll(); - } - }); - - addListUnaryOp(ListMin, new UnaryOp() { - @Override - public Object invoke(Object val) { - return ((InkList) val).minAsList(); - } - }); - - addListUnaryOp(ListMax, new UnaryOp() { - @Override - public Object invoke(Object val) { - return ((InkList) val).maxAsList(); - } - }); - - addListUnaryOp(Count, new UnaryOp() { - @Override - public Object invoke(Object val) { - return ((InkList) val).size(); - } - }); - - addListUnaryOp(ValueOfList, new UnaryOp() { - @Override - public Object invoke(Object val) { - return ((InkList) val).getMaxItem().getValue(); - } - }); - - BinaryOp divertTargetsEqual = new BinaryOp() { - - @Override - public Object invoke(Object left, Object right) { - return ((Path) left).equals((Path) left) ? (Integer) 1 : (Integer) 0; - } - - }; - - BinaryOp divertTargetsNotEqual = new BinaryOp() { - - @Override - public Object invoke(Object left, Object right) { - return ((Path) left).equals((Path) left) ? (Integer) 0 : (Integer) 1; - } - - }; - - addOpToNativeFunc(Equal, 2, ValueType.DivertTarget, divertTargetsEqual); - addOpToNativeFunc(NotEquals, 2, ValueType.DivertTarget, divertTargetsNotEqual); - } - - } - - private String name; - - private int numberOfParameters; - - private boolean isPrototype; - - private HashMap operationFuncs; - - private NativeFunctionCall prototype; - - // Require default constructor for serialisation - public NativeFunctionCall() { - generateNativeFunctionsIfNecessary(); - } - - public NativeFunctionCall(String name) { - generateNativeFunctionsIfNecessary(); - this.setName(name); - } - - // Only called internally to generate prototypes - NativeFunctionCall(String name, int numberOfParameters) { - isPrototype = true; - this.setName(name); - this.setNumberOfParameters(numberOfParameters); - } - - void addOpFuncForType(ValueType valType, Object op) { - if (operationFuncs == null) { - operationFuncs = new HashMap(); - } - - operationFuncs.put(valType, op); - } - - public RTObject call(List parameters) throws Exception { - - if (prototype != null) { - return prototype.call(parameters); - } - - if (getNumberOfParameters() != parameters.size()) { - throw new Exception("Unexpected number of parameters"); - } - - boolean hasList = false; - - for (RTObject p : parameters) { - if (p instanceof Void) - throw new StoryException( - "Attempting to perform operation on a void value. Did you forget to 'return' a value from a function you called here?"); - - if (p instanceof ListValue) - hasList = true; - - } - - // Binary operations on lists are treated outside of the standard - // coerscion rules - if (parameters.size() == 2 && hasList) - return callBinaryListOperation(parameters); - - List> coercedParams = coerceValuesToSingleType(parameters); - ValueType coercedType = coercedParams.get(0).getValueType(); - - // Originally CallType gets a type parameter taht is used to do some - // casting, but we can do without. - if (coercedType == ValueType.Int) { - return callType(coercedParams); - } else if (coercedType == ValueType.Float) { - return callType(coercedParams); - } else if (coercedType == ValueType.String) { - return callType(coercedParams); - } else if (coercedType == ValueType.DivertTarget) { - return callType(coercedParams); - } else if (coercedType == ValueType.List) { - return callType(coercedParams); - } - - return null; - - } - - Value callBinaryListOperation(List parameters) throws StoryException, Exception { - // List-Int addition/subtraction returns a List (e.g. "alpha" + 1 = - // "beta") - if (("+".equals(name) || "-".equals(name)) && parameters.get(0) instanceof ListValue - && parameters.get(1) instanceof IntValue) - return callListIncrementOperation(parameters); - - Value v1 = (Value) parameters.get(0); - Value v2 = (Value) parameters.get(1); - - // And/or with any other type requires coerscion to bool (int) - if ((name == "&&" || name == "||") - && (v1.getValueType() != ValueType.List || v2.getValueType() != ValueType.List)) { - BinaryOp op = (BinaryOp) operationFuncs.get(ValueType.Int); - int result = (int) op.invoke(v1.isTruthy() ? 1 : 0, v2.isTruthy() ? 1 : 0); - return new IntValue(result); - } - - // Normal (list • list) operation - if (v1.getValueType() == ValueType.List && v2.getValueType() == ValueType.List) { - List> p = new ArrayList>(); - p.add(v1); - p.add(v2); - - return (Value) callType(p); - } - - throw new StoryException( - "Can not call use '" + name + "' operation on " + v1.getValueType() + " and " + v2.getValueType()); - } - - Value callListIncrementOperation(List listIntParams) throws StoryException, Exception { - ListValue listVal = (ListValue) listIntParams.get(0); - IntValue intVal = (IntValue) listIntParams.get(1); - - InkList resultRawList = new InkList(); - - for (Entry listItemWithValue : listVal.getValue().entrySet()) { - - InkListItem listItem = listItemWithValue.getKey(); - Integer listItemValue = listItemWithValue.getValue(); - - // Find + or - operation - BinaryOp intOp = (BinaryOp) operationFuncs.get(ValueType.Int); - - // Return value unknown until it's evaluated - int targetInt = (int) intOp.invoke(listItemValue, intVal.value); - - // Find this item's origin (linear search should be ok, should be - // short haha) - ListDefinition itemOrigin = null; - for (ListDefinition origin : listVal.getValue().getOrigins()) { - if (origin.getName().equals(listItem.getOriginName())) { - itemOrigin = origin; - break; - } - } - - if (itemOrigin != null) { - InkListItem incrementedItem = itemOrigin.getItemWithValue(targetInt); - if (incrementedItem != null) - resultRawList.put(incrementedItem, targetInt); - } - } - - return new ListValue(resultRawList); - } - - private RTObject callType(List> parametersOfSingleType) throws StoryException, Exception { - - Value param1 = parametersOfSingleType.get(0); - ValueType valType = param1.getValueType(); - Value val1 = param1; - - int paramCount = parametersOfSingleType.size(); - - if (paramCount == 2 || paramCount == 1) { - Object opForTypeObj = operationFuncs.get(valType); - - if (opForTypeObj == null) { - throw new StoryException("Cannot perform operation '" + this.getName() + "' on " + valType); - } - - // Binary - if (paramCount == 2) { - Value param2 = parametersOfSingleType.get(1); - Value val2 = param2; - - BinaryOp opForType = (BinaryOp) opForTypeObj; - - // Return value unknown until it's evaluated - Object resultVal = opForType.invoke(val1.getValue(), val2.getValue()); - - return AbstractValue.create(resultVal); - } else { // Unary - UnaryOp opForType = (UnaryOp) opForTypeObj; - - Object resultVal = opForType.invoke(val1.getValue()); - - return AbstractValue.create(resultVal); - } - } else { - throw new Exception( - "Unexpected number of parameters to NativeFunctionCall: " + parametersOfSingleType.size()); - } - } - - List> coerceValuesToSingleType(List parametersIn) throws Exception { - ValueType valType = ValueType.Int; - - ListValue specialCaseList = null; - - for (RTObject obj : parametersIn) { - // Find out what the output type is - // "higher level" types infect both so that binary operations - // use the same type on both sides. e.g. binary operation of - // int and float causes the int to be casted to a float. - Value val = (Value) obj; - if (val.getValueType().ordinal() > valType.ordinal()) { - valType = val.getValueType(); - } - - if (val.getValueType() == ValueType.List) { - specialCaseList = (ListValue) val; - } - } - - // // Coerce to this chosen type - ArrayList> parametersOut = new ArrayList>(); - // for (RTObject p : parametersIn) { - // Value val = (Value) p; - // Value castedValue = (Value) val.cast(valType); - // parametersOut.add(castedValue); - - // Special case: Coercing to Ints to Lists - // We have to do it early when we have both parameters - // to hand - so that we can make use of the List's origin - if (valType == ValueType.List) { - - for (RTObject p : parametersIn) { - Value val = (Value) p; - if (val.getValueType() == ValueType.List) { - parametersOut.add(val); - } else if (val.getValueType() == ValueType.Int) { - int intVal = (int) val.getValueObject(); - ListDefinition list = specialCaseList.getValue().getOriginOfMaxItem(); - InkListItem item = list.getItemWithValue(intVal); - - if (item != null) { - ListValue castedValue = new ListValue(item, intVal); - parametersOut.add(castedValue); - } else - throw new StoryException( - "Could not find List item with the value " + intVal + " in " + list.getName()); - } else - throw new StoryException( - "Cannot mix Lists and " + val.getValueType() + " values in this operation"); - } - - } - - // Normal Coercing (with standard casting) - else { - for (RTObject p : parametersIn) { - Value val = (Value) p; - Value castedValue = (Value) val.cast(valType); - parametersOut.add(castedValue); - } - } - - return parametersOut; - } - - public String getName() { - return name; - } - - public int getNumberOfParameters() { - if (prototype != null) { - return prototype.getNumberOfParameters(); - } else { - return numberOfParameters; - } - } - - public void setName(String value) { - name = value; - if (!isPrototype) - prototype = nativeFunctions.get(name); - - } - - public void setNumberOfParameters(int value) { - numberOfParameters = value; - } - - @Override - public String toString() { - return "Native '" + getName() + "'"; - - } -} +package com.bladecoder.ink.runtime; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map.Entry; + +public class NativeFunctionCall extends RTObject { + static interface BinaryOp { + Object invoke(Object left, Object right); + } + + static interface UnaryOp { + Object invoke(Object val); + } + + public static final String Add = "+"; + public static final String And = "&&"; + public static final String Divide = "/"; + public static final String Equal = "=="; + public static final String Greater = ">"; + public static final String GreaterThanOrEquals = ">="; + public static final String Less = "<"; + public static final String LessThanOrEquals = "<="; + public static final String Max = "MAX"; + public static final String Min = "MIN"; + + public static final String Pow = "POW"; + public static final String Floor = "FLOOR"; + public static final String Ceiling = "CEILING"; + public static final String Int = "INT"; + public static final String Float = "FLOAT"; + + public static final String Mod = "%"; + public static final String Multiply = "*"; + private static HashMap nativeFunctions; + public static final String Negate = "_"; // distinguish from "-" for + // subtraction + public static final String Not = "!"; + + public static final String Has = "?"; + public static final String Hasnt = "!?"; + public static final String Intersect = "^"; + + public static final String ListMax = "LIST_MAX"; + public static final String ListMin = "LIST_MIN"; + public static final String All = "LIST_ALL"; + public static final String Count = "LIST_COUNT"; + public static final String ValueOfList = "LIST_VALUE"; + public static final String Invert = "LIST_INVERT"; + + public static final String NotEquals = "!="; + + public static final String Or = "||"; + + public static final String Subtract = "-"; + + static void addListBinaryOp(String name, BinaryOp op) { + addOpToNativeFunc(name, 2, ValueType.List, op); + } + + static void addListUnaryOp(String name, UnaryOp op) { + addOpToNativeFunc(name, 1, ValueType.List, op); + } + + static void addFloatBinaryOp(String name, BinaryOp op) { + addOpToNativeFunc(name, 2, ValueType.Float, op); + } + + static void addFloatUnaryOp(String name, UnaryOp op) { + addOpToNativeFunc(name, 1, ValueType.Float, op); + } + + static void addIntBinaryOp(String name, BinaryOp op) { + addOpToNativeFunc(name, 2, ValueType.Int, op); + } + + static void addIntUnaryOp(String name, UnaryOp op) { + addOpToNativeFunc(name, 1, ValueType.Int, op); + } + + static void addOpToNativeFunc(String name, int args, ValueType valType, Object op) { + NativeFunctionCall nativeFunc = nativeFunctions.get(name); + + // Operations for each data type, for a single operation (e.g. "+") + + if (nativeFunc == null) { + nativeFunc = new NativeFunctionCall(name, args); + nativeFunctions.put(name, nativeFunc); + } + + nativeFunc.addOpFuncForType(valType, op); + } + + static void addStringBinaryOp(String name, BinaryOp op) { + addOpToNativeFunc(name, 2, ValueType.String, op); + } + + public static boolean callExistsWithName(String functionName) { + generateNativeFunctionsIfNecessary(); + return nativeFunctions.containsKey(functionName); + } + + public static NativeFunctionCall callWithName(String functionName) { + return new NativeFunctionCall(functionName); + } + + static void generateNativeFunctionsIfNecessary() { + if (nativeFunctions == null) { + nativeFunctions = new HashMap<>(); + + // Why no bool operations? + // Before evaluation, all bools are coerced to ints in + // CoerceValuesToSingleType (see default value for valType at top). + // So, no operations are ever directly done in bools themselves. + // This also means that 1 == true works, since true is always converted + // to 1 first. + // However, many operations return a "native" bool (equals, etc). + + // Int operations + addIntBinaryOp(Add, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (Integer) left + (Integer) right; + } + }); + + addIntBinaryOp(Subtract, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (Integer) left - (Integer) right; + } + }); + + addIntBinaryOp(Multiply, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (Integer) left * (Integer) right; + } + }); + + addIntBinaryOp(Divide, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (Integer) left / (Integer) right; + } + }); + + addIntBinaryOp(Mod, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (Integer) left % (Integer) right; + } + }); + + addIntUnaryOp(Negate, new UnaryOp() { + + @Override + public Object invoke(Object val) { + return -(Integer) val; + } + }); + + addIntBinaryOp(Equal, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (Integer) left == (Integer) right; + } + }); + + addIntBinaryOp(Greater, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (Integer) left > (Integer) right; + } + }); + + addIntBinaryOp(Less, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (Integer) left < (Integer) right; + } + }); + addIntBinaryOp(GreaterThanOrEquals, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (Integer) left >= (Integer) right; + } + }); + addIntBinaryOp(LessThanOrEquals, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (Integer) left <= (Integer) right; + } + }); + addIntBinaryOp(NotEquals, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (Integer) left != (Integer) right; + } + }); + + addIntUnaryOp(Not, new UnaryOp() { + + @Override + public Object invoke(Object val) { + return (Integer) val == 0; + } + }); + + addIntBinaryOp(And, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (Integer) left != 0 && (Integer) right != 0; + } + }); + addIntBinaryOp(Or, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (Integer) left != 0 || (Integer) right != 0; + } + }); + addIntBinaryOp(Max, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return Math.max((Integer) left, (Integer) right); + } + }); + addIntBinaryOp(Min, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return Math.min((Integer) left, (Integer) right); + } + }); + addIntBinaryOp(Pow, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (float) Math.pow((Integer) left, (Integer) right); + } + }); + + addIntUnaryOp(Floor, new UnaryOp() { + + @Override + public Object invoke(Object val) { + return val; + } + }); + addIntUnaryOp(Ceiling, new UnaryOp() { + + @Override + public Object invoke(Object val) { + return val; + } + }); + addIntUnaryOp(Int, new UnaryOp() { + + @Override + public Object invoke(Object val) { + return val; + } + }); + addIntUnaryOp(Float, new UnaryOp() { + + @Override + public Object invoke(Object val) { + return val; + } + }); + + // Float operations + addFloatBinaryOp(Add, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (Float) left + (Float) right; + } + }); + + addFloatBinaryOp(Subtract, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (Float) left - (Float) right; + } + }); + + addFloatBinaryOp(Multiply, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (Float) left * (Float) right; + } + }); + + addFloatBinaryOp(Divide, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (Float) left / (Float) right; + } + }); + + addFloatBinaryOp(Mod, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (Float) left % (Float) right; + } + }); + + addFloatUnaryOp(Negate, new UnaryOp() { + + @Override + public Object invoke(Object val) { + return -(Float) val; + } + }); + + addFloatBinaryOp(Equal, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (Float) left == (Float) right; + } + }); + + addFloatBinaryOp(Greater, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (Float) left > (Float) right; + } + }); + + addFloatBinaryOp(Less, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (Float) left < (Float) right; + } + }); + addFloatBinaryOp(GreaterThanOrEquals, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (Float) left >= (Float) right; + } + }); + addFloatBinaryOp(LessThanOrEquals, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (Float) left <= (Float) right; + } + }); + addFloatBinaryOp(NotEquals, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (Float) left != (Float) right; + } + }); + + addFloatUnaryOp(Not, new UnaryOp() { + + @Override + public Object invoke(Object val) { + return (Float) val == 0; + } + }); + + addFloatBinaryOp(And, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (Float) left != 0 && (Float) right != 0; + } + }); + addFloatBinaryOp(Or, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (Float) left != 0 || (Float) right != 0; + } + }); + addFloatBinaryOp(Max, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return Math.max((Float) left, (Float) right); + } + }); + addFloatBinaryOp(Min, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return Math.min((Float) left, (Float) right); + } + }); + + addFloatBinaryOp(Pow, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (float) Math.pow((Float) left, (Float) right); + } + }); + + addFloatUnaryOp(Floor, new UnaryOp() { + + @Override + public Object invoke(Object val) { + return (float) Math.floor((double) val); + } + }); + addFloatUnaryOp(Ceiling, new UnaryOp() { + + @Override + public Object invoke(Object val) { + return (float) Math.ceil((double) val); + } + }); + addFloatUnaryOp(Int, new UnaryOp() { + + @Override + public Object invoke(Object val) { + return (int) val; + } + }); + addFloatUnaryOp(Float, new UnaryOp() { + + @Override + public Object invoke(Object val) { + return val; + } + }); + + // String operations + addStringBinaryOp(Add, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (String) left + (String) right; + } + }); + // concat + addStringBinaryOp(Equal, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return left.equals(right); + } + }); + + addStringBinaryOp(NotEquals, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return !left.equals(right); + } + }); + + addStringBinaryOp(Has, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return ((String) left).contains(right.toString()); + } + }); + + addStringBinaryOp(Hasnt, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return !((String) left).contains(right.toString()); + } + }); + + // List operations + addListBinaryOp(Add, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return ((InkList) left).union((InkList) right); + } + }); + + addListBinaryOp(Subtract, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return ((InkList) left).without((InkList) right); + } + }); + + addListBinaryOp(Has, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return ((InkList) left).contains((InkList) right); + } + }); + + addListBinaryOp(Hasnt, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return !((InkList) left).contains((InkList) right); + } + }); + + addListBinaryOp(Intersect, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return ((InkList) left).intersect((InkList) right); + } + }); + + addListBinaryOp(Equal, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return ((InkList) left).equals(right); + } + }); + + addListBinaryOp(Greater, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return ((InkList) left).size() > 0 && ((InkList) left).greaterThan((InkList) right); + } + }); + + addListBinaryOp(Less, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return ((InkList) left).lessThan((InkList) right); + } + }); + + addListBinaryOp(GreaterThanOrEquals, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return ((InkList) left).size() > 0 && ((InkList) left).greaterThanOrEquals((InkList) right); + } + }); + + addListBinaryOp(LessThanOrEquals, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return ((InkList) left).size() > 0 && ((InkList) left).lessThanOrEquals((InkList) right); + } + }); + + addListBinaryOp(NotEquals, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (!((InkList) left).equals(right)); + } + }); + + addListBinaryOp(And, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (((InkList) left).size() > 0 && ((InkList) right).size() > 0); + } + }); + + addListBinaryOp(Or, new BinaryOp() { + @Override + public Object invoke(Object left, Object right) { + return (((InkList) left).size() > 0 || ((InkList) right).size() > 0); + } + }); + + addListUnaryOp(Not, new UnaryOp() { + @Override + public Object invoke(Object val) { + return ((InkList) val).size() == 0; + } + }); + + // Placeholder to ensure that Invert gets created at all, + // since this function is never actually run, and is special cased + // in Call + addListUnaryOp(Invert, new UnaryOp() { + @Override + public Object invoke(Object val) { + return ((InkList) val).getInverse(); + } + }); + + addListUnaryOp(All, new UnaryOp() { + @Override + public Object invoke(Object val) { + return ((InkList) val).getAll(); + } + }); + + addListUnaryOp(ListMin, new UnaryOp() { + @Override + public Object invoke(Object val) { + return ((InkList) val).minAsList(); + } + }); + + addListUnaryOp(ListMax, new UnaryOp() { + @Override + public Object invoke(Object val) { + return ((InkList) val).maxAsList(); + } + }); + + addListUnaryOp(Count, new UnaryOp() { + @Override + public Object invoke(Object val) { + return ((InkList) val).size(); + } + }); + + addListUnaryOp(ValueOfList, new UnaryOp() { + @Override + public Object invoke(Object val) { + return ((InkList) val).getMaxItem().getValue(); + } + }); + + BinaryOp divertTargetsEqual = new BinaryOp() { + + @Override + public Object invoke(Object left, Object right) { + return ((Path) left).equals((Path) right); + } + }; + + BinaryOp divertTargetsNotEqual = new BinaryOp() { + + @Override + public Object invoke(Object left, Object right) { + return !((Path) left).equals((Path) right); + } + }; + + addOpToNativeFunc(Equal, 2, ValueType.DivertTarget, divertTargetsEqual); + addOpToNativeFunc(NotEquals, 2, ValueType.DivertTarget, divertTargetsNotEqual); + } + } + + private String name; + + private int numberOfParameters; + + private boolean isPrototype; + + private HashMap operationFuncs; + + private NativeFunctionCall prototype; + + // Require default constructor for serialisation + public NativeFunctionCall() { + generateNativeFunctionsIfNecessary(); + } + + public NativeFunctionCall(String name) { + generateNativeFunctionsIfNecessary(); + this.setName(name); + } + + // Only called internally to generate prototypes + NativeFunctionCall(String name, int numberOfParameters) { + isPrototype = true; + this.setName(name); + this.setNumberOfParameters(numberOfParameters); + } + + void addOpFuncForType(ValueType valType, Object op) { + if (operationFuncs == null) { + operationFuncs = new HashMap<>(); + } + + operationFuncs.put(valType, op); + } + + public RTObject call(List parameters) throws Exception { + + if (prototype != null) { + return prototype.call(parameters); + } + + if (getNumberOfParameters() != parameters.size()) { + throw new Exception("Unexpected number of parameters"); + } + + boolean hasList = false; + + for (RTObject p : parameters) { + if (p instanceof Void) + throw new StoryException("Attempting to perform " + this.name + + " on a void value. Did you forget to 'return' a value from a function you called here?"); + + if (p instanceof ListValue) hasList = true; + } + + // Binary operations on lists are treated outside of the standard + // coerscion rules + if (parameters.size() == 2 && hasList) return callBinaryListOperation(parameters); + + List> coercedParams = coerceValuesToSingleType(parameters); + ValueType coercedType = coercedParams.get(0).getValueType(); + + // Originally CallType gets a type parameter that is used to do some + // casting, but we can do without. + if (coercedType == ValueType.Int) { + return callType(coercedParams); + } else if (coercedType == ValueType.Float) { + return callType(coercedParams); + } else if (coercedType == ValueType.String) { + return callType(coercedParams); + } else if (coercedType == ValueType.DivertTarget) { + return callType(coercedParams); + } else if (coercedType == ValueType.List) { + return callType(coercedParams); + } + + return null; + } + + Value callBinaryListOperation(List parameters) throws StoryException, Exception { + // List-Int addition/subtraction returns a List (e.g. "alpha" + 1 = + // "beta") + if (("+".equals(name) || "-".equals(name)) + && parameters.get(0) instanceof ListValue + && parameters.get(1) instanceof IntValue) return callListIncrementOperation(parameters); + + Value v1 = (Value) parameters.get(0); + Value v2 = (Value) parameters.get(1); + + // And/or with any other type requires coerscion to bool (int) + if ((name == "&&" || name == "||") + && (v1.getValueType() != ValueType.List || v2.getValueType() != ValueType.List)) { + BinaryOp op = (BinaryOp) operationFuncs.get(ValueType.Int); + boolean result = (boolean) op.invoke(v1.isTruthy() ? 1 : 0, v2.isTruthy() ? 1 : 0); + return new BoolValue(result); + } + + // Normal (list • list) operation + if (v1.getValueType() == ValueType.List && v2.getValueType() == ValueType.List) { + List> p = new ArrayList<>(); + p.add(v1); + p.add(v2); + + return (Value) callType(p); + } + + throw new StoryException( + "Can not call use '" + name + "' operation on " + v1.getValueType() + " and " + v2.getValueType()); + } + + Value callListIncrementOperation(List listIntParams) { + ListValue listVal = (ListValue) listIntParams.get(0); + IntValue intVal = (IntValue) listIntParams.get(1); + + InkList resultRawList = new InkList(); + + for (Entry listItemWithValue : listVal.getValue().entrySet()) { + + InkListItem listItem = listItemWithValue.getKey(); + Integer listItemValue = listItemWithValue.getValue(); + + // Find + or - operation + BinaryOp intOp = (BinaryOp) operationFuncs.get(ValueType.Int); + + // Return value unknown until it's evaluated + int targetInt = (int) intOp.invoke(listItemValue, intVal.value); + + // Find this item's origin (linear search should be ok, should be + // short haha) + ListDefinition itemOrigin = null; + for (ListDefinition origin : listVal.getValue().getOrigins()) { + if (origin.getName().equals(listItem.getOriginName())) { + itemOrigin = origin; + break; + } + } + + if (itemOrigin != null) { + InkListItem incrementedItem = itemOrigin.getItemWithValue(targetInt); + if (incrementedItem != null) resultRawList.put(incrementedItem, targetInt); + } + } + + return new ListValue(resultRawList); + } + + private RTObject callType(List> parametersOfSingleType) throws StoryException, Exception { + + Value param1 = parametersOfSingleType.get(0); + ValueType valType = param1.getValueType(); + Value val1 = param1; + + int paramCount = parametersOfSingleType.size(); + + if (paramCount == 2 || paramCount == 1) { + Object opForTypeObj = operationFuncs.get(valType); + + if (opForTypeObj == null) { + throw new StoryException("Cannot perform operation '" + this.getName() + "' on " + valType); + } + + // Binary + if (paramCount == 2) { + Value param2 = parametersOfSingleType.get(1); + Value val2 = param2; + + BinaryOp opForType = (BinaryOp) opForTypeObj; + + // Return value unknown until it's evaluated + Object resultVal = opForType.invoke(val1.getValue(), val2.getValue()); + + return AbstractValue.create(resultVal); + } else { // Unary + UnaryOp opForType = (UnaryOp) opForTypeObj; + + Object resultVal = opForType.invoke(val1.getValue()); + + return AbstractValue.create(resultVal); + } + } else { + throw new Exception( + "Unexpected number of parameters to NativeFunctionCall: " + parametersOfSingleType.size()); + } + } + + List> coerceValuesToSingleType(List parametersIn) throws Exception { + ValueType valType = ValueType.Int; + + ListValue specialCaseList = null; + + for (RTObject obj : parametersIn) { + // Find out what the output type is + // "higher level" types infect both so that binary operations + // use the same type on both sides. e.g. binary operation of + // int and float causes the int to be casted to a float. + Value val = (Value) obj; + if (val.getValueType().ordinal() > valType.ordinal()) { + valType = val.getValueType(); + } + + if (val.getValueType() == ValueType.List) { + specialCaseList = (ListValue) val; + } + } + + // // Coerce to this chosen type + ArrayList> parametersOut = new ArrayList<>(); + + // Special case: Coercing to Ints to Lists + // We have to do it early when we have both parameters + // to hand - so that we can make use of the List's origin + if (valType == ValueType.List) { + + for (RTObject p : parametersIn) { + Value val = (Value) p; + if (val.getValueType() == ValueType.List) { + parametersOut.add(val); + } else if (val.getValueType() == ValueType.Int) { + int intVal = (int) val.getValueObject(); + ListDefinition list = specialCaseList.getValue().getOriginOfMaxItem(); + InkListItem item = list.getItemWithValue(intVal); + + if (item != null) { + ListValue castedValue = new ListValue(item, intVal); + parametersOut.add(castedValue); + } else + throw new StoryException( + "Could not find List item with the value " + intVal + " in " + list.getName()); + } else + throw new StoryException( + "Cannot mix Lists and " + val.getValueType() + " values in this operation"); + } + + } + + // Normal Coercing (with standard casting) + else { + for (RTObject p : parametersIn) { + Value val = (Value) p; + Value castedValue = (Value) val.cast(valType); + parametersOut.add(castedValue); + } + } + + return parametersOut; + } + + public String getName() { + return name; + } + + public int getNumberOfParameters() { + if (prototype != null) { + return prototype.getNumberOfParameters(); + } else { + return numberOfParameters; + } + } + + public void setName(String value) { + name = value; + if (!isPrototype) prototype = nativeFunctions.get(name); + } + + public void setNumberOfParameters(int value) { + numberOfParameters = value; + } + + @Override + public String toString() { + return "Native '" + getName() + "'"; + } +} diff --git a/src/main/java/com/bladecoder/ink/runtime/Path.java b/src/main/java/com/bladecoder/ink/runtime/Path.java index 135cc8b..caa2ab0 100644 --- a/src/main/java/com/bladecoder/ink/runtime/Path.java +++ b/src/main/java/com/bladecoder/ink/runtime/Path.java @@ -1,303 +1,287 @@ -package com.bladecoder.ink.runtime; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -public class Path { - private final static String PARENT_ID = "^"; - - private List components; - private boolean isRelative = false; - private String componentsString; - - public Path() { - components = new ArrayList(); - } - - public Path(Component head, Path tail) { - this(); - - components.add(head); - components.addAll(tail.components); - } - - public Path(Collection components) { - this(components, false); - } - - public Path(Collection components, boolean relative) { - this(); - this.components.addAll(components); - - this.isRelative = relative; - } - - public Path(String componentsString) { - this(); - setComponentsString(componentsString); - } - - public Component getComponent(int index) { - return components.get(index); - } - - public boolean isRelative() { - return isRelative; - } - - private void setRelative(boolean value) { - isRelative = value; - } - - public Component getHead() { - if (components.size() > 0) { - return components.get(0); - } else { - return null; - } - } - - public Path getTail() { - if (components.size() >= 2) { - List tailComps = components.subList(1, components.size()); - - return new Path(tailComps); - } else { - return Path.getSelf(); - } - } - - public int getLength() { - return components.size(); - } - - public Component getLastComponent() { - int lastComponentIdx = components.size() - 1; - if (lastComponentIdx >= 0) - return components.get(lastComponentIdx); - else - return null; - } - - public boolean containsNamedComponent() { - for (Component comp : components) { - if (!comp.isIndex()) { - return true; - } - - } - return false; - } - - public static Path getSelf() { - Path path = new Path(); - path.setRelative(true); - return path; - } - - public Path pathByAppendingPath(Path pathToAppend) { - Path p = new Path(); - int upwardMoves = 0; - for (int i = 0; i < pathToAppend.components.size(); ++i) { - if (pathToAppend.components.get(i).isParent()) { - upwardMoves++; - } else { - break; - } - } - for (int i = 0; i < this.components.size() - upwardMoves; ++i) { - p.components.add(this.components.get(i)); - } - for (int i = upwardMoves; i < pathToAppend.components.size(); ++i) { - p.components.add(pathToAppend.components.get(i)); - } - return p; - } - - public String getComponentsString() { - if (componentsString == null) { - StringBuilder sb = new StringBuilder(); - - if (components.size() > 0) { - - sb.append(components.get(0)); - - for (int i = 1; i < components.size(); i++) { - sb.append('.'); - sb.append(components.get(i)); - } - } - - componentsString = sb.toString(); - - if (isRelative) - componentsString = "." + componentsString; - } - - return componentsString; - } - - private void setComponentsString(String value) { - components.clear(); - componentsString = value; - - // Empty path, empty components - // (path is to root, like "/" in file system) - if (componentsString == null || componentsString.isEmpty()) - return; - - // When components start with ".", it indicates a relative path, e.g. - // .^.^.hello.5 - // is equivalent to file system style path: - // ../../hello/5 - if (componentsString.charAt(0) == '.') { - setRelative(true); - componentsString = componentsString.substring(1); - } else { - setRelative(false); - } - - String[] componentStrings = componentsString.split("\\."); - - for (String str : componentStrings) { - int index = 0; - - try { - index = Integer.parseInt(str); - components.add(new Component(index)); - } catch (NumberFormatException e) { - components.add(new Component(str)); - } - } - } - - @Override - public String toString() { - return getComponentsString(); - } - - @Override - public boolean equals(Object obj) { - return equals(obj instanceof Path ? (Path) obj : (Path) null); - } - - public boolean equals(Path otherPath) { - if (otherPath == null) - return false; - - if (otherPath.components.size() != this.components.size()) - return false; - - if (otherPath.isRelative() != this.isRelative()) - return false; - - // return - // otherPath.components.SequenceEqual(this.components); - for (int i = 0; i < otherPath.components.size(); i++) { - if (!otherPath.components.get(i).equals(components.get(i))) - return false; - } - - return true; - - } - - @Override - public int hashCode() { - return toString().hashCode(); - } - - public Path pathByAppendingComponent(Component c) { - Path p = new Path(); - p.components.addAll(components); - p.components.add(c); - return p; - } - - // Immutable Component - public static class Component { - private int index; - private String name; - - public Component(int index) { - // Debug.Assert(index >= 0); - this.setIndex(index); - this.setName(null); - } - - public Component(String name) { - // Debug.Assert(name != null && name.Length > 0); - this.setName(name); - this.setIndex(-1); - } - - public int getIndex() { - return index; - } - - public void setIndex(int value) { - index = value; - } - - public String getName() { - return name; - } - - public void setName(String value) { - name = value; - } - - public boolean isIndex() { - return getIndex() >= 0; - } - - public boolean isParent() { - return Path.PARENT_ID.equals(getName()); - } - - public static Component toParent() { - return new Component(PARENT_ID); - } - - @Override - public String toString() { - if (isIndex()) { - return Integer.toString(getIndex()); - } else { - return getName(); - } - } - - @Override - public boolean equals(Object obj) { - - return equals(obj instanceof Component ? (Component) obj : (Component) null); - - } - - public boolean equals(Component otherComp) { - - if (otherComp != null && otherComp.isIndex() == this.isIndex()) { - if (isIndex()) { - return getIndex() == otherComp.getIndex(); - } else { - return getName().equals(otherComp.getName()); - } - } - - return false; - } - - @Override - public int hashCode() { - if (isIndex()) - return getIndex(); - else - return getName().hashCode(); - - } - - } - -} \ No newline at end of file +package com.bladecoder.ink.runtime; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class Path { + private static final String PARENT_ID = "^"; + + private final List components; + private boolean isRelative = false; + private String componentsString; + + public Path() { + components = new ArrayList<>(); + } + + public Path(Component head, Path tail) { + this(); + + components.add(head); + components.addAll(tail.components); + } + + public Path(Collection components) { + this(components, false); + } + + public Path(Collection components, boolean relative) { + this(); + this.components.addAll(components); + + this.isRelative = relative; + } + + public Path(String componentsString) { + this(); + setComponentsString(componentsString); + } + + public Component getComponent(int index) { + return components.get(index); + } + + public boolean isRelative() { + return isRelative; + } + + private void setRelative(boolean value) { + isRelative = value; + } + + public Component getHead() { + if (!components.isEmpty()) { + return components.get(0); + } else { + return null; + } + } + + public Path getTail() { + if (components.size() >= 2) { + List tailComps = components.subList(1, components.size()); + + return new Path(tailComps); + } else { + return Path.getSelf(); + } + } + + public int getLength() { + return components.size(); + } + + public Component getLastComponent() { + int lastComponentIdx = components.size() - 1; + if (lastComponentIdx >= 0) return components.get(lastComponentIdx); + else return null; + } + + public boolean containsNamedComponent() { + for (Component comp : components) { + if (!comp.isIndex()) { + return true; + } + } + return false; + } + + public static Path getSelf() { + Path path = new Path(); + path.setRelative(true); + return path; + } + + public Path pathByAppendingPath(Path pathToAppend) { + Path p = new Path(); + int upwardMoves = 0; + for (int i = 0; i < pathToAppend.components.size(); ++i) { + if (pathToAppend.components.get(i).isParent()) { + upwardMoves++; + } else { + break; + } + } + for (int i = 0; i < this.components.size() - upwardMoves; ++i) { + p.components.add(this.components.get(i)); + } + for (int i = upwardMoves; i < pathToAppend.components.size(); ++i) { + p.components.add(pathToAppend.components.get(i)); + } + return p; + } + + public String getComponentsString() { + if (componentsString == null) { + StringBuilder sb = new StringBuilder(); + + if (components.size() > 0) { + + sb.append(components.get(0)); + + for (int i = 1; i < components.size(); i++) { + sb.append('.'); + sb.append(components.get(i)); + } + } + + componentsString = sb.toString(); + + if (isRelative) componentsString = "." + componentsString; + } + + return componentsString; + } + + private void setComponentsString(String value) { + components.clear(); + componentsString = value; + + // Empty path, empty components + // (path is to root, like "/" in file system) + if (componentsString == null || componentsString.isEmpty()) return; + + // When components start with ".", it indicates a relative path, e.g. + // .^.^.hello.5 + // is equivalent to file system style path: + // ../../hello/5 + if (componentsString.charAt(0) == '.') { + setRelative(true); + componentsString = componentsString.substring(1); + } else { + setRelative(false); + } + + String[] componentStrings = componentsString.split("\\."); + + for (String str : componentStrings) { + int index = 0; + + try { + index = Integer.parseInt(str); + components.add(new Component(index)); + } catch (NumberFormatException e) { + components.add(new Component(str)); + } + } + } + + @Override + public String toString() { + return getComponentsString(); + } + + @Override + public boolean equals(Object obj) { + return equals(obj instanceof Path ? (Path) obj : (Path) null); + } + + public boolean equals(Path otherPath) { + if (otherPath == null) return false; + + if (otherPath.components.size() != this.components.size()) return false; + + if (otherPath.isRelative() != this.isRelative()) return false; + + // return + // otherPath.components.SequenceEqual(this.components); + for (int i = 0; i < otherPath.components.size(); i++) { + if (!otherPath.components.get(i).equals(components.get(i))) return false; + } + + return true; + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + public Path pathByAppendingComponent(Component c) { + Path p = new Path(); + p.components.addAll(components); + p.components.add(c); + return p; + } + + // Immutable Component + public static class Component { + private int index; + private String name; + + public Component(int index) { + // Debug.Assert(index >= 0); + this.setIndex(index); + this.setName(null); + } + + public Component(String name) { + // Debug.Assert(name != null && name.Length > 0); + this.setName(name); + this.setIndex(-1); + } + + public int getIndex() { + return index; + } + + public void setIndex(int value) { + index = value; + } + + public String getName() { + return name; + } + + public void setName(String value) { + name = value; + } + + public boolean isIndex() { + return getIndex() >= 0; + } + + public boolean isParent() { + return Path.PARENT_ID.equals(getName()); + } + + public static Component toParent() { + return new Component(PARENT_ID); + } + + @Override + public String toString() { + if (isIndex()) { + return Integer.toString(getIndex()); + } else { + return getName(); + } + } + + @Override + public boolean equals(Object obj) { + + return equals(obj instanceof Component ? (Component) obj : (Component) null); + } + + public boolean equals(Component otherComp) { + + if (otherComp != null && otherComp.isIndex() == this.isIndex()) { + if (isIndex()) { + return getIndex() == otherComp.getIndex(); + } else { + return getName().equals(otherComp.getName()); + } + } + + return false; + } + + @Override + public int hashCode() { + if (isIndex()) return getIndex(); + else return getName().hashCode(); + } + } +} diff --git a/src/main/java/com/bladecoder/ink/runtime/Pointer.java b/src/main/java/com/bladecoder/ink/runtime/Pointer.java index 0170696..cabf498 100644 --- a/src/main/java/com/bladecoder/ink/runtime/Pointer.java +++ b/src/main/java/com/bladecoder/ink/runtime/Pointer.java @@ -8,73 +8,63 @@ * content within that container. This scheme makes it as fast and efficient as * possible to increment the pointer (move the story forwards) in a way that's * as native to the internal engine as possible. - * + * * @author rgarcia * */ class Pointer { - public Container container; - public int index; - - public Pointer() { - - } - - public Pointer(Pointer p) { - assign(p); - } - - public Pointer(Container container, int index) { - this.container = container; - this.index = index; - } - - public void assign(Pointer p) { - container = p.container; - index = p.index; - } - - public RTObject resolve() { - if (index < 0) - return container; - - if (container == null) - return null; - - if (container.getContent().size() == 0) - return container; - - if (index >= container.getContent().size()) - return null; - - return container.getContent().get(index); - } - - public boolean isNull() { - return container == null; - } - - public Path getPath() { - if (isNull()) - return null; - - if (index >= 0) - return container.getPath().pathByAppendingComponent(new Path.Component(index)); - else - return container.getPath(); - } - - @Override - public String toString() { - if (container == null) - return "Ink Pointer (null)"; - - return "Ink Pointer -> " + container.getPath().toString() + " -- index " + index; - } - - public static Pointer startOf(Container container) { - return new Pointer(container, 0); - } - - public static final Pointer Null = new Pointer(null, -1); + public Container container; + public int index; + + public Pointer() {} + + public Pointer(Pointer p) { + assign(p); + } + + public Pointer(Container container, int index) { + this.container = container; + this.index = index; + } + + public void assign(Pointer p) { + container = p.container; + index = p.index; + } + + public RTObject resolve() { + if (index < 0) return container; + + if (container == null) return null; + + if (container.getContent().isEmpty()) return container; + + if (index >= container.getContent().size()) return null; + + return container.getContent().get(index); + } + + public boolean isNull() { + return container == null; + } + + public Path getPath() { + if (isNull()) return null; + + if (index >= 0) return container.getPath().pathByAppendingComponent(new Path.Component(index)); + else return container.getPath(); + } + + @Override + public String toString() { + if (container == null) return "Ink Pointer (null)"; + + return "Ink Pointer -> " + container.getPath().toString() + " -- index " + index; + } + + public static Pointer startOf(Container container) { + return new Pointer(container, 0); + } + + public static final Pointer Null = new Pointer(null, -1); } diff --git a/src/main/java/com/bladecoder/ink/runtime/ProfileNode.java b/src/main/java/com/bladecoder/ink/runtime/ProfileNode.java index 3435136..7bdf877 100644 --- a/src/main/java/com/bladecoder/ink/runtime/ProfileNode.java +++ b/src/main/java/com/bladecoder/ink/runtime/ProfileNode.java @@ -10,164 +10,156 @@ /** * Node used in the hierarchical tree of timings used by the Profiler. Each node * corresponds to a single line viewable in a UI-based representation. - * + * * @author rgarcia */ public class ProfileNode { - private HashMap nodes; - private double selfMillisecs; - private double totalMillisecs; - private int selfSampleCount; - private int totalSampleCount; - - private String key; - - /** - * Horribly hacky field only used by ink unity integration, but saves - * constructing an entire data structure that mirrors the one in here purely to - * store the state of whether each node in the UI has been opened or not. - */ - public boolean openInUI; - - /** - * Whether this node contains any sub-nodes - i.e. does it call anything else - * that has been recorded? - * - * @return true if has children; otherwise, false. - */ - public boolean hasChildren() { - return nodes != null && nodes.size() > 0; - } - - /** - * The key for the node corresponds to the printable name of the callstack - * element. - */ - public String getKey() { - return key; - } - - ProfileNode() { - - } - - ProfileNode(String key) { - this.key = key; - } - - void addSample(String[] stack, double duration) { - addSample(stack, -1, duration); - } - - void addSample(String[] stack, int stackIdx, double duration) { - - totalSampleCount++; - totalMillisecs += duration; - - if (stackIdx == stack.length - 1) { - selfSampleCount++; - selfMillisecs += duration; - } - - if (stackIdx + 1 < stack.length) - addSampleToNode(stack, stackIdx + 1, duration); - } - - void addSampleToNode(String[] stack, int stackIdx, double duration) { - String nodeKey = stack[stackIdx]; - if (nodes == null) - nodes = new HashMap<>(); - - ProfileNode node = nodes.get(nodeKey); - - if (node == null) { - node = new ProfileNode(nodeKey); - nodes.put(nodeKey, node); - } - - node.addSample(stack, stackIdx, duration); - } - - /** - * Returns a sorted enumerable of the nodes in descending order of how long they - * took to run. - */ - public Iterable> getDescendingOrderedNodes() { - if (nodes == null) - return null; - - List> averageStepTimes = new LinkedList<>( - nodes.entrySet()); - - Collections.sort(averageStepTimes, new Comparator>() { - @Override - public int compare(Entry o1, Entry o2) { - return (int) (o1.getValue().totalMillisecs - o2.getValue().totalMillisecs); - } - }); - - return averageStepTimes; - } - - void printHierarchy(StringBuilder sb, int indent) { - pad(sb, indent); - - sb.append(key); - sb.append(": "); - sb.append(getOwnReport()); - sb.append('\n'); - - if (nodes == null) - return; - - for (Entry keyNode : getDescendingOrderedNodes()) { - keyNode.getValue().printHierarchy(sb, indent + 1); - } - } - - /** - * Generates a string giving timing information for this single node, including - * total milliseconds spent on the piece of ink, the time spent within itself - * (v.s. spent in children), as well as the number of samples (instruction - * steps) recorded for both too. - * - * @return The own report. - */ - public String getOwnReport() { - StringBuilder sb = new StringBuilder(); - sb.append("total "); - sb.append(Profiler.formatMillisecs(totalMillisecs)); - sb.append(", self "); - sb.append(Profiler.formatMillisecs(selfMillisecs)); - sb.append(" ("); - sb.append(selfSampleCount); - sb.append(" self samples, "); - sb.append(totalSampleCount); - sb.append(" total)"); - - return sb.toString(); - } - - void pad(StringBuilder sb, int spaces) { - for (int i = 0; i < spaces; i++) - sb.append(" "); - } - - /** - * Total number of milliseconds this node has been active for. - */ - public int getTotalMillisecs() { - return (int) totalMillisecs; - } - - /** - * String is a report of the sub-tree from this node, but without any of the - * header information that's prepended by the Profiler in its Report() method. - */ - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - printHierarchy(sb, 0); - return sb.toString(); - } + private HashMap nodes; + private double selfMillisecs; + private double totalMillisecs; + private int selfSampleCount; + private int totalSampleCount; + + private String key; + + /** + * Horribly hacky field only used by ink unity integration, but saves + * constructing an entire data structure that mirrors the one in here purely to + * store the state of whether each node in the UI has been opened or not. + */ + public boolean openInUI; + + /** + * Whether this node contains any sub-nodes - i.e. does it call anything else + * that has been recorded? + * + * @return true if has children; otherwise, false. + */ + public boolean hasChildren() { + return nodes != null && nodes.size() > 0; + } + + /** + * The key for the node corresponds to the printable name of the callstack + * element. + */ + public String getKey() { + return key; + } + + ProfileNode() {} + + ProfileNode(String key) { + this.key = key; + } + + void addSample(String[] stack, double duration) { + addSample(stack, -1, duration); + } + + void addSample(String[] stack, int stackIdx, double duration) { + + totalSampleCount++; + totalMillisecs += duration; + + if (stackIdx == stack.length - 1) { + selfSampleCount++; + selfMillisecs += duration; + } + + if (stackIdx + 1 < stack.length) addSampleToNode(stack, stackIdx + 1, duration); + } + + void addSampleToNode(String[] stack, int stackIdx, double duration) { + String nodeKey = stack[stackIdx]; + if (nodes == null) nodes = new HashMap<>(); + + ProfileNode node = nodes.get(nodeKey); + + if (node == null) { + node = new ProfileNode(nodeKey); + nodes.put(nodeKey, node); + } + + node.addSample(stack, stackIdx, duration); + } + + /** + * Returns a sorted enumerable of the nodes in descending order of how long they + * took to run. + */ + public Iterable> getDescendingOrderedNodes() { + if (nodes == null) return null; + + List> averageStepTimes = new LinkedList<>(nodes.entrySet()); + + Collections.sort(averageStepTimes, new Comparator>() { + @Override + public int compare(Entry o1, Entry o2) { + return (int) (o1.getValue().totalMillisecs - o2.getValue().totalMillisecs); + } + }); + + return averageStepTimes; + } + + void printHierarchy(StringBuilder sb, int indent) { + pad(sb, indent); + + sb.append(key); + sb.append(": "); + sb.append(getOwnReport()); + sb.append('\n'); + + if (nodes == null) return; + + for (Entry keyNode : getDescendingOrderedNodes()) { + keyNode.getValue().printHierarchy(sb, indent + 1); + } + } + + /** + * Generates a string giving timing information for this single node, including + * total milliseconds spent on the piece of ink, the time spent within itself + * (v.s. spent in children), as well as the number of samples (instruction + * steps) recorded for both too. + * + * @return The own report. + */ + public String getOwnReport() { + StringBuilder sb = new StringBuilder(); + sb.append("total "); + sb.append(Profiler.formatMillisecs(totalMillisecs)); + sb.append(", self "); + sb.append(Profiler.formatMillisecs(selfMillisecs)); + sb.append(" ("); + sb.append(selfSampleCount); + sb.append(" self samples, "); + sb.append(totalSampleCount); + sb.append(" total)"); + + return sb.toString(); + } + + void pad(StringBuilder sb, int spaces) { + for (int i = 0; i < spaces; i++) sb.append(" "); + } + + /** + * Total number of milliseconds this node has been active for. + */ + public int getTotalMillisecs() { + return (int) totalMillisecs; + } + + /** + * String is a report of the sub-tree from this node, but without any of the + * header information that's prepended by the Profiler in its Report() method. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + printHierarchy(sb, 0); + return sb.toString(); + } } diff --git a/src/main/java/com/bladecoder/ink/runtime/Profiler.java b/src/main/java/com/bladecoder/ink/runtime/Profiler.java index e7ff392..63536d1 100644 --- a/src/main/java/com/bladecoder/ink/runtime/Profiler.java +++ b/src/main/java/com/bladecoder/ink/runtime/Profiler.java @@ -1,5 +1,6 @@ package com.bladecoder.ink.runtime; +import com.bladecoder.ink.runtime.Path.Component; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -8,295 +9,293 @@ import java.util.List; import java.util.Map.Entry; -import com.bladecoder.ink.runtime.Path.Component; - /** * Simple ink profiler that logs every instruction in the story and counts * frequency and timing. To use: - * + * * var profiler = story.StartProfiling(), - * + * * (play your story for a bit) - * + * * var reportStr = profiler.Report(); - * + * * story.EndProfiling(); - * + * */ public class Profiler { - private Stopwatch continueWatch = new Stopwatch(); - private Stopwatch stepWatch = new Stopwatch(); - private Stopwatch snapWatch = new Stopwatch(); - - private double continueTotal; - private double snapTotal; - private double stepTotal; - - private String[] currStepStack; - private StepDetails currStepDetails; - private ProfileNode rootNode; - private int numContinues; - - private class StepDetails { - public String type; - public RTObject obj; - public double time; - - StepDetails(String type, RTObject obj, double time) { - this.type = type; - this.obj = obj; - this.time = time; - } - } - - private List stepDetails = new ArrayList(); - - /** - * The root node in the hierarchical tree of recorded ink timings. - */ - public ProfileNode getRootNode() { - return rootNode; - } - - Profiler() { - rootNode = new ProfileNode(); - } - - /** - * Generate a printable report based on the data recording during profiling. - */ - public String report() { - StringBuilder sb = new StringBuilder(); - - sb.append(String.format("%d CONTINUES / LINES:\n", numContinues)); - sb.append(String.format("TOTAL TIME: %s\n", formatMillisecs(continueTotal))); - sb.append(String.format("SNAPSHOTTING: %s\n", formatMillisecs(snapTotal))); - sb.append(String.format("OTHER: %s\n", formatMillisecs(continueTotal - (stepTotal + snapTotal)))); - sb.append(rootNode.toString()); - - return sb.toString(); - } - - void preContinue() { - continueWatch.reset(); - continueWatch.start(); - } - - void postContinue() { - continueWatch.stop(); - continueTotal += millisecs(continueWatch); - numContinues++; - } - - void preStep() { - currStepStack = null; - stepWatch.reset(); - stepWatch.start(); - } - - void step(CallStack callstack) { - stepWatch.stop(); - - String[] stack = new String[callstack.getElements().size()]; - for (int i = 0; i < stack.length; i++) { - Path objPath = callstack.getElements().get(i).currentPointer.getPath(); - String stackElementName = ""; - - for (int c = 0; c < objPath.getLength(); c++) { - Component comp = objPath.getComponent(c); - if (!comp.isIndex()) { - stackElementName = comp.getName(); - break; - } - } - - stack[i] = stackElementName; - } - - currStepStack = stack; - - RTObject currObj = callstack.getCurrentElement().currentPointer.resolve(); - - String stepType = null; - ControlCommand controlCommandStep = currObj instanceof ControlCommand ? (ControlCommand) currObj : null; - if (controlCommandStep != null) - stepType = controlCommandStep.getCommandType().toString() + " CC"; - else - stepType = currObj.getClass().getSimpleName(); - - currStepDetails = new StepDetails(stepType, currObj, 0f); - - stepWatch.start(); - } - - void postStep() { - stepWatch.stop(); - - double duration = millisecs(stepWatch); - stepTotal += duration; - - rootNode.addSample(currStepStack, duration); - - currStepDetails.time = duration; - stepDetails.add(currStepDetails); - } - - /** - * Generate a printable report specifying the average and maximum times spent - * stepping over different internal ink instruction types. This report type is - * primarily used to profile the ink engine itself rather than your own specific - * ink. - */ - public String stepLengthReport() { - StringBuilder sb = new StringBuilder(); - - sb.append("TOTAL: " + rootNode.getTotalMillisecs() + "ms\n"); - - // AVERAGE STEP TIMES - HashMap typeToDetails = new HashMap(); - - // average group by s.type - for (StepDetails sd : stepDetails) { - if (typeToDetails.containsKey(sd.type)) - continue; - - String type = sd.type; - double avg = 0f; - float num = 0; - - for (StepDetails sd2 : stepDetails) { - if (type.equals(sd2.type)) { - num++; - avg += sd2.time; - } - } - - avg = avg / num; - - typeToDetails.put(sd.type, avg); - } - - // sort by average - List> averageStepTimes = new LinkedList>(typeToDetails.entrySet()); - - Collections.sort(averageStepTimes, new Comparator>() { - public int compare(Entry o1, Entry o2) { - return (int) (o1.getValue() - o2.getValue()); - } - }); - - // join times - sb.append("AVERAGE STEP TIMES: "); - for (int i = 0; i < averageStepTimes.size(); i++) { - sb.append(averageStepTimes.get(i).getKey()); - sb.append(": "); - sb.append(averageStepTimes.get(i).getValue()); - sb.append("ms"); - - if (i != averageStepTimes.size() - 1) - sb.append(','); - } - - sb.append('\n'); - - - // ACCUMULATED STEP TIMES - typeToDetails.clear(); - - // average group by s.type - for (StepDetails sd : stepDetails) { - if (typeToDetails.containsKey(sd.type)) - continue; - - String type = sd.type; - double sum = 0f; - - for (StepDetails sd2 : stepDetails) { - if (type.equals(sd2.type)) { - sum += sd2.time; - } - } - - typeToDetails.put(sd.type + " (x"+typeToDetails.size()+")", sum); - } - - // sort by average - List> accumStepTimes = new LinkedList>(typeToDetails.entrySet()); - - Collections.sort(accumStepTimes, new Comparator>() { - public int compare(Entry o1, Entry o2) { - return (int) (o1.getValue() - o2.getValue()); - } - }); - - // join times - sb.append("ACCUMULATED STEP TIMES: "); - for (int i = 0; i < accumStepTimes.size(); i++) { - sb.append(accumStepTimes.get(i).getKey()); - sb.append(": "); - sb.append(accumStepTimes.get(i).getValue()); - - if (i != accumStepTimes.size() - 1) - sb.append(','); - } - - sb.append('\n'); - - return sb.toString(); - } - - /** - * Create a large log of all the internal instructions that were evaluated while - * profiling was active. Log is in a tab-separated format, for easy loading into - * a spreadsheet application. - */ - public String megalog() { - StringBuilder sb = new StringBuilder(); - - sb.append("Step type\tDescription\tPath\tTime\n"); - - for (StepDetails step : stepDetails) { - sb.append(step.type); - sb.append("\t"); - sb.append(step.obj.toString()); - sb.append("\t"); - sb.append(step.obj.getPath()); - sb.append("\t"); - sb.append(Double.toString(step.time)); - sb.append('\n'); - } - - return sb.toString(); - } - - void preSnapshot() { - snapWatch.reset(); - snapWatch.start(); - } - - void postSnapshot() { - snapWatch.stop(); - snapTotal += millisecs(snapWatch); - } - - double millisecs(Stopwatch watch) { - return watch.getElapsedMilliseconds(); - } - - static String formatMillisecs(double num) { - if (num > 5000) { - return String.format("%.1f secs", num / 1000.0); - } - if (num > 1000) { - return String.format("%.2f secs", num / 1000.0); - } else if (num > 100) { - return String.format("%.0f ms", num); - } else if (num > 1) { - return String.format("%.1f ms", num); - } else if (num > 0.01) { - return String.format("%.3f ms", num); - } else { - return String.format("%.0f ms", num); - } - } -} \ No newline at end of file + private Stopwatch continueWatch = new Stopwatch(); + private Stopwatch stepWatch = new Stopwatch(); + private Stopwatch snapWatch = new Stopwatch(); + + private double continueTotal; + private double snapTotal; + private double stepTotal; + + private String[] currStepStack; + private StepDetails currStepDetails; + private ProfileNode rootNode; + private int numContinues; + + private class StepDetails { + public String type; + public RTObject obj; + public double time; + + StepDetails(String type, RTObject obj, double time) { + this.type = type; + this.obj = obj; + this.time = time; + } + } + + private List stepDetails = new ArrayList<>(); + + /** + * The root node in the hierarchical tree of recorded ink timings. + */ + public ProfileNode getRootNode() { + return rootNode; + } + + Profiler() { + rootNode = new ProfileNode(); + } + + /** + * Generate a printable report based on the data recording during profiling. + */ + public String report() { + StringBuilder sb = new StringBuilder(); + + sb.append(String.format("%d CONTINUES / LINES:\n", numContinues)); + sb.append(String.format("TOTAL TIME: %s\n", formatMillisecs(continueTotal))); + sb.append(String.format("SNAPSHOTTING: %s\n", formatMillisecs(snapTotal))); + sb.append(String.format("OTHER: %s\n", formatMillisecs(continueTotal - (stepTotal + snapTotal)))); + sb.append(rootNode.toString()); + + return sb.toString(); + } + + void preContinue() { + continueWatch.reset(); + continueWatch.start(); + } + + void postContinue() { + continueWatch.stop(); + continueTotal += millisecs(continueWatch); + numContinues++; + } + + void preStep() { + currStepStack = null; + stepWatch.reset(); + stepWatch.start(); + } + + void step(CallStack callstack) { + stepWatch.stop(); + + String[] stack = new String[callstack.getElements().size()]; + for (int i = 0; i < stack.length; i++) { + + String stackElementName = ""; + + if (!callstack.getElements().get(i).currentPointer.isNull()) { + Path objPath = callstack.getElements().get(i).currentPointer.getPath(); + + for (int c = 0; c < objPath.getLength(); c++) { + Component comp = objPath.getComponent(c); + if (!comp.isIndex()) { + stackElementName = comp.getName(); + break; + } + } + } + + stack[i] = stackElementName; + } + + currStepStack = stack; + + RTObject currObj = callstack.getCurrentElement().currentPointer.resolve(); + + String stepType = null; + ControlCommand controlCommandStep = currObj instanceof ControlCommand ? (ControlCommand) currObj : null; + if (controlCommandStep != null) + stepType = controlCommandStep.getCommandType().toString() + " CC"; + else stepType = currObj.getClass().getSimpleName(); + + currStepDetails = new StepDetails(stepType, currObj, 0f); + + stepWatch.start(); + } + + void postStep() { + stepWatch.stop(); + + double duration = millisecs(stepWatch); + stepTotal += duration; + + rootNode.addSample(currStepStack, duration); + + currStepDetails.time = duration; + stepDetails.add(currStepDetails); + } + + /** + * Generate a printable report specifying the average and maximum times spent + * stepping over different internal ink instruction types. This report type is + * primarily used to profile the ink engine itself rather than your own specific + * ink. + */ + public String stepLengthReport() { + StringBuilder sb = new StringBuilder(); + + sb.append("TOTAL: " + rootNode.getTotalMillisecs() + "ms\n"); + + // AVERAGE STEP TIMES + HashMap typeToDetails = new HashMap<>(); + + // average group by s.type + for (StepDetails sd : stepDetails) { + if (typeToDetails.containsKey(sd.type)) continue; + + String type = sd.type; + double avg = 0f; + float num = 0; + + for (StepDetails sd2 : stepDetails) { + if (type.equals(sd2.type)) { + num++; + avg += sd2.time; + } + } + + avg = avg / num; + + typeToDetails.put(sd.type, avg); + } + + // sort by average + List> averageStepTimes = new LinkedList<>(typeToDetails.entrySet()); + + Collections.sort(averageStepTimes, new Comparator>() { + @Override + public int compare(Entry o1, Entry o2) { + return (int) (o1.getValue() - o2.getValue()); + } + }); + + // join times + sb.append("AVERAGE STEP TIMES: "); + for (int i = 0; i < averageStepTimes.size(); i++) { + sb.append(averageStepTimes.get(i).getKey()); + sb.append(": "); + sb.append(averageStepTimes.get(i).getValue()); + sb.append("ms"); + + if (i != averageStepTimes.size() - 1) sb.append(','); + } + + sb.append('\n'); + + // ACCUMULATED STEP TIMES + typeToDetails.clear(); + + // average group by s.type + for (StepDetails sd : stepDetails) { + if (typeToDetails.containsKey(sd.type)) continue; + + String type = sd.type; + double sum = 0f; + + for (StepDetails sd2 : stepDetails) { + if (type.equals(sd2.type)) { + sum += sd2.time; + } + } + + typeToDetails.put(sd.type + " (x" + typeToDetails.size() + ")", sum); + } + + // sort by average + List> accumStepTimes = new LinkedList<>(typeToDetails.entrySet()); + + Collections.sort(accumStepTimes, new Comparator>() { + @Override + public int compare(Entry o1, Entry o2) { + return (int) (o1.getValue() - o2.getValue()); + } + }); + + // join times + sb.append("ACCUMULATED STEP TIMES: "); + for (int i = 0; i < accumStepTimes.size(); i++) { + sb.append(accumStepTimes.get(i).getKey()); + sb.append(": "); + sb.append(accumStepTimes.get(i).getValue()); + + if (i != accumStepTimes.size() - 1) sb.append(','); + } + + sb.append('\n'); + + return sb.toString(); + } + + /** + * Create a large log of all the internal instructions that were evaluated while + * profiling was active. Log is in a tab-separated format, for easy loading into + * a spreadsheet application. + */ + public String megalog() { + StringBuilder sb = new StringBuilder(); + + sb.append("Step type\tDescription\tPath\tTime\n"); + + for (StepDetails step : stepDetails) { + sb.append(step.type); + sb.append("\t"); + sb.append(step.obj.toString()); + sb.append("\t"); + sb.append(step.obj.getPath()); + sb.append("\t"); + sb.append(Double.toString(step.time)); + sb.append('\n'); + } + + return sb.toString(); + } + + void preSnapshot() { + snapWatch.reset(); + snapWatch.start(); + } + + void postSnapshot() { + snapWatch.stop(); + snapTotal += millisecs(snapWatch); + } + + double millisecs(Stopwatch watch) { + return watch.getElapsedMilliseconds(); + } + + static String formatMillisecs(double num) { + if (num > 5000) { + return String.format("%.1f secs", num / 1000.0); + } + if (num > 1000) { + return String.format("%.2f secs", num / 1000.0); + } else if (num > 100) { + return String.format("%.0f ms", num); + } else if (num > 1) { + return String.format("%.1f ms", num); + } else if (num > 0.01) { + return String.format("%.3f ms", num); + } else { + return String.format("%.0f ms", num); + } + } +} diff --git a/src/main/java/com/bladecoder/ink/runtime/PushPopType.java b/src/main/java/com/bladecoder/ink/runtime/PushPopType.java index dd57fb0..353b465 100644 --- a/src/main/java/com/bladecoder/ink/runtime/PushPopType.java +++ b/src/main/java/com/bladecoder/ink/runtime/PushPopType.java @@ -1,5 +1,7 @@ -package com.bladecoder.ink.runtime; - -public enum PushPopType { - Tunnel, Function, FunctionEvaluationFromGame -} +package com.bladecoder.ink.runtime; + +public enum PushPopType { + Tunnel, + Function, + FunctionEvaluationFromGame +} diff --git a/src/main/java/com/bladecoder/ink/runtime/RTObject.java b/src/main/java/com/bladecoder/ink/runtime/RTObject.java index 1f22425..3e8bbdd 100644 --- a/src/main/java/com/bladecoder/ink/runtime/RTObject.java +++ b/src/main/java/com/bladecoder/ink/runtime/RTObject.java @@ -1,198 +1,183 @@ -package com.bladecoder.ink.runtime; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import com.bladecoder.ink.runtime.Path.Component; - -/** - * Base class for all ink runtime content. - */ -/* TODO: abstract */ -public class RTObject { - /** - * Runtime.RTObjects can be included in the main Story as a hierarchy. Usually - * parents are Container RTObjects. (TODO: Always?) The parent. - */ - private RTObject parent; - - private Path path; - - public RTObject() { - } - - // TODO: Come up with some clever solution for not having - // to have debug metadata on the RTObject itself, perhaps - // for serialisation purposes at least. - private DebugMetadata debugMetadata; - - public RTObject getParent() { - return parent; - } - - public void setParent(RTObject value) { - parent = value; - } - - - DebugMetadata getOwnDebugMetadata() { - return debugMetadata; - } - - public DebugMetadata getDebugMetadata() { - if (debugMetadata == null) { - if (getParent() != null) { - return getParent().getDebugMetadata(); - } - } - - return debugMetadata; - } - - public void setDebugMetadata(DebugMetadata value) { - debugMetadata = value; - } - - public Integer debugLineNumberOfPath(Path path) throws Exception { - // FIXME Added path.isRelative() because orginal code not working - if (path == null || path.isRelative()) - return null; - - // Try to get a line number from debug metadata - Container root = this.getRootContentContainer(); - - if (root != null) { - - RTObject targetContent = root.contentAtPath(path).obj; - - if (targetContent != null) { - DebugMetadata dm = targetContent.debugMetadata; - if (dm != null) { - return dm.startLineNumber; - } - - } - - } - - return null; - } - - public Path getPath() { - if (path == null) { - if (getParent() == null) { - path = new Path(); - } else { - List comps = new ArrayList(); - RTObject child = this; - Container container = child.getParent() instanceof Container ? (Container) child.getParent() - : (Container) null; - while (container != null) { - INamedContent namedChild = child instanceof INamedContent ? (INamedContent) child - : (INamedContent) null; - if (namedChild != null && namedChild.hasValidName()) { - comps.add(new Path.Component(namedChild.getName())); - } else { - comps.add(new Component(container.getContent().indexOf(child))); - } - child = container; - container = container.getParent() instanceof Container ? (Container) container.getParent() - : (Container) null; - } - - // Reverse list because components are searched in reverse - // order. - Collections.reverse(comps); - - path = new Path(comps); - } - } - - return path; - } - - public SearchResult resolvePath(Path path) throws Exception { - if (path.isRelative()) { - Container nearestContainer = this instanceof Container ? (Container) this : (Container) null; - - if (nearestContainer == null) { - // Debug.Assert(this.getparent() != null, "Can't resolve - // relative path because we don't have a parent"); - nearestContainer = this.getParent() instanceof Container ? (Container) this.getParent() - : (Container) null; - // Debug.Assert(nearestContainer != null, "Expected parent to be - // a container"); - // Debug.Assert(path.getcomponents()[0].isParent); - path = path.getTail(); - } - - return nearestContainer.contentAtPath(path); - } else { - return this.getRootContentContainer().contentAtPath(path); - } - } - - public Path convertPathToRelative(Path globalPath) { - // 1. Find last shared ancestor - // 2. Drill up using ".." style (actually represented as "^") - // 3. Re-build downward chain from common ancestor - Path ownPath = this.getPath(); - int minPathLength = Math.min(globalPath.getLength(), ownPath.getLength()); - int lastSharedPathCompIndex = -1; - for (int i = 0; i < minPathLength; ++i) { - Component ownComp = ownPath.getComponent(i); - Component otherComp = globalPath.getComponent(i); - - if (ownComp.equals(otherComp)) { - lastSharedPathCompIndex = i; - } else { - break; - } - } - // No shared path components, so just use global path - if (lastSharedPathCompIndex == -1) - return globalPath; - - int numUpwardsMoves = (ownPath.getLength() - 1) - lastSharedPathCompIndex; - ArrayList newPathComps = new ArrayList(); - - for (int up = 0; up < numUpwardsMoves; ++up) - newPathComps.add(Path.Component.toParent()); - - for (int down = lastSharedPathCompIndex + 1; down < globalPath.getLength(); ++down) - newPathComps.add(globalPath.getComponent(down)); - - Path relativePath = new Path(newPathComps, true); - return relativePath; - } - - // Find most compact representation for a path, whether relative or global - public String compactPathString(Path otherPath) { - String globalPathStr = null; - String relativePathStr = null; - if (otherPath.isRelative()) { - relativePathStr = otherPath.getComponentsString(); - globalPathStr = this.getPath().pathByAppendingPath(otherPath).getComponentsString(); - } else { - Path relativePath = convertPathToRelative(otherPath); - relativePathStr = relativePath.getComponentsString(); - globalPathStr = otherPath.getComponentsString(); - } - if (relativePathStr.length() < globalPathStr.length()) - return relativePathStr; - else - return globalPathStr; - } - - public Container getRootContentContainer() { - RTObject ancestor = this; - while (ancestor.getParent() != null) { - ancestor = ancestor.getParent(); - } - return ancestor instanceof Container ? (Container) ancestor : (Container) null; - } - - RTObject copy() throws Exception { - throw new UnsupportedOperationException(this.getClass().getSimpleName() + " doesn't support copying"); - } -} +package com.bladecoder.ink.runtime; + +import com.bladecoder.ink.runtime.Path.Component; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Base class for all ink runtime content. + */ +/* TODO: abstract */ +public class RTObject { + /** + * Runtime.RTObjects can be included in the main Story as a hierarchy. Usually + * parents are Container RTObjects. + */ + private Container parent; + + private Path path; + + public RTObject() {} + + // TODO: Come up with some clever solution for not having + // to have debug metadata on the RTObject itself, perhaps + // for serialisation purposes at least. + private DebugMetadata debugMetadata; + + public Container getParent() { + return parent; + } + + public void setParent(Container value) { + parent = value; + } + + DebugMetadata getOwnDebugMetadata() { + return debugMetadata; + } + + public DebugMetadata getDebugMetadata() { + if (debugMetadata == null) { + if (getParent() != null) { + return getParent().getDebugMetadata(); + } + } + + return debugMetadata; + } + + public void setDebugMetadata(DebugMetadata value) { + debugMetadata = value; + } + + public Integer debugLineNumberOfPath(Path path) throws Exception { + // FIXME Added path.isRelative() because orginal code not working + if (path == null || path.isRelative()) return null; + + // Try to get a line number from debug metadata + Container root = this.getRootContentContainer(); + + if (root != null) { + + RTObject targetContent = root.contentAtPath(path).obj; + + if (targetContent != null) { + DebugMetadata dm = targetContent.debugMetadata; + if (dm != null) { + return dm.startLineNumber; + } + } + } + + return null; + } + + public Path getPath() { + if (path == null) { + if (getParent() == null) { + path = new Path(); + } else { + List comps = new ArrayList(); + RTObject child = this; + Container container = child.getParent(); + while (container != null) { + if (child instanceof Container && ((Container) child).hasValidName()) { + comps.add(new Path.Component(((Container) child).getName())); + } else { + comps.add(new Component(container.getContent().indexOf(child))); + } + child = container; + container = container.getParent(); + } + + // Reverse list because components are searched in reverse + // order. + Collections.reverse(comps); + + path = new Path(comps); + } + } + + return path; + } + + public SearchResult resolvePath(Path path) throws Exception { + if (path.isRelative()) { + Container nearestContainer = this instanceof Container ? (Container) this : null; + + if (nearestContainer == null) { + // Debug.Assert(this.getparent() != null, "Can't resolve + // relative path because we don't have a parent"); + nearestContainer = this.getParent() != null ? this.getParent() : null; + // Debug.Assert(nearestContainer != null, "Expected parent to be + // a container"); + // Debug.Assert(path.getcomponents()[0].isParent); + path = path.getTail(); + } + + return nearestContainer.contentAtPath(path); + } else { + return this.getRootContentContainer().contentAtPath(path); + } + } + + public Path convertPathToRelative(Path globalPath) { + // 1. Find last shared ancestor + // 2. Drill up using ".." style (actually represented as "^") + // 3. Re-build downward chain from common ancestor + Path ownPath = this.getPath(); + int minPathLength = Math.min(globalPath.getLength(), ownPath.getLength()); + int lastSharedPathCompIndex = -1; + for (int i = 0; i < minPathLength; ++i) { + Component ownComp = ownPath.getComponent(i); + Component otherComp = globalPath.getComponent(i); + + if (ownComp.equals(otherComp)) { + lastSharedPathCompIndex = i; + } else { + break; + } + } + // No shared path components, so just use global path + if (lastSharedPathCompIndex == -1) return globalPath; + + int numUpwardsMoves = (ownPath.getLength() - 1) - lastSharedPathCompIndex; + ArrayList newPathComps = new ArrayList(); + + for (int up = 0; up < numUpwardsMoves; ++up) newPathComps.add(Path.Component.toParent()); + + for (int down = lastSharedPathCompIndex + 1; down < globalPath.getLength(); ++down) + newPathComps.add(globalPath.getComponent(down)); + + return new Path(newPathComps, true); + } + + // Find most compact representation for a path, whether relative or global + public String compactPathString(Path otherPath) { + String globalPathStr = null; + String relativePathStr = null; + if (otherPath.isRelative()) { + relativePathStr = otherPath.getComponentsString(); + globalPathStr = this.getPath().pathByAppendingPath(otherPath).getComponentsString(); + } else { + Path relativePath = convertPathToRelative(otherPath); + relativePathStr = relativePath.getComponentsString(); + globalPathStr = otherPath.getComponentsString(); + } + if (relativePathStr.length() < globalPathStr.length()) return relativePathStr; + else return globalPathStr; + } + + public Container getRootContentContainer() { + RTObject ancestor = this; + while (ancestor.getParent() != null) { + ancestor = ancestor.getParent(); + } + return ancestor instanceof Container ? (Container) ancestor : null; + } + + RTObject copy() throws Exception { + throw new UnsupportedOperationException(this.getClass().getSimpleName() + " doesn't support copying"); + } +} diff --git a/src/main/java/com/bladecoder/ink/runtime/SearchResult.java b/src/main/java/com/bladecoder/ink/runtime/SearchResult.java index 1b7c36f..458846e 100644 --- a/src/main/java/com/bladecoder/ink/runtime/SearchResult.java +++ b/src/main/java/com/bladecoder/ink/runtime/SearchResult.java @@ -8,27 +8,25 @@ * try to recover by finding an approximate result by working up the story hierarchy * in the path to find the closest valid container. Instead of crashing horribly, * we might see some slight oddness in the content, but hopefully it recovers! - + * * @author rgarcia */ -class SearchResult { - public RTObject obj; - public boolean approximate; - - public SearchResult() { - - } - - public SearchResult(SearchResult sr) { - obj = sr.obj; - approximate = sr.approximate; - } +public class SearchResult { + public RTObject obj; + public boolean approximate; + + public SearchResult() {} + + public SearchResult(SearchResult sr) { + obj = sr.obj; + approximate = sr.approximate; + } - public RTObject correctObj() { - return approximate ? null : obj; - } + public RTObject correctObj() { + return approximate ? null : obj; + } - public Container getContainer() { - return obj instanceof Container? (Container)obj:null; - } + public Container getContainer() { + return obj instanceof Container ? (Container) obj : null; + } } diff --git a/src/main/java/com/bladecoder/ink/runtime/SimpleJson.java b/src/main/java/com/bladecoder/ink/runtime/SimpleJson.java index 630ef7b..a09f94c 100644 --- a/src/main/java/com/bladecoder/ink/runtime/SimpleJson.java +++ b/src/main/java/com/bladecoder/ink/runtime/SimpleJson.java @@ -1,588 +1,565 @@ -package com.bladecoder.ink.runtime; - -import java.io.BufferedWriter; -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.StringWriter; -import java.io.UnsupportedEncodingException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Stack; - -/** - * Simple custom JSON serialisation implementation that takes JSON-able - * System.Collections that are produced by the ink engine and converts to and - * from JSON text. - */ -class SimpleJson { - - public static HashMap textToDictionary(String text) throws Exception { - return new Reader(text).toHashMap(); - } - - public static List textToArray(String text) throws Exception { - return new Reader(text).toArray(); - } - - static class Reader { - private int offset; - - private Object rootObject; - - private String text; - - public Reader(String text) throws Exception { - this.text = text; - offset = 0; - skipWhitespace(); - rootObject = readObject(); - } - - void expect(boolean condition, String message) throws Exception { - if (!condition) { - if (message == null) { - message = "Unexpected token"; - } else { - message = "Expected " + message; - } - message += " at offset " + offset; - throw new Exception(message); - } - - } - - void expect(String expectedStr) throws Exception { - if (!tryRead(expectedStr)) - expect(false, expectedStr); - - } - - boolean isNumberChar(char c) throws Exception { - return c >= '0' && c <= '9' || c == '.' || c == '-' || c == '+' || c == 'E' || c == 'e'; - } - - boolean IsFirstNumberChar(char c) { - return c >= '0' && c <= '9' || c == '-' || c == '+'; - } - - List readArray() throws Exception { - List list = new ArrayList<>(); - expect("["); - skipWhitespace(); - // Empty list? - if (tryRead("]")) - return list; - - do { - skipWhitespace(); - // Value - Object val = readObject(); - // Add to array - list.add(val); - skipWhitespace(); - } while (tryRead(",")); - expect("]"); - return list; - } - - HashMap readHashMap() throws Exception { - HashMap dict = new HashMap<>(); - expect("{"); - skipWhitespace(); - // Empty HashMap? - if (tryRead("}")) - return dict; - - do { - skipWhitespace(); - // Key - String key = readString(); - expect(key != null, "dictionary key"); - skipWhitespace(); - // : - expect(":"); - skipWhitespace(); - // Value - Object val = readObject(); - expect(val != null, "dictionary value"); - // Add to HashMap - dict.put(key, val); - skipWhitespace(); - } while (tryRead(",")); - expect("}"); - return dict; - } - - Object readNumber() throws Exception { - int startOffset = offset; - boolean isFloat = false; - for (; offset < text.length(); offset++) { - char c = text.charAt(offset); - if (c == '.' || c == 'e' || c == 'E') - isFloat = true; - - if (isNumberChar(c)) - continue; - else - break; - } - - String numStr = text.substring(startOffset, offset); - if (isFloat) { - try { - float f = Float.parseFloat(numStr); - return f; - } catch (NumberFormatException e) { - - } - } else { - try { - int i = Integer.parseInt(numStr); - return i; - } catch (NumberFormatException e) { - - } - - } - - throw new Exception("Failed to parse number value: " + numStr); - } - - Object readObject() throws Exception { - char currentChar = text.charAt(offset); - - if (currentChar == '{') - return readHashMap(); - else if (currentChar == '[') - return readArray(); - else if (currentChar == '"') - return readString(); - else if (IsFirstNumberChar(currentChar)) - return readNumber(); - else if (tryRead("true")) - return true; - else if (tryRead("false")) - return false; - else if (tryRead("null")) - return null; - - throw new Exception("Unhandled RTObject type in JSON: " + text.substring(offset, offset + 30)); - } - - String readString() throws Exception { - expect("\""); - StringBuilder sb = new StringBuilder(); - - for (; offset < text.length(); offset++) { - char c = text.charAt(offset); - - if (c == '\\') { - // Escaped character - offset++; - if (offset >= text.length()) { - throw new Exception("Unexpected EOF while reading string"); - } - c = text.charAt(offset); - switch (c) { - case '"': - case '\\': - case '/': // Yes, JSON allows this to be escaped - sb.append(c); - break; - case 'n': - sb.append('\n'); - break; - case 't': - sb.append('\t'); - break; - case 'r': - case 'b': - case 'f': - // Ignore other control characters - break; - case 'u': - // 4-digit Unicode - if (offset + 4 >= text.length()) { - throw new Exception("Unexpected EOF while reading string"); - } - - // c# expr: _text.SubString(_offset + 1, 4); - String digits = text.substring(offset + 1, offset + 6); - - int uchar; - - try { - uchar = Integer.parseInt(digits, 16); - sb.append((char) uchar); - offset += 4; - } catch (NumberFormatException e) { - throw new Exception("Invalid Unicode escape character at offset " + (offset - 1)); - } - break; - - default: - // The escaped character is invalid per json spec - throw new Exception("Invalid Unicode escape character at offset " + (offset - 1)); - } - } else if (c == '"') { - break; - } else { - sb.append(c); - } - - } - expect("\""); - - return sb.toString(); - } - - void skipWhitespace() throws Exception { - while (offset < text.length()) { - char c = text.charAt(offset); - if (c == ' ' || c == '\t' || c == '\n' || c == '\r') - offset++; - else - break; - } - } - - @SuppressWarnings("unchecked") - public HashMap toHashMap() throws Exception { - return (HashMap) rootObject; - } - - @SuppressWarnings("unchecked") - public List toArray() { - return (List) rootObject; - } - - boolean tryRead(String textToRead) throws Exception { - if (offset + textToRead.length() > text.length()) - return false; - - for (int i = 0; i < textToRead.length(); i++) { - if (textToRead.charAt(i) != text.charAt(offset + i)) - return false; - - } - offset += textToRead.length(); - return true; - } - } - - public static class Writer { - Stack stateStack = new Stack<>(); - java.io.Writer writer; - - public Writer() { - writer = new StringWriter(); - } - - public Writer(OutputStream stream) throws UnsupportedEncodingException { - writer = new BufferedWriter(new OutputStreamWriter(stream, "UTF-8")); - } - - public void writeObject(InnerWriter inner) throws Exception { - writeObjectStart(); - inner.write(this); - writeObjectEnd(); - } - - public void writeObjectStart() throws Exception { - startNewObject(true); - stateStack.push(new StateElement(State.Object, 0)); - writer.write("{"); - } - - public void writeObjectEnd() throws Exception { - Assert(getState() == State.Object); - writer.write("}"); - stateStack.pop(); - } - - public void writeProperty(String name, InnerWriter inner) throws Exception { - writePropertyString(name, inner); - } - - public void writeProperty(int id, InnerWriter inner) throws Exception { - writePropertyInteger(id, inner); - } - - public void writeProperty(String name, String content) throws Exception { - writePropertyStart(name); - write(content); - writePropertyEnd(); - } - - public void writeProperty(String name, int content) throws Exception { - writePropertyStart(name); - write(content); - writePropertyEnd(); - } - - public void writeProperty(String name, boolean content) throws Exception { - writePropertyStart(name); - write(content); - writePropertyEnd(); - } - - public void writePropertyStart(String name) throws Exception { - Assert(getState() == State.Object); - - if (getChildCount() > 0) - writer.write(","); - - writer.write("\""); - writer.write(name); - writer.write("\":"); - - incrementChildCount(); - - stateStack.push(new StateElement(State.Property, 0)); - } - - public void writePropertyStart(int id) throws Exception { - writePropertyStart(Integer.toString(id)); - } - - public void writePropertyEnd() throws Exception { - Assert(getState() == State.Property); - Assert(getChildCount() == 1); - stateStack.pop(); - } - - public void writePropertyNameStart() throws Exception { - Assert(getState() == State.Object); - - if (getChildCount() > 0) - writer.write(","); - - writer.write("\""); - - incrementChildCount(); - - stateStack.push(new StateElement(State.Property, 0)); - stateStack.push(new StateElement(State.PropertyName, 0)); - } - - public void writePropertyNameEnd() throws Exception { - Assert(getState() == State.PropertyName); - - writer.write("\":"); - - // Pop PropertyName, leaving Property state - stateStack.pop(); - } - - public void writePropertyNameInner(String str) throws Exception { - Assert(getState() == State.PropertyName); - writer.write(str); - } - - // allow name to be String or int - void writePropertyString(String name, InnerWriter inner) throws Exception { - writePropertyStart(name); - - inner.write(this); - - writePropertyEnd(); - } - - void writePropertyInteger(Integer name, InnerWriter inner) throws Exception { - writePropertyStart(name); - - inner.write(this); - - writePropertyEnd(); - } - - public void writeArrayStart() throws Exception { - startNewObject(true); - stateStack.push(new StateElement(State.Array, 0)); - writer.write("["); - } - - public void writeArrayEnd() throws Exception { - Assert(getState() == State.Array); - writer.write("]"); - stateStack.pop(); - } - - public void write(int i) throws Exception { - startNewObject(false); - writer.write(Integer.toString(i)); - } - - public void write(float f) throws Exception { - startNewObject(false); - - // TODO: Find an heap-allocation-free way to do this please! - // writer.write(formatStr, obj (the float)) requires boxing - // Following implementation seems to work ok but requires creating temporary - // garbage String. - String floatStr = Float.toString(f); - - if (floatStr == "Infinity") { - writer.write("3.4E+38"); // JSON doesn't support, do our best alternative - } else if (floatStr == "-Infinity") { - writer.write("-3.4E+38"); // JSON doesn't support, do our best alternative - } else if (floatStr == "NaN") { - writer.write("0.0"); // JSON doesn't support, not much we can do - } else { - writer.write(floatStr); - if (!floatStr.contains(".") && !floatStr.contains("E")) - writer.write(".0"); // ensure it gets read back in as a floating point value - } - } - - public void write(String str) throws Exception { - write(str, true); - } - - public void write(String str, boolean escape) throws Exception { - startNewObject(false); - - writer.write("\""); - if (escape) - writeEscapedString(str); - else - writer.write(str); - writer.write("\""); - } - - public void write(boolean b) throws Exception { - startNewObject(false); - writer.write(b ? "true" : "false"); - } - - public void writeNull() throws Exception { - startNewObject(false); - writer.write("null"); - } - - public void writeStringStart() throws Exception { - startNewObject(false); - stateStack.push(new StateElement(State.String, 0)); - writer.write("\""); - } - - public void writeStringEnd() throws Exception { - Assert(getState() == State.String); - writer.write("\""); - stateStack.pop(); - } - - public void writeStringInner(String str) throws Exception { - writeStringInner(str, true); - } - - public void writeStringInner(String str, boolean escape) throws Exception { - Assert(getState() == State.String); - if (escape) - writeEscapedString(str); - else - writer.write(str); - } - - void writeEscapedString(String str) throws IOException { - for (char c : str.toCharArray()) { - if (c < ' ') { - // Don't write any control characters except \n and \t - switch (c) { - case '\n': - writer.write("\\n"); - break; - case '\t': - writer.write("\\t"); - break; - } - } else { - switch (c) { - case '\\': - case '"': - writer.write("\\"); - writer.write(c); - break; - default: - writer.write(c); - break; - } - } - } - } - - void startNewObject(boolean container) throws Exception { - - if (container) - Assert(getState() == State.None || getState() == State.Property || getState() == State.Array); - else - Assert(getState() == State.Property || getState() == State.Array); - - if (getState() == State.Array && getChildCount() > 0) - writer.write(","); - - if (getState() == State.Property) - Assert(getChildCount() == 0); - - if (getState() == State.Array || getState() == State.Property) - incrementChildCount(); - } - - State getState() { - if (stateStack.size() > 0) - return stateStack.peek().type; - else - return State.None; - } - - int getChildCount() { - - if (stateStack.size() > 0) - return stateStack.peek().childCount; - else - return 0; - } - - void incrementChildCount() throws Exception { - Assert(stateStack.size() > 0); - StateElement currEl = stateStack.pop(); - currEl.childCount++; - stateStack.push(currEl); - } - - // Shouldn't hit this Assert outside of initial JSON development, - // so it's save to make it debug-only. - void Assert(boolean condition) throws Exception { - if (!condition) - throw new Exception("Assert failed while writing JSON"); - } - - @Override - public String toString() { - return writer.toString(); - } - - enum State { - None, Object, Array, Property, PropertyName, String - }; - - // Struct in C# - class StateElement { - public State type; - public int childCount; - - public StateElement(State type, int childCount) { - this.type = type; - this.childCount = childCount; - } - } - - } - - interface InnerWriter { - void write(Writer w) throws Exception; - } - -} +package com.bladecoder.ink.runtime; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Stack; + +/** + * Simple custom JSON serialisation implementation that takes JSON-able + * System.Collections that are produced by the ink engine and converts to and + * from JSON text. + */ +class SimpleJson { + + public static HashMap textToDictionary(String text) throws Exception { + return new Reader(text).toHashMap(); + } + + public static List textToArray(String text) throws Exception { + return new Reader(text).toArray(); + } + + static class Reader { + private int offset; + + private Object rootObject; + + private String text; + + public Reader(String text) throws Exception { + this.text = text; + offset = 0; + skipWhitespace(); + rootObject = readObject(); + } + + void expect(boolean condition, String message) throws Exception { + if (!condition) { + if (message == null) { + message = "Unexpected token"; + } else { + message = "Expected " + message; + } + message += " at offset " + offset; + throw new Exception(message); + } + } + + void expect(String expectedStr) throws Exception { + if (!tryRead(expectedStr)) expect(false, expectedStr); + } + + boolean isNumberChar(char c) throws Exception { + return c >= '0' && c <= '9' || c == '.' || c == '-' || c == '+' || c == 'E' || c == 'e'; + } + + boolean IsFirstNumberChar(char c) { + return c >= '0' && c <= '9' || c == '-' || c == '+'; + } + + List readArray() throws Exception { + List list = new ArrayList<>(); + expect("["); + skipWhitespace(); + // Empty list? + if (tryRead("]")) return list; + + do { + skipWhitespace(); + // Value + Object val = readObject(); + // Add to array + list.add(val); + skipWhitespace(); + } while (tryRead(",")); + expect("]"); + return list; + } + + HashMap readHashMap() throws Exception { + HashMap dict = new HashMap<>(); + expect("{"); + skipWhitespace(); + // Empty HashMap? + if (tryRead("}")) return dict; + + do { + skipWhitespace(); + // Key + String key = readString(); + expect(key != null, "dictionary key"); + skipWhitespace(); + // : + expect(":"); + skipWhitespace(); + // Value + Object val = readObject(); + expect(val != null, "dictionary value"); + // Add to HashMap + dict.put(key, val); + skipWhitespace(); + } while (tryRead(",")); + expect("}"); + return dict; + } + + Object readNumber() throws Exception { + int startOffset = offset; + boolean isFloat = false; + for (; offset < text.length(); offset++) { + char c = text.charAt(offset); + if (c == '.' || c == 'e' || c == 'E') isFloat = true; + + if (isNumberChar(c)) continue; + else break; + } + + String numStr = text.substring(startOffset, offset); + if (isFloat) { + try { + float f = Float.parseFloat(numStr); + return f; + } catch (NumberFormatException e) { + + } + } else { + try { + int i = Integer.parseInt(numStr); + return i; + } catch (NumberFormatException e) { + + } + } + + throw new Exception("Failed to parse number value: " + numStr); + } + + Object readObject() throws Exception { + char currentChar = text.charAt(offset); + + if (currentChar == '{') return readHashMap(); + else if (currentChar == '[') return readArray(); + else if (currentChar == '"') return readString(); + else if (IsFirstNumberChar(currentChar)) return readNumber(); + else if (tryRead("true")) return true; + else if (tryRead("false")) return false; + else if (tryRead("null")) return null; + + throw new Exception("Unhandled RTObject type in JSON: " + text.substring(offset, offset + 30)); + } + + String readString() throws Exception { + expect("\""); + StringBuilder sb = new StringBuilder(); + + for (; offset < text.length(); offset++) { + char c = text.charAt(offset); + + if (c == '\\') { + // Escaped character + offset++; + if (offset >= text.length()) { + throw new Exception("Unexpected EOF while reading string"); + } + c = text.charAt(offset); + switch (c) { + case '"': + case '\\': + case '/': // Yes, JSON allows this to be escaped + sb.append(c); + break; + case 'n': + sb.append('\n'); + break; + case 't': + sb.append('\t'); + break; + case 'r': + case 'b': + case 'f': + // Ignore other control characters + break; + case 'u': + // 4-digit Unicode + if (offset + 4 >= text.length()) { + throw new Exception("Unexpected EOF while reading string"); + } + + // c# expr: _text.SubString(_offset + 1, 4); + String digits = text.substring(offset + 1, offset + 6); + + int uchar; + + try { + uchar = Integer.parseInt(digits, 16); + sb.append((char) uchar); + offset += 4; + } catch (NumberFormatException e) { + throw new Exception("Invalid Unicode escape character at offset " + (offset - 1)); + } + break; + + default: + // The escaped character is invalid per json spec + throw new Exception("Invalid Unicode escape character at offset " + (offset - 1)); + } + } else if (c == '"') { + break; + } else { + sb.append(c); + } + } + expect("\""); + + return sb.toString(); + } + + void skipWhitespace() throws Exception { + while (offset < text.length()) { + char c = text.charAt(offset); + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') offset++; + else break; + } + } + + @SuppressWarnings("unchecked") + public HashMap toHashMap() throws Exception { + return (HashMap) rootObject; + } + + @SuppressWarnings("unchecked") + public List toArray() { + return (List) rootObject; + } + + boolean tryRead(String textToRead) throws Exception { + if (offset + textToRead.length() > text.length()) return false; + + for (int i = 0; i < textToRead.length(); i++) { + if (textToRead.charAt(i) != text.charAt(offset + i)) return false; + } + offset += textToRead.length(); + return true; + } + } + + public static class Writer { + Stack stateStack = new Stack<>(); + java.io.Writer writer; + + public Writer() { + writer = new StringWriter(); + } + + public Writer(OutputStream stream) throws UnsupportedEncodingException { + writer = new BufferedWriter(new OutputStreamWriter(stream, "UTF-8")); + } + + public void writeObject(InnerWriter inner) throws Exception { + writeObjectStart(); + inner.write(this); + writeObjectEnd(); + } + + public void clear() { + StringWriter stringWriter = writer instanceof StringWriter ? (StringWriter) writer : null; + if (stringWriter == null) { + throw new UnsupportedOperationException( + "Writer.Clear() is only supported for the StringWriter variant, not for streams"); + } + + stringWriter.getBuffer().setLength(0); + } + + public void writeObjectStart() throws Exception { + startNewObject(true); + stateStack.push(new StateElement(State.Object, 0)); + writer.write("{"); + } + + public void writeObjectEnd() throws Exception { + Assert(getState() == State.Object); + writer.write("}"); + stateStack.pop(); + if (getState() == State.None) writer.flush(); + } + + public void writeProperty(String name, InnerWriter inner) throws Exception { + writePropertyString(name, inner); + } + + public void writeProperty(int id, InnerWriter inner) throws Exception { + writePropertyInteger(id, inner); + } + + public void writeProperty(String name, String content) throws Exception { + writePropertyStart(name); + write(content); + writePropertyEnd(); + } + + public void writeProperty(String name, int content) throws Exception { + writePropertyStart(name); + write(content); + writePropertyEnd(); + } + + public void writeProperty(String name, boolean content) throws Exception { + writePropertyStart(name); + write(content); + writePropertyEnd(); + } + + public void writePropertyStart(String name) throws Exception { + Assert(getState() == State.Object); + + if (getChildCount() > 0) writer.write(","); + + writer.write("\""); + writer.write(name); + writer.write("\":"); + + incrementChildCount(); + + stateStack.push(new StateElement(State.Property, 0)); + } + + public void writePropertyStart(int id) throws Exception { + writePropertyStart(Integer.toString(id)); + } + + public void writePropertyEnd() throws Exception { + Assert(getState() == State.Property); + Assert(getChildCount() == 1); + stateStack.pop(); + } + + public void writePropertyNameStart() throws Exception { + Assert(getState() == State.Object); + + if (getChildCount() > 0) writer.write(","); + + writer.write("\""); + + incrementChildCount(); + + stateStack.push(new StateElement(State.Property, 0)); + stateStack.push(new StateElement(State.PropertyName, 0)); + } + + public void writePropertyNameEnd() throws Exception { + Assert(getState() == State.PropertyName); + + writer.write("\":"); + + // Pop PropertyName, leaving Property state + stateStack.pop(); + } + + public void writePropertyNameInner(String str) throws Exception { + Assert(getState() == State.PropertyName); + writer.write(str); + } + + // allow name to be String or int + void writePropertyString(String name, InnerWriter inner) throws Exception { + writePropertyStart(name); + + inner.write(this); + + writePropertyEnd(); + } + + void writePropertyInteger(Integer name, InnerWriter inner) throws Exception { + writePropertyStart(name); + + inner.write(this); + + writePropertyEnd(); + } + + public void writeArrayStart() throws Exception { + startNewObject(true); + stateStack.push(new StateElement(State.Array, 0)); + writer.write("["); + } + + public void writeArrayEnd() throws Exception { + Assert(getState() == State.Array); + writer.write("]"); + stateStack.pop(); + } + + public void write(int i) throws Exception { + startNewObject(false); + writer.write(Integer.toString(i)); + } + + public void write(float f) throws Exception { + startNewObject(false); + + // TODO: Find an heap-allocation-free way to do this please! + // writer.write(formatStr, obj (the float)) requires boxing + // Following implementation seems to work ok but requires creating temporary + // garbage String. + String floatStr = Float.toString(f); + + if (floatStr == "Infinity") { + writer.write("3.4E+38"); // JSON doesn't support, do our best alternative + } else if (floatStr == "-Infinity") { + writer.write("-3.4E+38"); // JSON doesn't support, do our best alternative + } else if (floatStr == "NaN") { + writer.write("0.0"); // JSON doesn't support, not much we can do + } else { + writer.write(floatStr); + if (!floatStr.contains(".") && !floatStr.contains("E")) + writer.write(".0"); // ensure it gets read back in as a floating point value + } + } + + public void write(String str) throws Exception { + write(str, true); + } + + public void write(String str, boolean escape) throws Exception { + startNewObject(false); + + writer.write("\""); + if (escape) writeEscapedString(str); + else writer.write(str); + writer.write("\""); + } + + public void write(boolean b) throws Exception { + startNewObject(false); + writer.write(b ? "true" : "false"); + } + + public void writeNull() throws Exception { + startNewObject(false); + writer.write("null"); + } + + public void writeStringStart() throws Exception { + startNewObject(false); + stateStack.push(new StateElement(State.String, 0)); + writer.write("\""); + } + + public void writeStringEnd() throws Exception { + Assert(getState() == State.String); + writer.write("\""); + stateStack.pop(); + } + + public void writeStringInner(String str) throws Exception { + writeStringInner(str, true); + } + + public void writeStringInner(String str, boolean escape) throws Exception { + Assert(getState() == State.String); + if (escape) writeEscapedString(str); + else writer.write(str); + } + + void writeEscapedString(String str) throws IOException { + for (char c : str.toCharArray()) { + if (c < ' ') { + // Don't write any control characters except \n and \t + switch (c) { + case '\n': + writer.write("\\n"); + break; + case '\t': + writer.write("\\t"); + break; + } + } else { + switch (c) { + case '\\': + case '"': + writer.write("\\"); + writer.write(c); + break; + default: + writer.write(c); + break; + } + } + } + } + + void startNewObject(boolean container) throws Exception { + + if (container) + Assert(getState() == State.None || getState() == State.Property || getState() == State.Array); + else Assert(getState() == State.Property || getState() == State.Array); + + if (getState() == State.Array && getChildCount() > 0) writer.write(","); + + if (getState() == State.Property) Assert(getChildCount() == 0); + + if (getState() == State.Array || getState() == State.Property) incrementChildCount(); + } + + State getState() { + if (stateStack.size() > 0) return stateStack.peek().type; + else return State.None; + } + + int getChildCount() { + + if (stateStack.size() > 0) return stateStack.peek().childCount; + else return 0; + } + + void incrementChildCount() throws Exception { + Assert(stateStack.size() > 0); + StateElement currEl = stateStack.pop(); + currEl.childCount++; + stateStack.push(currEl); + } + + // Shouldn't hit this Assert outside of initial JSON development, + // so it's save to make it debug-only. + void Assert(boolean condition) throws Exception { + if (!condition) throw new Exception("Assert failed while writing JSON"); + } + + @Override + public String toString() { + return writer.toString(); + } + + enum State { + None, + Object, + Array, + Property, + PropertyName, + String + }; + + // Struct in C# + class StateElement { + public State type; + public int childCount; + + public StateElement(State type, int childCount) { + this.type = type; + this.childCount = childCount; + } + } + } + + interface InnerWriter { + void write(Writer w) throws Exception; + } +} diff --git a/src/main/java/com/bladecoder/ink/runtime/StatePatch.java b/src/main/java/com/bladecoder/ink/runtime/StatePatch.java index d8d0f93..7f053db 100644 --- a/src/main/java/com/bladecoder/ink/runtime/StatePatch.java +++ b/src/main/java/com/bladecoder/ink/runtime/StatePatch.java @@ -4,66 +4,66 @@ import java.util.HashSet; class StatePatch { - private HashMap globals; - private HashSet changedVariables = new HashSet<>(); - private HashMap visitCounts = new HashMap<>(); - private HashMap turnIndices = new HashMap<>(); + private HashMap globals; + private HashSet changedVariables = new HashSet<>(); + private HashMap visitCounts = new HashMap<>(); + private HashMap turnIndices = new HashMap<>(); - public StatePatch(StatePatch toCopy) { - if (toCopy != null) { - globals = new HashMap<>(toCopy.globals); - changedVariables = new HashSet<>(toCopy.changedVariables); - visitCounts = new HashMap<>(toCopy.visitCounts); - turnIndices = new HashMap<>(toCopy.turnIndices); - } else { - globals = new HashMap<>(); - changedVariables = new HashSet<>(); - visitCounts = new HashMap<>(); - turnIndices = new HashMap<>(); - } - } + public StatePatch(StatePatch toCopy) { + if (toCopy != null) { + globals = new HashMap<>(toCopy.globals); + changedVariables = new HashSet<>(toCopy.changedVariables); + visitCounts = new HashMap<>(toCopy.visitCounts); + turnIndices = new HashMap<>(toCopy.turnIndices); + } else { + globals = new HashMap<>(); + changedVariables = new HashSet<>(); + visitCounts = new HashMap<>(); + turnIndices = new HashMap<>(); + } + } - public RTObject getGlobal(String name) { - return globals.get(name); - } + public RTObject getGlobal(String name) { + return globals.get(name); + } - public void setGlobal(String name, RTObject value) { - globals.put(name, value); - } + public void setGlobal(String name, RTObject value) { + globals.put(name, value); + } - public void addChangedVariable(String name) { - changedVariables.add(name); - } + public void addChangedVariable(String name) { + changedVariables.add(name); + } - public Integer getVisitCount(Container container) { - return visitCounts.get(container); - } + public Integer getVisitCount(Container container) { + return visitCounts.get(container); + } - public void setVisitCount(Container container, int count) { - visitCounts.put(container, count); - } + public void setVisitCount(Container container, int count) { + visitCounts.put(container, count); + } - public void setTurnIndex(Container container, int index) { - turnIndices.put(container, index); - } + public void setTurnIndex(Container container, int index) { + turnIndices.put(container, index); + } - public Integer getTurnIndex(Container container) { - return turnIndices.get(container); - } + public Integer getTurnIndex(Container container) { + return turnIndices.get(container); + } - public HashMap getGlobals() { - return globals; - } + public HashMap getGlobals() { + return globals; + } - public HashSet getChangedVariables() { - return changedVariables; - } + public HashSet getChangedVariables() { + return changedVariables; + } - public HashMap getVisitCounts() { - return visitCounts; - } + public HashMap getVisitCounts() { + return visitCounts; + } - public HashMap getTurnIndices() { - return turnIndices; - } + public HashMap getTurnIndices() { + return turnIndices; + } } diff --git a/src/main/java/com/bladecoder/ink/runtime/Stopwatch.java b/src/main/java/com/bladecoder/ink/runtime/Stopwatch.java index 8f0dacd..301f18e 100644 --- a/src/main/java/com/bladecoder/ink/runtime/Stopwatch.java +++ b/src/main/java/com/bladecoder/ink/runtime/Stopwatch.java @@ -4,145 +4,144 @@ import java.util.Calendar; public class Stopwatch { - // constants - private static final long nsPerTick = 100; - private static final long nsPerMs = 1000000; - private static final long nsPerSs = 1000000000; - private static final long nsPerMm = 60000000000L; - private static final long nsPerHh = 3600000000000L; + // constants + private static final long nsPerTick = 100; + private static final long nsPerMs = 1000000; + private static final long nsPerSs = 1000000000; + private static final long nsPerMm = 60000000000L; + private static final long nsPerHh = 3600000000000L; - private long startTime = 0; - private long stopTime = 0; - private boolean running = false; + private long startTime = 0; + private long stopTime = 0; + private boolean running = false; - /** - * Starts measuring elapsed time for an interval. - */ - public void start() { - this.startTime = System.nanoTime(); - this.running = true; - } + /** + * Starts measuring elapsed time for an interval. + */ + public void start() { + this.startTime = System.nanoTime(); + this.running = true; + } - /** - * Stops measuring elapsed time for an interval. - */ - public void stop() { - this.stopTime = System.nanoTime(); - this.running = false; - } + /** + * Stops measuring elapsed time for an interval. + */ + public void stop() { + this.stopTime = System.nanoTime(); + this.running = false; + } - /** - * Stops time interval measurement and resets the elapsed time to zero. - */ - public void reset() { - this.startTime = 0; - this.stopTime = 0; - this.running = false; - } + /** + * Stops time interval measurement and resets the elapsed time to zero. + */ + public void reset() { + this.startTime = 0; + this.stopTime = 0; + this.running = false; + } - /** - * Gets the total elapsed time measured by the current instance, in nanoseconds. - * 1 Tick = 100 nanoseconds - */ - public long getElapsedTicks() { - long elapsed; - if (running) { - elapsed = (System.nanoTime() - startTime); - } else { - elapsed = (stopTime - startTime); - } - return elapsed / nsPerTick; - } + /** + * Gets the total elapsed time measured by the current instance, in nanoseconds. + * 1 Tick = 100 nanoseconds + */ + public long getElapsedTicks() { + long elapsed; + if (running) { + elapsed = (System.nanoTime() - startTime); + } else { + elapsed = (stopTime - startTime); + } + return elapsed / nsPerTick; + } - /** - * Gets the total elapsed time measured by the current instance, in - * milliseconds. 10000 Ticks = 1 millisecond (1000000 nanoseconds) - */ - public long getElapsedMilliseconds() { - long elapsed; - if (running) { - elapsed = (System.nanoTime() - startTime); - } else { - elapsed = (stopTime - startTime); - } - return elapsed / nsPerMs; - } + /** + * Gets the total elapsed time measured by the current instance, in + * milliseconds. 10000 Ticks = 1 millisecond (1000000 nanoseconds) + */ + public long getElapsedMilliseconds() { + long elapsed; + if (running) { + elapsed = (System.nanoTime() - startTime); + } else { + elapsed = (stopTime - startTime); + } + return elapsed / nsPerMs; + } - /** - * Gets the total elapsed time measured by the current instance, in seconds. - * 10000000 Ticks = 1 second (1000 milliseconds) - */ - public long getElapsedSeconds() { - long elapsed; - if (running) { - elapsed = (System.nanoTime() - startTime); - } else { - elapsed = (stopTime - startTime); - } - return elapsed / nsPerSs; - } + /** + * Gets the total elapsed time measured by the current instance, in seconds. + * 10000000 Ticks = 1 second (1000 milliseconds) + */ + public long getElapsedSeconds() { + long elapsed; + if (running) { + elapsed = (System.nanoTime() - startTime); + } else { + elapsed = (stopTime - startTime); + } + return elapsed / nsPerSs; + } - /** - * Gets the total elapsed time measured by the current instance, in minutes. - * 600000000 Ticks = 1 minute (60 seconds) - */ - public long getElapsedMinutes() { - long elapsed; - if (running) { - elapsed = (System.nanoTime() - startTime); - } else { - elapsed = (stopTime - startTime); - } - return elapsed / nsPerMm; - } + /** + * Gets the total elapsed time measured by the current instance, in minutes. + * 600000000 Ticks = 1 minute (60 seconds) + */ + public long getElapsedMinutes() { + long elapsed; + if (running) { + elapsed = (System.nanoTime() - startTime); + } else { + elapsed = (stopTime - startTime); + } + return elapsed / nsPerMm; + } - /** - * Gets the total elapsed time measured by the current instance, in hours. - * 36000000000 Ticks = 1 hour (60 minutes) - */ - public long getElapsedHours() { - long elapsed; - if (running) { - elapsed = (System.nanoTime() - startTime); - } else { - elapsed = (stopTime - startTime); - } - return elapsed / nsPerHh; - } + /** + * Gets the total elapsed time measured by the current instance, in hours. + * 36000000000 Ticks = 1 hour (60 minutes) + */ + public long getElapsedHours() { + long elapsed; + if (running) { + elapsed = (System.nanoTime() - startTime); + } else { + elapsed = (stopTime - startTime); + } + return elapsed / nsPerHh; + } - /** - * Gets the total elapsed time with format 00:00:00.0000000 = 00:mm:ss.SSS + - * 9999 Ticks - */ - public String getElapsed() { - String timeFormatted = ""; - timeFormatted = this.formatTime(this.getElapsedTicks()); - return timeFormatted; - } + /** + * Gets the total elapsed time with format 00:00:00.0000000 = 00:mm:ss.SSS + + * 9999 Ticks + */ + public String getElapsed() { + String timeFormatted = ""; + timeFormatted = this.formatTime(this.getElapsedTicks()); + return timeFormatted; + } - /** - * Gets the total elapsed time with format 00:00:00.0000000 = 00:mm:ss.SSS + - * #### Ticks - * - * @param elapsedTicks - * elapsed ticks between start and stop nano time - */ - private String formatTime(final long elapsedTicks) { - String formattedTime = ""; - // should be hh:mm:ss.SSS, but 00 starts with 01 - SimpleDateFormat formatter = new SimpleDateFormat("00:mm:ss.SSS"); - Calendar calendar = Calendar.getInstance(); - - if (elapsedTicks <= 9999) { - calendar.setTimeInMillis(0); - formattedTime = formatter.format(calendar.getTime()) + String.valueOf(String.format("%04d", elapsedTicks)); - } else { - calendar.setTimeInMillis(elapsedTicks * nsPerTick / nsPerMs); - String formattedTicks = String.format("%07d", elapsedTicks); - formattedTicks = formattedTicks.substring(formattedTicks.length() - 4); - formattedTime = formatter.format(calendar.getTime()) + formattedTicks; - } - return formattedTime; - } + /** + * Gets the total elapsed time with format 00:00:00.0000000 = 00:mm:ss.SSS + + * #### Ticks + * + * @param elapsedTicks + * elapsed ticks between start and stop nano time + */ + private String formatTime(final long elapsedTicks) { + String formattedTime = ""; + // should be hh:mm:ss.SSS, but 00 starts with 01 + SimpleDateFormat formatter = new SimpleDateFormat("00:mm:ss.SSS"); + Calendar calendar = Calendar.getInstance(); + if (elapsedTicks <= 9999) { + calendar.setTimeInMillis(0); + formattedTime = formatter.format(calendar.getTime()) + String.valueOf(String.format("%04d", elapsedTicks)); + } else { + calendar.setTimeInMillis(elapsedTicks * nsPerTick / nsPerMs); + String formattedTicks = String.format("%07d", elapsedTicks); + formattedTicks = formattedTicks.substring(formattedTicks.length() - 4); + formattedTime = formatter.format(calendar.getTime()) + formattedTicks; + } + return formattedTime; + } } diff --git a/src/main/java/com/bladecoder/ink/runtime/Story.java b/src/main/java/com/bladecoder/ink/runtime/Story.java index 9506e69..a15c607 100644 --- a/src/main/java/com/bladecoder/ink/runtime/Story.java +++ b/src/main/java/com/bladecoder/ink/runtime/Story.java @@ -1,5 +1,8 @@ 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; @@ -7,2629 +10,2919 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Map.Entry; import java.util.Random; import java.util.Stack; -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; - } - } - - // 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 = 19; - - /** - * 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; - - // 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. - */ - public void bindExternalFunction(String funcName, ExternalFunction func) throws Exception { - ifAsyncWeCant("bind an external function"); - Assert(!externals.containsKey(funcName), "Function '" + funcName + "' has already been bound."); - externals.put(funcName, func); - } - - @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 (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 { - Container fallbackFunctionContainer = null; - - ExternalFunction func = externals.get(funcName); - - // Try to use fallback function? - if (func == 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 = func.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; - } - - /** - * 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 StoryException, Exception { - continueInternal(0); - } - - void continueInternal(float millisecsLimitAsync) throws StoryException, 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 StoryException("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; - 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); - if (recursiveContinueCount == 1) - state.getVariablesState().setbatchObservingVariableChanges(false); - asyncContinueActive = false; - } - - recursiveContinueCount--; - - if (profiler != null) - profiler.postContinue(); - } - - 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) { - 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. - */ - 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 - * @throws StoryException - */ - public void observeVariable(String variableName, VariableObserver observer) throws StoryException, Exception { - ifAsyncWeCant("observe a new variable"); - - if (variableObservers == null) - variableObservers = new HashMap<>(); - - if (!state.getVariablesState().globalVariableExistsWithName(variableName)) - throw new StoryException( - "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); - } - } else { - // Remove observer for all variables - for (List obs : variableObservers.values()) { - obs.remove(observer); - } - } - } - - 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; +public class Story 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; + } + } + + static 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 final HashMap externals; + + private boolean hasValidatedExternals; + + private StoryState state; + + private Container temporaryEvaluationContainer; + + private HashMap> variableObservers; + + private final 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); + + if (funcDef != null && funcDef.lookaheadSafe && state.inStringEvaluation()) { + // 16th Jan 2023: Example ink that was failing: + // + // A line above + // ~ temp text = "{theFunc()}" + // {text} + // + // === function theFunc() + // { external(): + // Boom + // } + // + // EXTERNAL external() + // + // What was happening: The external() call would exit out early due to + // _stateSnapshotAtLastNewline having a value, leaving the evaluation stack + // without a return value on it. When the if-statement tried to pop a value, + // the evaluation stack would be empty, and there would be an exception. + // + // The snapshot rewinding code is only designed to work when outside of + // string generation code (there's a check for that in the snapshot rewinding code), + // hence these things are incompatible, you can't have unsafe functions that + // cause snapshot rewinding in the middle of string generation. + // + error("External function " + funcName + + " could not be called because 1) it wasn't marked as lookaheadSafe when BindExternalFunction was called and 2) the story is in the middle of string generation, either because choice text is being generated, or because you have ink like \"hello {func()}\". You can work around this by generating the result of your function into a temporary variable before the string or choice gets generated: ~ temp x = " + + funcName + "()"); + return; + } + + // 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().startVariableObservation(); + } else if (asyncContinueActive && !isAsyncTimeLimited) { + asyncContinueActive = false; + } + + // 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(); + + HashMap changedVariablesToObserve = null; + + // 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) + changedVariablesToObserve = state.getVariablesState().completeVariableObservation(); + 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()); + } + } + + // Send out variable observation events at the last second, since it might trigger new ink to be run + if (changedVariablesToObserve != null && changedVariablesToObserve.size() > 0) { + state.getVariablesState().notifyObservers(changedVariablesToObserve); + } + } + + 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 + List keysToRemove = new ArrayList<>(); + for (Map.Entry> obs : variableObservers.entrySet()) { + obs.getValue().remove(observer); + if (obs.getValue().size() == 0) { + keysToRemove.add(obs.getKey()); + } + } + + for (String keyToRemove : keysToRemove) { + variableObservers.remove(keyToRemove); + } + } + } + + 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; + + 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 (int i = contentStackForTag.size() - 1; i >= 0; --i) { + StringValue strVal = contentStackForTag.get(i); + 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 (int i = contentToRetain.size() - 1; i >= 0; --i) { + RTObject c = contentToRetain.get(i); + + state.pushToOutputStream(c); + } + + // Build string out of the content we collected + StringBuilder sb = new StringBuilder(); + + for (int i = contentStackForString.size() - 1; i >= 0; --i) { + RTObject c = contentStackForString.get(i); + + sb.append(c.toString()); + } + + // Return to expression evaluation (from content mode) + state.setInExpressionEvaluation(true); + state.pushEvaluationStack(new StringValue(sb.toString())); + break; + } + case ChoiceCount: + int 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()); + } - IntValue minInt = null; + state.pushEvaluationStack(new IntValue(eitherCount)); + break; - o = state.popEvaluationStack(); + case Random: { + IntValue maxInt = null; - if (o instanceof IntValue) - minInt = (IntValue) o; + RTObject o = state.popEvaluationStack(); - if (minInt == null) - error("Invalid value for minimum parameter of RANDOM(min, max)"); + if (o instanceof IntValue) maxInt = (IntValue) o; - if (maxInt == null) - error("Invalid value for maximum parameter of RANDOM(min, max)"); + IntValue minInt = null; - // +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"); + o = state.popEvaluationStack(); - int resultSeed = state.getStorySeed() + state.getPreviousRandom(); - Random random = new Random(resultSeed); + if (o instanceof IntValue) minInt = (IntValue) o; - int nextRandom = random.nextInt(Integer.MAX_VALUE); - int chosenValue = (nextRandom % randomRange) + minInt.value; - state.pushEvaluationStack(new IntValue(chosenValue)); + if (minInt == null) error("Invalid value for minimum parameter of RANDOM(min, max)"); - // Next random number (rather than keeping the Random object - // around) - state.setPreviousRandom(state.getPreviousRandom() + 1); - break; - } + if (maxInt == null) error("Invalid value for maximum parameter of RANDOM(min, max)"); - case SeedRandom: { - IntValue seed = null; + // +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"); - RTObject o = state.popEvaluationStack(); + int resultSeed = state.getStorySeed() + state.getPreviousRandom(); + Random random = new Random(resultSeed); - if (o instanceof IntValue) - seed = (IntValue) o; + int nextRandom = random.nextInt(Integer.MAX_VALUE); + int chosenValue = (nextRandom % randomRange) + minInt.value; + state.pushEvaluationStack(new IntValue(chosenValue)); - if (seed == null) - error("Invalid value passed to SEED_RANDOM"); + // Next random number (rather than keeping the Random object + // around) + state.setPreviousRandom(state.getPreviousRandom() + 1); + break; + } - // Story seed affects both RANDOM and shuffle behaviour - state.setStorySeed(seed.value); - state.setPreviousRandom(0); + case SeedRandom: { + IntValue seed = null; - // 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; + RTObject o = state.popEvaluationStack(); - case SequenceShuffleIndex: - int shuffleIndex = nextSequenceShuffleIndex(); - state.pushEvaluationStack(new IntValue(shuffleIndex)); - break; + if (o instanceof IntValue) seed = (IntValue) o; - case StartThread: - // Handled in main step function - break; + if (seed == null) error("Invalid value passed to SEED_RANDOM"); - case Done: + // Story seed affects both RANDOM and shuffle behaviour + state.setStorySeed(seed.value); + state.setPreviousRandom(0); - // 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(); - } + // 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; - // In normal flow - allow safe exit without warning - else { - state.setDidSafeExit(true); + case SequenceShuffleIndex: + int shuffleIndex = nextSequenceShuffleIndex(); + state.pushEvaluationStack(new IntValue(shuffleIndex)); + break; - // Stop flow in current thread - state.setCurrentPointer(Pointer.Null); - } + case StartThread: + // Handled in main step function + break; - break; + case Done: - // Force flow to end completely - case End: - state.forceEnd(); - 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 ListFromInt: { - IntValue intVal = null; + // In normal flow - allow safe exit without warning + else { + state.setDidSafeExit(true); - RTObject o = state.popEvaluationStack(); + // Stop flow in current thread + state.setCurrentPointer(Pointer.Null); + } - if (o instanceof IntValue) - intVal = (IntValue) o; + break; - StringValue listNameVal = null; + // Force flow to end completely + case End: + state.forceEnd(); + break; - o = state.popEvaluationStack(); + case ListFromInt: { + IntValue intVal = null; - if (o instanceof StringValue) - listNameVal = (StringValue) o; + RTObject o = state.popEvaluationStack(); - if (intVal == null) { - throw new StoryException("Passed non-integer when creating a list element from a numerical value."); - } + if (o instanceof IntValue) intVal = (IntValue) o; - ListValue generatedListValue = null; + StringValue listNameVal = null; - ListDefinition foundListDef = listDefinitions.getListDefinition(listNameVal.value); + o = state.popEvaluationStack(); - if (foundListDef != null) { - InkListItem foundItem; + if (o instanceof StringValue) listNameVal = (StringValue) o; - foundItem = foundListDef.getItemWithValue(intVal.value); + if (intVal == null) { + throw new StoryException( + "Passed non-integer when creating a list element from a numerical value."); + } - if (foundItem != null) { - generatedListValue = new ListValue(foundItem, intVal.value); - } - } else { - throw new StoryException("Failed to find List called " + listNameVal.value); - } + ListValue generatedListValue = null; - if (generatedListValue == null) - generatedListValue = new ListValue(); + ListDefinition foundListDef = listDefinitions.getListDefinition(listNameVal.value); - state.pushEvaluationStack(generatedListValue); - break; - } + if (foundListDef != null) { + InkListItem foundItem; - case ListRange: { - RTObject p = state.popEvaluationStack(); - Value max = p instanceof Value ? (Value) p : null; + foundItem = foundListDef.getItemWithValue(intVal.value); - p = state.popEvaluationStack(); - Value min = p instanceof Value ? (Value) p : null; + if (foundItem != null) { + generatedListValue = new ListValue(foundItem, intVal.value); + } + } else { + throw new StoryException("Failed to find List called " + listNameVal.value); + } - p = state.popEvaluationStack(); - ListValue targetList = p instanceof ListValue ? (ListValue) p : null; + if (generatedListValue == null) generatedListValue = new ListValue(); - if (targetList == null || min == null || max == null) - throw new StoryException("Expected List, minimum and maximum for LIST_RANGE"); + state.pushEvaluationStack(generatedListValue); + break; + } - InkList result = targetList.value.listWithSubRange(min.getValueObject(), max.getValueObject()); + case ListRange: { + RTObject p = state.popEvaluationStack(); + Value max = p instanceof Value ? (Value) p : null; - state.pushEvaluationStack(new ListValue(result)); - break; - } + p = state.popEvaluationStack(); + Value min = p instanceof Value ? (Value) p : null; - case ListRandom: { + p = state.popEvaluationStack(); + ListValue targetList = p instanceof ListValue ? (ListValue) p : null; - RTObject o = state.popEvaluationStack(); - ListValue listVal = o instanceof ListValue ? (ListValue) o : null; + if (targetList == null || min == null || max == null) + throw new StoryException("Expected List, minimum and maximum for LIST_RANGE"); - if (listVal == null) - throw new StoryException("Expected list for LIST_RANDOM"); + InkList result = targetList.value.listWithSubRange(min.getValueObject(), max.getValueObject()); - InkList list = listVal.value; + state.pushEvaluationStack(new ListValue(result)); + break; + } - InkList newList = null; + case ListRandom: { + RTObject o = state.popEvaluationStack(); + ListValue listVal = o instanceof ListValue ? (ListValue) o : null; - // List was empty: return empty list - if (list.size() == 0) { - newList = new InkList(); - } + if (listVal == null) throw new StoryException("Expected list for LIST_RANDOM"); - // 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 list = listVal.value; - int nextRandom = random.nextInt(Integer.MAX_VALUE); - int listItemIndex = nextRandom % list.size(); + InkList newList = null; - // Iterate through to get the random element - Iterator> listEnumerator = list.entrySet().iterator(); + // List was empty: return empty list + if (list.size() == 0) { + newList = new InkList(); + } - Entry randomItem = null; + // 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); - for (int i = 0; i <= listItemIndex; i++) { - randomItem = listEnumerator.next(); - } + int nextRandom = random.nextInt(Integer.MAX_VALUE); + int listItemIndex = nextRandom % list.size(); - // Origin list is simply the origin of the one element - newList = new InkList(randomItem.getKey().getOriginName(), this); - newList.put(randomItem.getKey(), randomItem.getValue()); + // Iterate through to get the random element + Iterator> listEnumerator = + list.entrySet().iterator(); - state.setPreviousRandom(nextRandom); - } + Entry randomItem = null; - state.pushEvaluationStack(new ListValue(newList)); - break; - } + for (int i = 0; i <= listItemIndex; i++) { + randomItem = listEnumerator.next(); + } - default: - error("unhandled ControlCommand: " + evalCommand); - break; - } + // Origin list is simply the origin of the one element + newList = new InkList(randomItem.getKey().getOriginName(), this); + newList.put(randomItem.getKey(), randomItem.getValue()); - return true; - } + state.setPreviousRandom(nextRandom); + } - // Variable assignment - else if (contentObj instanceof VariableAssignment) + state.pushEvaluationStack(new ListValue(newList)); + break; + } - { - VariableAssignment varAss = (VariableAssignment) contentObj; - RTObject assignedVal = state.popEvaluationStack(); + default: + error("unhandled ControlCommand: " + evalCommand); + break; + } - // 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(); - } - - /** - * Reset the runtime error and warning list within the state. - */ - public 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()); - - 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; - - 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); - - // 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; - } + return true; + } + + // 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().isEmpty() && 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(); + + 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(false); + } + + 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(true); + 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/StoryException.java b/src/main/java/com/bladecoder/ink/runtime/StoryException.java index fb29de2..5053104 100644 --- a/src/main/java/com/bladecoder/ink/runtime/StoryException.java +++ b/src/main/java/com/bladecoder/ink/runtime/StoryException.java @@ -1,28 +1,26 @@ -package com.bladecoder.ink.runtime; - -/** - * Exception that represents an error when running a Story at runtime. An - * exception being thrown of this type is typically when there's a bug in your - * ink, rather than in the ink engine itself! - */ -@SuppressWarnings("serial") -public class StoryException extends Exception { - public boolean useEndLineNumber; - - /** - * Constructs a default instance of a StoryException without a message. - */ - public StoryException() throws Exception { - } - - /** - * Constructs an instance of a StoryException with a message. - * - * @param message - * The error message. - */ - public StoryException(String message) throws Exception { - super(message); - } - -} +package com.bladecoder.ink.runtime; + +/** + * Exception that represents an error when running a Story at runtime. An + * exception being thrown of this type is typically when there's a bug in your + * ink, rather than in the ink engine itself! + */ +@SuppressWarnings("serial") +public class StoryException extends Exception { + public boolean useEndLineNumber; + + /** + * Constructs a default instance of a StoryException without a message. + */ + public StoryException() throws Exception {} + + /** + * Constructs an instance of a StoryException with a message. + * + * @param message + * The error message. + */ + public StoryException(String message) throws Exception { + super(message); + } +} diff --git a/src/main/java/com/bladecoder/ink/runtime/StoryState.java b/src/main/java/com/bladecoder/ink/runtime/StoryState.java index 887db52..0dddaed 100644 --- a/src/main/java/com/bladecoder/ink/runtime/StoryState.java +++ b/src/main/java/com/bladecoder/ink/runtime/StoryState.java @@ -1,17 +1,17 @@ package com.bladecoder.ink.runtime; +import com.bladecoder.ink.runtime.CallStack.Element; +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.HashMap; import java.util.List; +import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; import java.util.Random; -import com.bladecoder.ink.runtime.CallStack.Element; -import com.bladecoder.ink.runtime.CallStack.Thread; -import com.bladecoder.ink.runtime.SimpleJson.InnerWriter; -import com.bladecoder.ink.runtime.SimpleJson.Writer; - /** * All story state information is included in the StoryState class, including * global variables, read counts, the pointer to the current point in the story, @@ -20,1251 +20,1347 @@ * functions ToJson and LoadJson. */ public class StoryState { - /** - * The current version of the state save file JSON-based format. - */ - public static final int kInkSaveStateVersion = 8; - public static final int kMinCompatibleLoadVersion = 8; - - // REMEMBER! REMEMBER! REMEMBER! - // When adding state, update the Copy method and serialisation - // REMEMBER! REMEMBER! REMEMBER! - private List outputStream; - private CallStack callStack; - private List currentChoices; - private List currentErrors; - private List currentWarnings; - private int currentTurnIndex; - private boolean didSafeExit; - private final Pointer divertedPointer = new Pointer(); - private List evaluationStack; - private Story story; - private int storySeed; - private int previousRandom; - private HashMap turnIndices; - private VariablesState variablesState; - private HashMap visitCounts; - private String currentText; - - private boolean outputStreamTextDirty = true; - private boolean outputStreamTagsDirty = true; - private List currentTags; - - private StatePatch patch; - - StoryState(Story story) { - this.story = story; - - outputStream = new ArrayList<>(); - outputStreamDirty(); - - evaluationStack = new ArrayList<>(); - - callStack = new CallStack(story); - variablesState = new VariablesState(callStack, story.getListDefinitions()); - - visitCounts = new HashMap<>(); - turnIndices = new HashMap<>(); - currentTurnIndex = -1; - - // Seed the shuffle random numbers - long timeSeed = System.currentTimeMillis(); - - storySeed = new Random(timeSeed).nextInt() % 100; - previousRandom = 0; - - currentChoices = new ArrayList<>(); - - goToStart(); - } - - int getCallStackDepth() { - return callStack.getDepth(); - } - - void addError(String message, boolean isWarning) { - if (!isWarning) { - if (currentErrors == null) - currentErrors = new ArrayList<>(); - - currentErrors.add(message); - } else { - if (currentWarnings == null) - currentWarnings = new ArrayList<>(); - - currentWarnings.add(message); - } - } - - // Warning: Any RTObject content referenced within the StoryState will - // be re-referenced rather than cloned. This is generally okay though since - // RTObjects are treated as immutable after they've been set up. - // (e.g. we don't edit a Runtime.StringValue after it's been created an added.) - // I wonder if there's a sensible way to enforce that..?? - StoryState copyAndStartPatching() { - StoryState copy = new StoryState(story); - - copy.patch = new StatePatch(patch); - copy.getOutputStream().addAll(outputStream); - copy.outputStreamDirty(); - copy.currentChoices.addAll(currentChoices); - - if (hasError()) { - copy.currentErrors = new ArrayList<>(); - copy.currentErrors.addAll(currentErrors); - } - - if (hasWarning()) { - copy.currentWarnings = new ArrayList<>(); - copy.currentWarnings.addAll(currentWarnings); - } - - copy.callStack = new CallStack(callStack); - - // ref copy - exactly the same variables state! - // we're expecting not to read it only while in patch mode - // (though the callstack will be modified) - copy.variablesState = variablesState; - copy.variablesState.setCallStack(copy.callStack); - copy.variablesState.setPatch(copy.patch); - - copy.evaluationStack.addAll(evaluationStack); - - if (!divertedPointer.isNull()) - copy.divertedPointer.assign(divertedPointer); - - copy.setPreviousPointer(getPreviousPointer()); - - // visit counts and turn indicies will be read only, not modified - // while in patch mode - copy.visitCounts = visitCounts; - copy.turnIndices = turnIndices; - - copy.currentTurnIndex = currentTurnIndex; - copy.storySeed = storySeed; - copy.previousRandom = previousRandom; - - copy.setDidSafeExit(didSafeExit); - - return copy; - } - - void popFromOutputStream(int count) { - outputStream.subList(outputStream.size() - count, outputStream.size()).clear(); - - outputStreamDirty(); - } - - String getCurrentText() { - if (outputStreamTextDirty) { - StringBuilder sb = new StringBuilder(); - - for (RTObject outputObj : outputStream) { - StringValue textContent = null; - if (outputObj instanceof StringValue) - textContent = (StringValue) outputObj; - - if (textContent != null) { - sb.append(textContent.value); - } - } - - currentText = cleanOutputWhitespace(sb.toString()); - - outputStreamTextDirty = false; - } - - return currentText; - } - - /** - * Cleans inline whitespace in the following way: - Removes all whitespace from - * the start and end of line (including just before a \n) - Turns all - * consecutive space and tab runs into single spaces (HTML style) - */ - String cleanOutputWhitespace(String str) { - StringBuilder sb = new StringBuilder(str.length()); - - int currentWhitespaceStart = -1; - int startOfLine = 0; - - for (int i = 0; i < str.length(); i++) { - char c = str.charAt(i); - - boolean isInlineWhitespace = c == ' ' || c == '\t'; - - if (isInlineWhitespace && currentWhitespaceStart == -1) - currentWhitespaceStart = i; - - if (!isInlineWhitespace) { - if (c != '\n' && currentWhitespaceStart > 0 && currentWhitespaceStart != startOfLine) { - sb.append(' '); - } - currentWhitespaceStart = -1; - } - - if (c == '\n') - startOfLine = i + 1; - - if (!isInlineWhitespace) - sb.append(c); - } - - return sb.toString(); - } - - /** - * Ends the current ink flow, unwrapping the callstack but without affecting any - * variables. Useful if the ink is (say) in the middle a nested tunnel, and you - * want it to reset so that you can divert elsewhere using ChoosePathString(). - * Otherwise, after finishing the content you diverted to, it would continue - * where it left off. Calling this is equivalent to calling -> END in ink. - */ - public void forceEnd() throws Exception { - - callStack.reset(); - - currentChoices.clear(); - - setCurrentPointer(Pointer.Null); - setPreviousPointer(Pointer.Null); - - setDidSafeExit(true); - } - - // Add the end of a function call, trim any whitespace from the end. - // We always trim the start and end of the text that a function produces. - // The start whitespace is discard as it is generated, and the end - // whitespace is trimmed in one go here when we pop the function. - void trimWhitespaceFromFunctionEnd() { - assert (callStack.getCurrentElement().type == PushPopType.Function); - - int functionStartPoint = callStack.getCurrentElement().functionStartInOuputStream; - - // If the start point has become -1, it means that some non-whitespace - // text has been pushed, so it's safe to go as far back as we're able. - if (functionStartPoint == -1) { - functionStartPoint = 0; - } - - // Trim whitespace from END of function call - for (int i = outputStream.size() - 1; i >= functionStartPoint; i--) { - RTObject obj = outputStream.get(i); - - if (!(obj instanceof StringValue)) - continue; - StringValue txt = (StringValue) obj; - - if (obj instanceof ControlCommand) - break; - - if (txt.isNewline() || txt.isInlineWhitespace()) { - outputStream.remove(i); - outputStreamDirty(); - } else { - break; - } - } - } - - void popCallstack() throws Exception { - popCallstack(null); - } - - void popCallstack(PushPopType popType) throws Exception { - // Add the end of a function call, trim any whitespace from the end. - if (callStack.getCurrentElement().type == PushPopType.Function) - trimWhitespaceFromFunctionEnd(); - - callStack.pop(popType); - } - - Pointer getCurrentPointer() { - return callStack.getCurrentElement().currentPointer; - } - - List getCurrentTags() { - if (outputStreamTagsDirty) { - currentTags = new ArrayList<>(); - - for (RTObject outputObj : outputStream) { - Tag tag = null; - if (outputObj instanceof Tag) - tag = (Tag) outputObj; - - if (tag != null) { - currentTags.add(tag.getText()); - } - } - outputStreamTagsDirty = false; - } - - return currentTags; - } - - boolean getInExpressionEvaluation() { - return callStack.getCurrentElement().inExpressionEvaluation; - } - - Pointer getPreviousPointer() { - return callStack.getcurrentThread().previousPointer; - } - - void goToStart() { - callStack.getCurrentElement().currentPointer.assign(Pointer.startOf(story.getMainContentContainer())); - } - - boolean hasError() { - return currentErrors != null && currentErrors.size() > 0; - } - - boolean inStringEvaluation() { - for (int i = outputStream.size() - 1; i >= 0; i--) { - ControlCommand cmd = outputStream.get(i) instanceof ControlCommand ? (ControlCommand) outputStream.get(i) - : null; - - if (cmd != null && cmd.getCommandType() == ControlCommand.CommandType.BeginString) { - return true; - } - } - - return false; - } - - /** - * Loads a previously saved state in JSON format. - * - * @param json The JSON String to load. - */ - public void loadJson(String json) throws Exception { - HashMap jObject = SimpleJson.textToDictionary(json); - loadJsonObj(jObject); - } - - List getCurrentChoices() { - if (canContinue()) - return new ArrayList<>(); - - return currentChoices; - } - - List getGeneratedChoices() { - return currentChoices; - } - - boolean canContinue() { - return !getCurrentPointer().isNull() && !hasError(); - } - - List getCurrentErrors() { - return currentErrors; - } - - List getCurrentWarnings() { - return currentWarnings; - } - - boolean hasWarning() { - return currentWarnings != null && currentWarnings.size() > 0; - } - - List getOutputStream() { - return outputStream; - } - - CallStack getCallStack() { - return callStack; - } - - VariablesState getVariablesState() { - return variablesState; - } - - List getEvaluationStack() { - return evaluationStack; - } - - int getStorySeed() { - return storySeed; - } - - void setStorySeed(int s) { - storySeed = s; - } - - int getPreviousRandom() { - return previousRandom; - } - - void setPreviousRandom(int i) { - previousRandom = i; - } - - int getCurrentTurnIndex() { - return currentTurnIndex; - } - - boolean outputStreamContainsContent() { - for (RTObject content : outputStream) { - if (content instanceof StringValue) - return true; - } - return false; - } - - boolean outputStreamEndsInNewline() { - if (outputStream.size() > 0) { - - for (int i = outputStream.size() - 1; i >= 0; i--) { - RTObject obj = outputStream.get(i); - if (obj instanceof ControlCommand) // e.g. BeginString - break; - StringValue text = outputStream.get(i) instanceof StringValue ? (StringValue) outputStream.get(i) - : null; - - if (text != null) { - if (text.isNewline()) - return true; - else if (text.isNonWhitespace()) - break; - } - } - } - - return false; - } - - RTObject peekEvaluationStack() { - return evaluationStack.get(evaluationStack.size() - 1); - } - - RTObject popEvaluationStack() { - RTObject obj = evaluationStack.get(evaluationStack.size() - 1); - evaluationStack.remove(evaluationStack.size() - 1); - return obj; - } - - List popEvaluationStack(int numberOfObjects) throws Exception { - if (numberOfObjects > evaluationStack.size()) { - throw new Exception("trying to pop too many objects"); - } - - List popped = new ArrayList<>( - evaluationStack.subList(evaluationStack.size() - numberOfObjects, evaluationStack.size())); - evaluationStack.subList(evaluationStack.size() - numberOfObjects, evaluationStack.size()).clear(); - - return popped; - } - - void pushEvaluationStack(RTObject obj) { - - // Include metadata about the origin List for set values when - // they're used, so that lower level functions can make use - // of the origin list to get related items, or make comparisons - // with the integer values etc. - ListValue listValue = null; - if (obj instanceof ListValue) - listValue = (ListValue) obj; - - if (listValue != null) { - // Update origin when list is has something to indicate the list - // origin - InkList rawList = listValue.getValue(); - - if (rawList.getOriginNames() != null) { - - if (rawList.getOrigins() == null) - rawList.setOrigins(new ArrayList()); - - rawList.getOrigins().clear(); - - for (String n : rawList.getOriginNames()) { - ListDefinition def = story.getListDefinitions().getListDefinition(n); - if (!rawList.getOrigins().contains(def)) - rawList.getOrigins().add(def); - - } - } - } - - evaluationStack.add(obj); - } - - // Push to output stream, but split out newlines in text for consistency - // in dealing with them later. - void pushToOutputStream(RTObject obj) { - StringValue text = obj instanceof StringValue ? (StringValue) obj : null; - - if (text != null) { - List listText = trySplittingHeadTailWhitespace(text); - if (listText != null) { - for (StringValue textObj : listText) { - pushToOutputStreamIndividual(textObj); - } - outputStreamDirty(); - return; - } - } - - pushToOutputStreamIndividual(obj); - } - - void pushToOutputStreamIndividual(RTObject obj) { - Glue glue = obj instanceof Glue ? (Glue) obj : null; - StringValue text = obj instanceof StringValue ? (StringValue) obj : null; - - boolean includeInOutput = true; - - // New glue, so chomp away any whitespace from the end of the stream - if (glue != null) { - trimNewlinesFromOutputStream(); - includeInOutput = true; - } - // New text: do we really want to append it, if it's whitespace? - // Two different reasons for whitespace to be thrown away: - // - Function start/end trimming - // - User defined glue: <> - // We also need to know when to stop trimming, when there's non-whitespace. - else if (text != null) { - - // Where does the current function call begin? - int functionTrimIndex = -1; - Element currEl = callStack.getCurrentElement(); - if (currEl.type == PushPopType.Function) { - functionTrimIndex = currEl.functionStartInOuputStream; - } - - // Do 2 things: - // - Find latest glue - // - Check whether we're in the middle of string evaluation - // If we're in string eval within the current function, we - // don't want to trim back further than the length of the current string. - int glueTrimIndex = -1; - for (int i = outputStream.size() - 1; i >= 0; i--) { - RTObject o = outputStream.get(i); - ControlCommand c = o instanceof ControlCommand ? (ControlCommand) o : null; - Glue g = o instanceof Glue ? (Glue) o : null; - - // Find latest glue - if (g != null) { - glueTrimIndex = i; - break; - } - - // Don't function-trim past the start of a string evaluation section - else if (c != null && c.getCommandType() == ControlCommand.CommandType.BeginString) { - if (i >= functionTrimIndex) { - functionTrimIndex = -1; - } - break; - } - } - - // Where is the most agressive (earliest) trim point? - int trimIndex = -1; - if (glueTrimIndex != -1 && functionTrimIndex != -1) - trimIndex = Math.min(functionTrimIndex, glueTrimIndex); - else if (glueTrimIndex != -1) - trimIndex = glueTrimIndex; - else - trimIndex = functionTrimIndex; - - // So, are we trimming then? - if (trimIndex != -1) { - - // While trimming, we want to throw all newlines away, - // whether due to glue or the start of a function - if (text.isNewline()) { - includeInOutput = false; - } - - // Able to completely reset when normal text is pushed - else if (text.isNonWhitespace()) { - - if (glueTrimIndex > -1) - removeExistingGlue(); - - // Tell all functions in callstack that we have seen proper text, - // so trimming whitespace at the start is done. - if (functionTrimIndex > -1) { - List callstackElements = callStack.getElements(); - for (int i = callstackElements.size() - 1; i >= 0; i--) { - Element el = callstackElements.get(i); - if (el.type == PushPopType.Function) { - el.functionStartInOuputStream = -1; - } else { - break; - } - } - } - } - } - - // De-duplicate newlines, and don't ever lead with a newline - else if (text.isNewline()) { - if (outputStreamEndsInNewline() || !outputStreamContainsContent()) - includeInOutput = false; - } - } - - if (includeInOutput) { - outputStream.add(obj); - outputStreamDirty(); - } - - } - - // Only called when non-whitespace is appended - void removeExistingGlue() { - for (int i = outputStream.size() - 1; i >= 0; i--) { - RTObject c = outputStream.get(i); - if (c instanceof Glue) { - outputStream.remove(i); - } else if (c instanceof ControlCommand) { // e.g. - // BeginString - break; - } - } - - outputStreamDirty(); - } - - void outputStreamDirty() { - outputStreamTextDirty = true; - outputStreamTagsDirty = true; - } - - void resetErrors() { - currentErrors = null; - } - - void resetOutput(List objs) { - outputStream.clear(); - if (objs != null) - outputStream.addAll(objs); - outputStreamDirty(); - } - - void resetOutput() { - resetOutput(null); - } - - // Don't make public since the method need to be wrapped in Story for visit - // counting - void setChosenPath(Path path, boolean incrementingTurnIndex) throws Exception { - // Changing direction, assume we need to clear current set of choices - currentChoices.clear(); - - final Pointer newPointer = new Pointer(story.pointerAtPath(path)); - if (!newPointer.isNull() && newPointer.index == -1) - newPointer.index = 0; - - setCurrentPointer(newPointer); - - if (incrementingTurnIndex) - currentTurnIndex++; - } - - void startFunctionEvaluationFromGame(Container funcContainer, Object[] arguments) throws Exception { - callStack.push(PushPopType.FunctionEvaluationFromGame, evaluationStack.size()); - callStack.getCurrentElement().currentPointer.assign(Pointer.startOf(funcContainer)); - - passArgumentsToEvaluationStack(arguments); - } - - void passArgumentsToEvaluationStack(Object[] arguments) throws Exception { - // Pass arguments onto the evaluation stack - if (arguments != null) { - for (int i = 0; i < arguments.length; i++) { - if (!(arguments[i] instanceof Integer || arguments[i] instanceof Float - || arguments[i] instanceof String)) { - throw new Exception( - "ink arguments when calling EvaluateFunction / ChoosePathStringWithParameters must be int, float or string"); - } - - pushEvaluationStack(Value.create(arguments[i])); - } - } - } - - boolean tryExitFunctionEvaluationFromGame() { - if (callStack.getCurrentElement().type == PushPopType.FunctionEvaluationFromGame) { - setCurrentPointer(Pointer.Null); - didSafeExit = true; - return true; - } - - return false; - } - - Object completeFunctionEvaluationFromGame() throws StoryException, Exception { - if (callStack.getCurrentElement().type != PushPopType.FunctionEvaluationFromGame) { - throw new StoryException("Expected external function evaluation to be complete. Stack trace: " - + callStack.getCallStackTrace()); - } - - int originalEvaluationStackHeight = callStack.getCurrentElement().evaluationStackHeightWhenPushed; - - // Do we have a returned value? - // Potentially pop multiple values off the stack, in case we need - // to clean up after ourselves (e.g. caller of EvaluateFunction may - // have passed too many arguments, and we currently have no way to check - // for that) - RTObject returnedObj = null; - while (evaluationStack.size() > originalEvaluationStackHeight) { - RTObject poppedObj = popEvaluationStack(); - if (returnedObj == null) - returnedObj = poppedObj; - } - - // Finally, pop the external function evaluation - callStack.pop(PushPopType.FunctionEvaluationFromGame); - - // What did we get back? - if (returnedObj != null) { - if (returnedObj instanceof Void) - return null; - - // Some kind of value, if not void - Value returnVal = null; - - if (returnedObj instanceof Value) - returnVal = (Value) returnedObj; - - // DivertTargets get returned as the string of components - // (rather than a Path, which isn't public) - if (returnVal.getValueType() == ValueType.DivertTarget) { - return returnVal.getValueObject().toString(); - } - - // Other types can just have their exact object type: - // int, float, string. VariablePointers get returned as strings. - return returnVal.getValueObject(); - } - - return null; - } - - void setCurrentPointer(Pointer value) { - callStack.getCurrentElement().currentPointer.assign(value); - } - - void setInExpressionEvaluation(boolean value) { - callStack.getCurrentElement().inExpressionEvaluation = value; - } - - @SuppressWarnings("unchecked") - public void setJsonToken(HashMap value) throws StoryException, Exception { - - HashMap jObject = value; - - Object jSaveVersion = jObject.get("inkSaveVersion"); - - if (jSaveVersion == null) { - throw new StoryException("ink save format incorrect, can't load."); - } else if ((int) jSaveVersion < kMinCompatibleLoadVersion) { - throw new StoryException("Ink save format isn't compatible with the current version (saw '" + jSaveVersion - + "', but minimum is " + kMinCompatibleLoadVersion + "), so can't load."); - } - - callStack.setJsonToken((HashMap) jObject.get("callstackThreads"), story); - variablesState.setjsonToken((HashMap) jObject.get("variablesState")); - - evaluationStack = Json.jArrayToRuntimeObjList((List) jObject.get("evalStack")); - - outputStream = Json.jArrayToRuntimeObjList((List) jObject.get("outputStream")); - outputStreamDirty(); - - currentChoices = Json.jArrayToRuntimeObjList((List) jObject.get("currentChoices")); - - Object currentDivertTargetPath = jObject.get("currentDivertTarget"); - if (currentDivertTargetPath != null) { - Path divertPath = new Path(currentDivertTargetPath.toString()); - setDivertedPointer(story.pointerAtPath(divertPath)); - } - - visitCounts = Json.jObjectToIntHashMap((HashMap) jObject.get("visitCounts")); - turnIndices = Json.jObjectToIntHashMap((HashMap) jObject.get("turnIndices")); - currentTurnIndex = (int) jObject.get("turnIdx"); - storySeed = (int) jObject.get("storySeed"); - previousRandom = (int) jObject.get("previousRandom"); - - Object jChoiceThreadsObj = jObject.get("choiceThreads"); - HashMap jChoiceThreads = (HashMap) jChoiceThreadsObj; - - for (Choice c : currentChoices) { - Thread foundActiveThread = callStack.getThreadWithIndex(c.originalThreadIndex); - if (foundActiveThread != null) { - c.setThreadAtGeneration(foundActiveThread.copy()); - } else { - HashMap jSavedChoiceThread = (HashMap) jChoiceThreads - .get(Integer.toString(c.originalThreadIndex)); - c.setThreadAtGeneration(new CallStack.Thread(jSavedChoiceThread, story)); - } - } - - } - - void setPreviousPointer(Pointer value) { - callStack.getcurrentThread().previousPointer.assign(value); - } - - /** - * Exports the current state to json format, in order to save the game. - * - * @return The save state in json format. - */ - public String toJson() throws Exception { - SimpleJson.Writer writer = new SimpleJson.Writer(); - writeJson(writer); - - return writer.toString(); - } - - /** - * Exports the current state to json format, in order to save the game. For this - * overload you can pass in a custom stream, such as a FileStream. - * - * @throws Exception - */ - public void toJson(OutputStream stream) throws Exception { - SimpleJson.Writer writer = new SimpleJson.Writer(stream); - writeJson(writer); - } - - void trimNewlinesFromOutputStream() { - int removeWhitespaceFrom = -1; - - // Work back from the end, and try to find the point where - // we need to start removing content. - // - Simply work backwards to find the first newline in a String of - // whitespace - // e.g. This is the content \n \n\n - // ^---------^ whitespace to remove - // ^--- first while loop stops here - int i = outputStream.size() - 1; - while (i >= 0) { - RTObject obj = outputStream.get(i); - ControlCommand cmd = obj instanceof ControlCommand ? (ControlCommand) obj : null; - StringValue txt = obj instanceof StringValue ? (StringValue) obj : null; - - if (cmd != null || (txt != null && txt.isNonWhitespace())) { - break; - } else if (txt != null && txt.isNewline()) { - removeWhitespaceFrom = i; - } - i--; - } - - // Remove the whitespace - if (removeWhitespaceFrom >= 0) { - i = removeWhitespaceFrom; - while (i < outputStream.size()) { - StringValue text = outputStream.get(i) instanceof StringValue ? (StringValue) outputStream.get(i) - : null; - if (text != null) { - outputStream.remove(i); - } else { - i++; - } - } - } - - outputStreamDirty(); - } - - // At both the start and the end of the String, split out the new lines like - // so: - // - // " \n \n \n the String \n is awesome \n \n " - // ^-----------^ ^-------^ - // - // Excess newlines are converted into single newlines, and spaces discarded. - // Outside spaces are significant and retained. "Interior" newlines within - // the main String are ignored, since this is for the purpose of gluing - // only. - // - // - If no splitting is necessary, null is returned. - // - A newline on its own is returned in an list for consistency. - List trySplittingHeadTailWhitespace(StringValue single) { - String str = single.value; - - int headFirstNewlineIdx = -1; - int headLastNewlineIdx = -1; - for (int i = 0; i < str.length(); ++i) { - char c = str.charAt(i); - if (c == '\n') { - if (headFirstNewlineIdx == -1) - headFirstNewlineIdx = i; - headLastNewlineIdx = i; - } else if (c == ' ' || c == '\t') - continue; - else - break; - } - - int tailLastNewlineIdx = -1; - int tailFirstNewlineIdx = -1; - for (int i = 0; i < str.length(); ++i) { - char c = str.charAt(i); - if (c == '\n') { - if (tailLastNewlineIdx == -1) - tailLastNewlineIdx = i; - tailFirstNewlineIdx = i; - } else if (c == ' ' || c == '\t') - continue; - else - break; - } - - // No splitting to be done? - if (headFirstNewlineIdx == -1 && tailLastNewlineIdx == -1) - return null; - - List listTexts = new ArrayList<>(); - int innerStrStart = 0; - int innerStrEnd = str.length(); - - if (headFirstNewlineIdx != -1) { - if (headFirstNewlineIdx > 0) { - StringValue leadingSpaces = new StringValue(str.substring(0, headFirstNewlineIdx)); - listTexts.add(leadingSpaces); - } - listTexts.add(new StringValue("\n")); - innerStrStart = headLastNewlineIdx + 1; - } - - if (tailLastNewlineIdx != -1) { - innerStrEnd = tailFirstNewlineIdx; - } - - if (innerStrEnd > innerStrStart) { - String innerStrText = str.substring(innerStrStart, innerStrEnd); - listTexts.add(new StringValue(innerStrText)); - } - - if (tailLastNewlineIdx != -1 && tailFirstNewlineIdx > headLastNewlineIdx) { - listTexts.add(new StringValue("\n")); - if (tailLastNewlineIdx < str.length() - 1) { - int numSpaces = (str.length() - tailLastNewlineIdx) - 1; - StringValue trailingSpaces = new StringValue( - str.substring(tailLastNewlineIdx + 1, numSpaces + tailLastNewlineIdx + 1)); - listTexts.add(trailingSpaces); - } - } - - return listTexts; - } - - /** - * Gets the visit/read count of a particular Container at the given path. For a - * knot or stitch, that path String will be in the form: - * - * knot knot.stitch - * - * @return The number of times the specific knot or stitch has been enountered - * by the ink engine. - * - * @param pathString The dot-separated path String of the specific knot or - * stitch. - * @throws Exception - * - */ - public int visitCountAtPathString(String pathString) throws Exception { - Integer visitCountOut; - - if (patch != null) { - Container container = story.contentAtPath(new Path(pathString)).getContainer(); - if (container == null) - throw new Exception("Content at path not found: " + pathString); - - visitCountOut = patch.getVisitCount(container); - if (visitCountOut != null) - return visitCountOut; - } - - visitCountOut = visitCounts.get(pathString); - if (visitCountOut != null) - return visitCountOut; - - return 0; - } - - int visitCountForContainer(Container container) throws Exception { - if (!container.getVisitsShouldBeCounted()) { - story.error("Read count for target (" + container.getName() + " - on " + container.getDebugMetadata() - + ") unknown."); - return 0; - } - - if (patch != null && patch.getVisitCount(container) != null) - return patch.getVisitCount(container); - - String containerPathStr = container.getPath().toString(); - - if (visitCounts.containsKey(containerPathStr)) - return visitCounts.get(containerPathStr); - - return 0; - } - - void incrementVisitCountForContainer(Container container) throws Exception { - if (patch != null) { - int currCount = visitCountForContainer(container); - currCount++; - patch.setVisitCount(container, currCount); - - return; - } - - Integer count = 0; - String containerPathStr = container.getPath().toString(); - - if (visitCounts.containsKey(containerPathStr)) - count = visitCounts.get(containerPathStr); - - count++; - visitCounts.put(containerPathStr, count); - } - - void recordTurnIndexVisitToContainer(Container container) { - if (patch != null) { - patch.setTurnIndex(container, currentTurnIndex); - return; - } - - String containerPathStr = container.getPath().toString(); - turnIndices.put(containerPathStr, currentTurnIndex); - } - - int turnsSinceForContainer(Container container) throws Exception { - if (!container.getTurnIndexShouldBeCounted()) { - story.error("TURNS_SINCE() for target (" + container.getName() + " - on " + container.getDebugMetadata() - + ") unknown."); - } - - int index = 0; - - if (patch != null && patch.getTurnIndex(container) != null) { - index = patch.getTurnIndex(container); - return currentTurnIndex - index; - } - - String containerPathStr = container.getPath().toString(); - - if (turnIndices.containsKey(containerPathStr)) { - index = turnIndices.get(containerPathStr); - return currentTurnIndex - index; - } else { - return -1; - } - } - - public Pointer getDivertedPointer() { - return divertedPointer; - } - - public void setDivertedPointer(Pointer p) { - divertedPointer.assign(p); - } - - public boolean isDidSafeExit() { - return didSafeExit; - } - - public void setDidSafeExit(boolean didSafeExit) { - this.didSafeExit = didSafeExit; - } - - void setCallStack(CallStack cs) { - callStack = cs; - } - - void restoreAfterPatch() { - // VariablesState was being borrowed by the patched - // state, so restore it with our own callstack. - // _patch will be null normally, but if you're in the - // middle of a save, it may contain a _patch for save purpsoes. - variablesState.setCallStack(callStack); - variablesState.setPatch(patch); // usually null - } - - void applyAnyPatch() { - if (patch == null) - return; - - variablesState.applyPatch(); - - for (Entry pathToCount : patch.getVisitCounts().entrySet()) - applyCountChanges(pathToCount.getKey(), pathToCount.getValue(), true); - - for (Entry pathToIndex : patch.getTurnIndices().entrySet()) - applyCountChanges(pathToIndex.getKey(), pathToIndex.getValue(), false); - - patch = null; - } - - void applyCountChanges(Container container, int newCount, boolean isVisit) { - HashMap counts = isVisit ? visitCounts : turnIndices; - - counts.put(container.getPath().toString(), newCount); - } - - void writeJson(SimpleJson.Writer writer) throws Exception { - writer.writeObjectStart(); - - boolean hasChoiceThreads = false; - for (Choice c : currentChoices) { - c.originalThreadIndex = c.getThreadAtGeneration().threadIndex; - - if (callStack.getThreadWithIndex(c.originalThreadIndex) == null) { - if (!hasChoiceThreads) { - hasChoiceThreads = true; - writer.writePropertyStart("choiceThreads"); - writer.writeObjectStart(); - } - - writer.writePropertyStart(c.originalThreadIndex); - c.getThreadAtGeneration().writeJson(writer); - writer.writePropertyEnd(); - } - } - - if (hasChoiceThreads) { - writer.writeObjectEnd(); - writer.writePropertyEnd(); - } - - writer.writeProperty("callstackThreads", new InnerWriter() { - - @Override - public void write(Writer w) throws Exception { - callStack.writeJson(w); - } - - }); - - writer.writeProperty("variablesState", new InnerWriter() { - - @Override - public void write(Writer w) throws Exception { - variablesState.writeJson(w); - } - - }); - - writer.writeProperty("evalStack", new InnerWriter() { - - @Override - public void write(Writer w) throws Exception { - Json.writeListRuntimeObjs(w, evaluationStack); - } - - }); - - writer.writeProperty("outputStream", new InnerWriter() { - - @Override - public void write(Writer w) throws Exception { - Json.writeListRuntimeObjs(w, outputStream); - } - }); - - writer.writeProperty("currentChoices", new InnerWriter() { - - @Override - public void write(Writer w) throws Exception { - w.writeArrayStart(); - for (Choice c : currentChoices) - Json.writeChoice(w, c); - w.writeArrayEnd(); - } - }); - - if (!divertedPointer.isNull()) - writer.writeProperty("currentDivertTarget", divertedPointer.getPath().getComponentsString()); - - writer.writeProperty("visitCounts", new InnerWriter() { - - @Override - public void write(Writer w) throws Exception { - Json.WriteIntDictionary(w, visitCounts); - } - }); - - writer.writeProperty("turnIndices", new InnerWriter() { - - @Override - public void write(Writer w) throws Exception { - Json.WriteIntDictionary(w, turnIndices); - } - }); - - writer.writeProperty("turnIdx", currentTurnIndex); - writer.writeProperty("storySeed", storySeed); - writer.writeProperty("previousRandom", previousRandom); - - writer.writeProperty("inkSaveVersion", kInkSaveStateVersion); - - // Not using this right now, but could do in future. - writer.writeProperty("inkFormatVersion", Story.inkVersionCurrent); - - writer.writeObjectEnd(); - } - - @SuppressWarnings("unchecked") - void loadJsonObj(HashMap jObject) throws Exception { - Object jSaveVersion = jObject.get("inkSaveVersion"); - - if (jSaveVersion == null) { - throw new StoryException("ink save format incorrect, can't load."); - } else if ((int) jSaveVersion < kMinCompatibleLoadVersion) { - throw new StoryException("Ink save format isn't compatible with the current version (saw '" + jSaveVersion - + "', but minimum is " + kMinCompatibleLoadVersion + "), so can't load."); - } - - callStack.setJsonToken((HashMap) jObject.get("callstackThreads"), story); - variablesState.setJsonToken((HashMap) jObject.get("variablesState")); - - evaluationStack = Json.jArrayToRuntimeObjList((List) jObject.get("evalStack")); - - outputStream = Json.jArrayToRuntimeObjList((List) jObject.get("outputStream")); - outputStreamDirty(); - - currentChoices = Json.jArrayToRuntimeObjList((List) jObject.get("currentChoices")); - - Object currentDivertTargetPath = jObject.get("currentDivertTarget"); - if (currentDivertTargetPath != null) { - Path divertPath = new Path(currentDivertTargetPath.toString()); - divertedPointer.assign(story.pointerAtPath(divertPath)); - } - - visitCounts = Json.jObjectToIntHashMap((HashMap) jObject.get("visitCounts")); - turnIndices = Json.jObjectToIntHashMap((HashMap) jObject.get("turnIndices")); - - currentTurnIndex = (int) jObject.get("turnIdx"); - storySeed = (int) jObject.get("storySeed"); - - // Not optional, but bug in inkjs means it's actually missing in inkjs saves - Object previousRandomObj = jObject.get("previousRandom"); - if (previousRandomObj != null) { - previousRandom = (int) previousRandomObj; - } else { - previousRandom = 0; - } + /** + * The current version of the state save file JSON-based format. + */ + // + // 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"; + + // REMEMBER! REMEMBER! REMEMBER! + // When adding state, update the Copy method and serialisation + // REMEMBER! REMEMBER! REMEMBER! + + // TODO: Consider removing currentErrors / currentWarnings altogether + // and relying on client error handler code immediately handling StoryExceptions + // etc + // Or is there a specific reason we need to collect potentially multiple + // errors before throwing/exiting? + private List currentErrors; + private List currentWarnings; + private int currentTurnIndex; + private boolean didSafeExit; + private final Pointer divertedPointer = new Pointer(); + private List evaluationStack; + private final Story story; + private int storySeed; + private int previousRandom; + private HashMap turnIndices; + private VariablesState variablesState; + private HashMap visitCounts; + private String currentText; + + private boolean outputStreamTextDirty = true; + private boolean outputStreamTagsDirty = true; + private List currentTags; + + private StatePatch patch; + + private HashMap namedFlows; + private Flow currentFlow; + + private List aliveFlowNames; + + boolean aliveFlowNamesDirty = true; + + StoryState(Story story) { + this.story = story; + + currentFlow = new Flow(kDefaultFlowName, story); + outputStreamDirty(); + aliveFlowNamesDirty = true; + + evaluationStack = new ArrayList<>(); + + variablesState = new VariablesState(getCallStack(), story.getListDefinitions()); + + visitCounts = new HashMap<>(); + turnIndices = new HashMap<>(); + currentTurnIndex = -1; + + // Seed the shuffle random numbers + long timeSeed = System.currentTimeMillis(); + + storySeed = new Random(timeSeed).nextInt() % 100; + previousRandom = 0; + + goToStart(); + } + + int getCallStackDepth() { + return getCallStack().getDepth(); + } + + void addError(String message, boolean isWarning) { + if (!isWarning) { + if (currentErrors == null) currentErrors = new ArrayList<>(); + + currentErrors.add(message); + } else { + if (currentWarnings == null) currentWarnings = new ArrayList<>(); + + currentWarnings.add(message); + } + } + + // Warning: Any RTObject content referenced within the StoryState will + // be re-referenced rather than cloned. This is generally okay though since + // RTObjects are treated as immutable after they've been set up. + // (e.g. we don't edit a Runtime.StringValue after it's been created an added.) + // I wonder if there's a sensible way to enforce that..?? + StoryState copyAndStartPatching(boolean forBackgroundSave) { + StoryState copy = new StoryState(story); + + copy.patch = new StatePatch(patch); + + // Hijack the new default flow to become a copy of our current one + // If the patch is applied, then this new flow will replace the old one in + // _namedFlows + copy.currentFlow.name = currentFlow.name; + copy.currentFlow.callStack = new CallStack(currentFlow.callStack); + copy.currentFlow.outputStream.addAll(currentFlow.outputStream); + copy.outputStreamDirty(); + + // When background saving we need to make copies of choices since they each have + // a snapshot of the thread at the time of generation since the game could progress + // significantly and threads modified during the save process. + // However, when doing internal saving and restoring of snapshots this isn't an issue, + // and we can simply ref-copy the choices with their existing threads. + if (forBackgroundSave) { + for (Choice choice : currentFlow.currentChoices) copy.currentFlow.currentChoices.add(choice.clone()); + } else { + copy.currentFlow.currentChoices.addAll(currentFlow.currentChoices); + } + + // The copy of the state has its own copy of the named flows dictionary, + // except with the current flow replaced with the copy above + // (Assuming we're in multi-flow mode at all. If we're not then + // the above copy is simply the default flow copy and we're done) + if (namedFlows != null) { + copy.namedFlows = new HashMap<>(); + for (Map.Entry namedFlow : namedFlows.entrySet()) + copy.namedFlows.put(namedFlow.getKey(), namedFlow.getValue()); + copy.namedFlows.put(currentFlow.name, copy.currentFlow); + copy.aliveFlowNamesDirty = true; + } + + if (hasError()) { + copy.currentErrors = new ArrayList<>(); + copy.currentErrors.addAll(currentErrors); + } + + if (hasWarning()) { + copy.currentWarnings = new ArrayList<>(); + copy.currentWarnings.addAll(currentWarnings); + } - Object jChoiceThreadsObj = jObject.get("choiceThreads"); - HashMap jChoiceThreads = (HashMap) jChoiceThreadsObj; - - for (Choice c : currentChoices) { - Thread foundActiveThread = callStack.getThreadWithIndex(c.originalThreadIndex); - if (foundActiveThread != null) { - c.setThreadAtGeneration(foundActiveThread.copy()); - } else { - HashMap jSavedChoiceThread = (HashMap) jChoiceThreads - .get(Integer.toString(c.originalThreadIndex)); - c.setThreadAtGeneration(new CallStack.Thread(jSavedChoiceThread, story)); - } - } - } + // ref copy - exactly the same variables state! + // we're expecting not to read it only while in patch mode + // (though the callstack will be modified) + copy.variablesState = variablesState; + copy.variablesState.setCallStack(copy.getCallStack()); + copy.variablesState.setPatch(copy.patch); + + copy.evaluationStack.addAll(evaluationStack); + + if (!divertedPointer.isNull()) copy.divertedPointer.assign(divertedPointer); + + copy.setPreviousPointer(getPreviousPointer()); + + // visit counts and turn indicies will be read only, not modified + // while in patch mode + copy.visitCounts = visitCounts; + copy.turnIndices = turnIndices; + + copy.currentTurnIndex = currentTurnIndex; + copy.storySeed = storySeed; + copy.previousRandom = previousRandom; + + copy.setDidSafeExit(didSafeExit); + + return copy; + } + + void popFromOutputStream(int count) { + getOutputStream() + .subList(getOutputStream().size() - count, getOutputStream().size()) + .clear(); + + outputStreamDirty(); + } + + 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 (!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; + } + } + } + } + + currentText = cleanOutputWhitespace(sb.toString()); + + outputStreamTextDirty = false; + } + + return currentText; + } + + /** + * Cleans inline whitespace in the following way: - Removes all whitespace from + * the start and end of line (including just before a \n) - Turns all + * consecutive space and tab runs into single spaces (HTML style) + */ + String cleanOutputWhitespace(String str) { + StringBuilder sb = new StringBuilder(str.length()); + + int currentWhitespaceStart = -1; + int startOfLine = 0; + + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + + boolean isInlineWhitespace = c == ' ' || c == '\t'; + + if (isInlineWhitespace && currentWhitespaceStart == -1) currentWhitespaceStart = i; + + if (!isInlineWhitespace) { + if (c != '\n' && currentWhitespaceStart > 0 && currentWhitespaceStart != startOfLine) { + sb.append(' '); + } + currentWhitespaceStart = -1; + } + + if (c == '\n') startOfLine = i + 1; + + if (!isInlineWhitespace) sb.append(c); + } + + return sb.toString(); + } + + /** + * Ends the current ink flow, unwrapping the callstack but without affecting any + * variables. Useful if the ink is (say) in the middle a nested tunnel, and you + * want it to reset so that you can divert elsewhere using ChoosePathString(). + * Otherwise, after finishing the content you diverted to, it would continue + * where it left off. Calling this is equivalent to calling -> END in ink. + */ + public void forceEnd() throws Exception { + + getCallStack().reset(); + + currentFlow.currentChoices.clear(); + + setCurrentPointer(Pointer.Null); + setPreviousPointer(Pointer.Null); + + setDidSafeExit(true); + } + + // Add the end of a function call, trim any whitespace from the end. + // We always trim the start and end of the text that a function produces. + // The start whitespace is discard as it is generated, and the end + // whitespace is trimmed in one go here when we pop the function. + void trimWhitespaceFromFunctionEnd() { + assert (getCallStack().getCurrentElement().type == PushPopType.Function); + + int functionStartPoint = getCallStack().getCurrentElement().functionStartInOuputStream; + + // If the start point has become -1, it means that some non-whitespace + // text has been pushed, so it's safe to go as far back as we're able. + if (functionStartPoint == -1) { + functionStartPoint = 0; + } + + // Trim whitespace from END of function call + for (int i = getOutputStream().size() - 1; i >= functionStartPoint; i--) { + RTObject obj = getOutputStream().get(i); + + if (obj instanceof ControlCommand) break; + + if (!(obj instanceof StringValue)) continue; + StringValue txt = (StringValue) obj; + + if (txt.isNewline() || txt.isInlineWhitespace()) { + getOutputStream().remove(i); + outputStreamDirty(); + } else { + break; + } + } + } + + void popCallstack() throws Exception { + popCallstack(null); + } + + void popCallstack(PushPopType popType) throws Exception { + // Add the end of a function call, trim any whitespace from the end. + if (getCallStack().getCurrentElement().type == PushPopType.Function) trimWhitespaceFromFunctionEnd(); + + getCallStack().pop(popType); + } + + /** + * Get the previous state of currentPathString, which can be helpful + * for finding out where the story was before it ended (when the path + * string becomes null) + */ + public String previousPathString() { + Pointer pointer = getPreviousPointer(); + if (pointer.isNull()) return null; + else return pointer.getPath().toString(); + } + + Pointer getCurrentPointer() { + return getCallStack().getCurrentElement().currentPointer; + } + + List getCurrentTags() { + if (outputStreamTagsDirty) { + currentTags = new ArrayList<>(); + + boolean inTag = false; + StringBuilder sb = new StringBuilder(); + + for (RTObject outputObj : getOutputStream()) { + + 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; + } + + return currentTags; + } + + public String getCurrentFlowName() { + return currentFlow.name; + } + + public boolean currentFlowIsDefaultFlow() { + return Objects.equals(currentFlow.name, kDefaultFlowName); + } + + public List aliveFlowNames() { + if (aliveFlowNamesDirty) { + aliveFlowNames = new ArrayList<>(); + + if (namedFlows != null) { + for (String flowName : namedFlows.keySet()) { + if (!Objects.equals(flowName, kDefaultFlowName)) { + aliveFlowNames.add(flowName); + } + } + } + + aliveFlowNamesDirty = false; + } + + return aliveFlowNames; + } + + boolean getInExpressionEvaluation() { + return getCallStack().getCurrentElement().inExpressionEvaluation; + } + + Pointer getPreviousPointer() { + return getCallStack().getcurrentThread().previousPointer; + } + + void goToStart() { + getCallStack().getCurrentElement().currentPointer.assign(Pointer.startOf(story.getMainContentContainer())); + } + + void switchFlowInternal(String flowName) throws Exception { + if (flowName == null) throw new Exception("Must pass a non-null string to Story.SwitchFlow"); + + if (namedFlows == null) { + namedFlows = new HashMap<>(); + namedFlows.put(kDefaultFlowName, currentFlow); + } + + if (flowName.equals(currentFlow.name)) { + return; + } + + Flow flow = namedFlows.get(flowName); + if (flow == null) { + flow = new Flow(flowName, story); + namedFlows.put(flowName, flow); + aliveFlowNamesDirty = true; + } + + currentFlow = flow; + variablesState.setCallStack(currentFlow.callStack); + + // Cause text to be regenerated from output stream if necessary + outputStreamDirty(); + } + + void switchToDefaultFlowInternal() throws Exception { + if (namedFlows == null) return; + + switchFlowInternal(kDefaultFlowName); + } + + void removeFlowInternal(String flowName) throws Exception { + if (flowName == null) throw new Exception("Must pass a non-null string to Story.DestroyFlow"); + if (flowName.equals(kDefaultFlowName)) throw new Exception("Cannot destroy default flow"); + + // If we're currently in the flow that's being removed, switch back to default + if (currentFlow.name.equals(flowName)) { + switchToDefaultFlowInternal(); + } + + namedFlows.remove(flowName); + aliveFlowNamesDirty = true; + } + + boolean hasError() { + return currentErrors != null && !currentErrors.isEmpty(); + } + + boolean inStringEvaluation() { + for (int i = getOutputStream().size() - 1; i >= 0; i--) { + ControlCommand cmd = getOutputStream().get(i) instanceof ControlCommand + ? (ControlCommand) getOutputStream().get(i) + : null; + + if (cmd != null && cmd.getCommandType() == ControlCommand.CommandType.BeginString) { + return true; + } + } + + return false; + } + + /** + * Loads a previously saved state in JSON format. + * + * @param json The JSON String to load. + */ + public void loadJson(String json) throws Exception { + HashMap jObject = SimpleJson.textToDictionary(json); + loadJsonObj(jObject); + } + + List getCurrentChoices() { + // If we can continue generating text content rather than choices, + // then we reflect the choice list as being empty, since choices + // should always come at the end. + if (canContinue()) return new ArrayList<>(); + + return currentFlow.currentChoices; + } + + List getGeneratedChoices() { + return currentFlow.currentChoices; + } + + boolean canContinue() { + return !getCurrentPointer().isNull() && !hasError(); + } + + List getCurrentErrors() { + return currentErrors; + } + + List getCurrentWarnings() { + return currentWarnings; + } + + boolean hasWarning() { + return currentWarnings != null && currentWarnings.size() > 0; + } + + List getOutputStream() { + return currentFlow.outputStream; + } + + CallStack getCallStack() { + return currentFlow.callStack; + } + + VariablesState getVariablesState() { + return variablesState; + } + + List getEvaluationStack() { + return evaluationStack; + } + + int getStorySeed() { + return storySeed; + } + + void setStorySeed(int s) { + storySeed = s; + } + + int getPreviousRandom() { + return previousRandom; + } + + void setPreviousRandom(int i) { + previousRandom = i; + } + + int getCurrentTurnIndex() { + return currentTurnIndex; + } + + boolean outputStreamContainsContent() { + for (RTObject content : getOutputStream()) { + if (content instanceof StringValue) return true; + } + return false; + } + + boolean outputStreamEndsInNewline() { + if (getOutputStream().size() > 0) { + + for (int i = getOutputStream().size() - 1; i >= 0; i--) { + RTObject obj = getOutputStream().get(i); + if (obj instanceof ControlCommand) // e.g. BeginString + break; + StringValue text = getOutputStream().get(i) instanceof StringValue + ? (StringValue) getOutputStream().get(i) + : null; + + if (text != null) { + if (text.isNewline()) return true; + else if (text.isNonWhitespace()) break; + } + } + } + + return false; + } + + RTObject peekEvaluationStack() { + return evaluationStack.get(evaluationStack.size() - 1); + } + + RTObject popEvaluationStack() { + RTObject obj = evaluationStack.get(evaluationStack.size() - 1); + evaluationStack.remove(evaluationStack.size() - 1); + + return obj; + } + + List popEvaluationStack(int numberOfObjects) throws Exception { + if (numberOfObjects > evaluationStack.size()) { + throw new Exception("trying to pop too many objects"); + } + + List popped = new ArrayList<>( + evaluationStack.subList(evaluationStack.size() - numberOfObjects, evaluationStack.size())); + evaluationStack + .subList(evaluationStack.size() - numberOfObjects, evaluationStack.size()) + .clear(); + + return popped; + } + + void pushEvaluationStack(RTObject obj) { + + // Include metadata about the origin List for set values when + // they're used, so that lower level functions can make use + // of the origin list to get related items, or make comparisons + // with the integer values etc. + ListValue listValue = null; + if (obj instanceof ListValue) listValue = (ListValue) obj; + + if (listValue != null) { + // Update origin when list is has something to indicate the list + // origin + InkList rawList = listValue.getValue(); + + if (rawList.getOriginNames() != null) { + + if (rawList.getOrigins() == null) rawList.setOrigins(new ArrayList<>()); + + rawList.getOrigins().clear(); + + for (String n : rawList.getOriginNames()) { + ListDefinition def = story.getListDefinitions().getListDefinition(n); + if (!rawList.getOrigins().contains(def)) + rawList.getOrigins().add(def); + } + } + } + + evaluationStack.add(obj); + } + + // Push to output stream, but split out newlines in text for consistency + // in dealing with them later. + void pushToOutputStream(RTObject obj) { + StringValue text = obj instanceof StringValue ? (StringValue) obj : null; + + if (text != null) { + List listText = trySplittingHeadTailWhitespace(text); + if (listText != null) { + for (StringValue textObj : listText) { + pushToOutputStreamIndividual(textObj); + } + outputStreamDirty(); + return; + } + } + + pushToOutputStreamIndividual(obj); + } + + void pushToOutputStreamIndividual(RTObject obj) { + Glue glue = obj instanceof Glue ? (Glue) obj : null; + StringValue text = obj instanceof StringValue ? (StringValue) obj : null; + + boolean includeInOutput = true; + + // New glue, so chomp away any whitespace from the end of the stream + if (glue != null) { + trimNewlinesFromOutputStream(); + includeInOutput = true; + } + // New text: do we really want to append it, if it's whitespace? + // Two different reasons for whitespace to be thrown away: + // - Function start/end trimming + // - User defined glue: <> + // We also need to know when to stop trimming, when there's non-whitespace. + else if (text != null) { + + // Where does the current function call begin? + int functionTrimIndex = -1; + Element currEl = getCallStack().getCurrentElement(); + if (currEl.type == PushPopType.Function) { + functionTrimIndex = currEl.functionStartInOuputStream; + } + + // Do 2 things: + // - Find latest glue + // - Check whether we're in the middle of string evaluation + // If we're in string eval within the current function, we + // don't want to trim back further than the length of the current string. + int glueTrimIndex = -1; + for (int i = getOutputStream().size() - 1; i >= 0; i--) { + RTObject o = getOutputStream().get(i); + ControlCommand c = o instanceof ControlCommand ? (ControlCommand) o : null; + Glue g = o instanceof Glue ? (Glue) o : null; + + // Find latest glue + if (g != null) { + glueTrimIndex = i; + break; + } + + // Don't function-trim past the start of a string evaluation section + else if (c != null && c.getCommandType() == ControlCommand.CommandType.BeginString) { + if (i >= functionTrimIndex) { + functionTrimIndex = -1; + } + break; + } + } + + // Where is the most agressive (earliest) trim point? + int trimIndex = -1; + if (glueTrimIndex != -1 && functionTrimIndex != -1) trimIndex = Math.min(functionTrimIndex, glueTrimIndex); + else if (glueTrimIndex != -1) trimIndex = glueTrimIndex; + else trimIndex = functionTrimIndex; + + // So, are we trimming then? + if (trimIndex != -1) { + + // While trimming, we want to throw all newlines away, + // whether due to glue or the start of a function + if (text.isNewline()) { + includeInOutput = false; + } + + // Able to completely reset when normal text is pushed + else if (text.isNonWhitespace()) { + + if (glueTrimIndex > -1) removeExistingGlue(); + + // Tell all functions in callstack that we have seen proper text, + // so trimming whitespace at the start is done. + if (functionTrimIndex > -1) { + List callstackElements = getCallStack().getElements(); + for (int i = callstackElements.size() - 1; i >= 0; i--) { + Element el = callstackElements.get(i); + if (el.type == PushPopType.Function) { + el.functionStartInOuputStream = -1; + } else { + break; + } + } + } + } + } + + // De-duplicate newlines, and don't ever lead with a newline + else if (text.isNewline()) { + if (outputStreamEndsInNewline() || !outputStreamContainsContent()) includeInOutput = false; + } + } + + if (includeInOutput) { + getOutputStream().add(obj); + outputStreamDirty(); + } + } + + // Only called when non-whitespace is appended + void removeExistingGlue() { + for (int i = getOutputStream().size() - 1; i >= 0; i--) { + RTObject c = getOutputStream().get(i); + if (c instanceof Glue) { + getOutputStream().remove(i); + } else if (c instanceof ControlCommand) { // e.g. + // BeginString + break; + } + } + + outputStreamDirty(); + } + + void outputStreamDirty() { + outputStreamTextDirty = true; + outputStreamTagsDirty = true; + } + + void resetErrors() { + currentErrors = null; + } + + void resetOutput(List objs) { + getOutputStream().clear(); + if (objs != null) getOutputStream().addAll(objs); + outputStreamDirty(); + } + + void resetOutput() { + resetOutput(null); + } + + // Don't make public since the method need to be wrapped in Story for visit + // counting + void setChosenPath(Path path, boolean incrementingTurnIndex) throws Exception { + // Changing direction, assume we need to clear current set of choices + currentFlow.currentChoices.clear(); + + final Pointer newPointer = new Pointer(story.pointerAtPath(path)); + if (!newPointer.isNull() && newPointer.index == -1) newPointer.index = 0; + + setCurrentPointer(newPointer); + + if (incrementingTurnIndex) currentTurnIndex++; + } + + void startFunctionEvaluationFromGame(Container funcContainer, Object[] arguments) throws Exception { + getCallStack().push(PushPopType.FunctionEvaluationFromGame, evaluationStack.size()); + getCallStack().getCurrentElement().currentPointer.assign(Pointer.startOf(funcContainer)); + + passArgumentsToEvaluationStack(arguments); + } + + void passArgumentsToEvaluationStack(Object[] arguments) throws Exception { + // Pass arguments onto the evaluation stack + if (arguments != null) { + for (int i = 0; i < arguments.length; i++) { + if (!(arguments[i] instanceof Integer + || arguments[i] instanceof Float + || arguments[i] instanceof String + || arguments[i] instanceof Boolean + || arguments[i] instanceof InkList)) { + throw new Exception( + "ink arguments when calling EvaluateFunction / ChoosePathStringWithParameters must be " + + "int, float, string, bool or InkList. Argument was " + + (arguments[i] == null + ? "null" + : arguments[i].getClass().getName())); + } + + pushEvaluationStack(Value.create(arguments[i])); + } + } + } + + boolean tryExitFunctionEvaluationFromGame() { + if (getCallStack().getCurrentElement().type == PushPopType.FunctionEvaluationFromGame) { + setCurrentPointer(Pointer.Null); + didSafeExit = true; + return true; + } + + return false; + } + + Object completeFunctionEvaluationFromGame() throws Exception { + if (getCallStack().getCurrentElement().type != PushPopType.FunctionEvaluationFromGame) { + throw new Exception("Expected external function evaluation to be complete. Stack trace: " + + getCallStack().getCallStackTrace()); + } + + int originalEvaluationStackHeight = getCallStack().getCurrentElement().evaluationStackHeightWhenPushed; + + // Do we have a returned value? + // Potentially pop multiple values off the stack, in case we need + // to clean up after ourselves (e.g. caller of EvaluateFunction may + // have passed too many arguments, and we currently have no way to check + // for that) + RTObject returnedObj = null; + while (evaluationStack.size() > originalEvaluationStackHeight) { + RTObject poppedObj = popEvaluationStack(); + if (returnedObj == null) returnedObj = poppedObj; + } + + // Finally, pop the external function evaluation + getCallStack().pop(PushPopType.FunctionEvaluationFromGame); + + // What did we get back? + if (returnedObj != null) { + if (returnedObj instanceof Void) return null; + + // Some kind of value, if not void + Value returnVal = null; + + if (returnedObj instanceof Value) returnVal = (Value) returnedObj; + + // DivertTargets get returned as the string of components + // (rather than a Path, which isn't public) + if (returnVal.getValueType() == ValueType.DivertTarget) { + return returnVal.getValueObject().toString(); + } + + // Other types can just have their exact object type: + // int, float, string. VariablePointers get returned as strings. + return returnVal.getValueObject(); + } + + return null; + } + + void setCurrentPointer(Pointer value) { + getCallStack().getCurrentElement().currentPointer.assign(value); + } + + void setInExpressionEvaluation(boolean value) { + getCallStack().getCurrentElement().inExpressionEvaluation = value; + } + + void setPreviousPointer(Pointer value) { + getCallStack().getcurrentThread().previousPointer.assign(value); + } + + /** + * Exports the current state to json format, in order to save the game. + * + * @return The save state in json format. + */ + public String toJson() throws Exception { + SimpleJson.Writer writer = new SimpleJson.Writer(); + writeJson(writer); + + return writer.toString(); + } + + /** + * Exports the current state to json format, in order to save the game. For this + * overload you can pass in a custom stream, such as a FileStream. + * + * @throws Exception + */ + public void toJson(OutputStream stream) throws Exception { + SimpleJson.Writer writer = new SimpleJson.Writer(stream); + writeJson(writer); + } + + void trimNewlinesFromOutputStream() { + int removeWhitespaceFrom = -1; + + // Work back from the end, and try to find the point where + // we need to start removing content. + // - Simply work backwards to find the first newline in a String of + // whitespace + // e.g. This is the content \n \n\n + // ^---------^ whitespace to remove + // ^--- first while loop stops here + int i = getOutputStream().size() - 1; + while (i >= 0) { + RTObject obj = getOutputStream().get(i); + ControlCommand cmd = obj instanceof ControlCommand ? (ControlCommand) obj : null; + StringValue txt = obj instanceof StringValue ? (StringValue) obj : null; + + if (cmd != null || (txt != null && txt.isNonWhitespace())) { + break; + } else if (txt != null && txt.isNewline()) { + removeWhitespaceFrom = i; + } + i--; + } + + // Remove the whitespace + if (removeWhitespaceFrom >= 0) { + i = removeWhitespaceFrom; + while (i < getOutputStream().size()) { + StringValue text = getOutputStream().get(i) instanceof StringValue + ? (StringValue) getOutputStream().get(i) + : null; + if (text != null) { + getOutputStream().remove(i); + } else { + i++; + } + } + } + + outputStreamDirty(); + } + + // At both the start and the end of the String, split out the new lines like + // so: + // + // " \n \n \n the String \n is awesome \n \n " + // ^-----------^ ^-------^ + // + // Excess newlines are converted into single newlines, and spaces discarded. + // Outside spaces are significant and retained. "Interior" newlines within + // the main String are ignored, since this is for the purpose of gluing + // only. + // + // - If no splitting is necessary, null is returned. + // - A newline on its own is returned in an list for consistency. + List trySplittingHeadTailWhitespace(StringValue single) { + String str = single.value; + + int headFirstNewlineIdx = -1; + int headLastNewlineIdx = -1; + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if (c == '\n') { + if (headFirstNewlineIdx == -1) headFirstNewlineIdx = i; + headLastNewlineIdx = i; + } else if (c == ' ' || c == '\t') continue; + else break; + } + + int tailLastNewlineIdx = -1; + int tailFirstNewlineIdx = -1; + for (int i = str.length() - 1; i >= 0; i--) { + char c = str.charAt(i); + if (c == '\n') { + if (tailLastNewlineIdx == -1) tailLastNewlineIdx = i; + tailFirstNewlineIdx = i; + } else if (c == ' ' || c == '\t') continue; + else break; + } + + // No splitting to be done? + if (headFirstNewlineIdx == -1 && tailLastNewlineIdx == -1) return null; + + List listTexts = new ArrayList<>(); + int innerStrStart = 0; + int innerStrEnd = str.length(); + + if (headFirstNewlineIdx != -1) { + if (headFirstNewlineIdx > 0) { + StringValue leadingSpaces = new StringValue(str.substring(0, headFirstNewlineIdx)); + listTexts.add(leadingSpaces); + } + listTexts.add(new StringValue("\n")); + innerStrStart = headLastNewlineIdx + 1; + } + + if (tailLastNewlineIdx != -1) { + innerStrEnd = tailFirstNewlineIdx; + } + + if (innerStrEnd > innerStrStart) { + String innerStrText = str.substring(innerStrStart, innerStrEnd); + listTexts.add(new StringValue(innerStrText)); + } + + if (tailLastNewlineIdx != -1 && tailFirstNewlineIdx > headLastNewlineIdx) { + listTexts.add(new StringValue("\n")); + if (tailLastNewlineIdx < str.length() - 1) { + int numSpaces = (str.length() - tailLastNewlineIdx) - 1; + StringValue trailingSpaces = + new StringValue(str.substring(tailLastNewlineIdx + 1, numSpaces + tailLastNewlineIdx + 1)); + listTexts.add(trailingSpaces); + } + } + + return listTexts; + } + + /** + * Gets the visit/read count of a particular Container at the given path. For a + * knot or stitch, that path String will be in the form: + *

+ * knot knot.stitch + * + * @param pathString The dot-separated path String of the specific knot or + * stitch. + * @return The number of times the specific knot or stitch has been enountered + * by the ink engine. + * @throws Exception + */ + public int visitCountAtPathString(String pathString) throws Exception { + Integer visitCountOut; + + if (patch != null) { + Container container = story.contentAtPath(new Path(pathString)).getContainer(); + if (container == null) throw new Exception("Content at path not found: " + pathString); + + visitCountOut = patch.getVisitCount(container); + if (visitCountOut != null) return visitCountOut; + } + + visitCountOut = visitCounts.get(pathString); + if (visitCountOut != null) return visitCountOut; + + return 0; + } + + int visitCountForContainer(Container container) throws Exception { + if (!container.getVisitsShouldBeCounted()) { + story.error("Read count for target (" + container.getName() + " - on " + container.getDebugMetadata() + + ") unknown."); + return 0; + } + + if (patch != null && patch.getVisitCount(container) != null) return patch.getVisitCount(container); + + String containerPathStr = container.getPath().toString(); + + if (visitCounts.containsKey(containerPathStr)) return visitCounts.get(containerPathStr); + + return 0; + } + + void incrementVisitCountForContainer(Container container) throws Exception { + if (patch != null) { + int currCount = visitCountForContainer(container); + currCount++; + patch.setVisitCount(container, currCount); + + return; + } + + Integer count = 0; + String containerPathStr = container.getPath().toString(); + + if (visitCounts.containsKey(containerPathStr)) count = visitCounts.get(containerPathStr); + + count++; + visitCounts.put(containerPathStr, count); + } + + void recordTurnIndexVisitToContainer(Container container) { + if (patch != null) { + patch.setTurnIndex(container, currentTurnIndex); + return; + } + + String containerPathStr = container.getPath().toString(); + turnIndices.put(containerPathStr, currentTurnIndex); + } + + int turnsSinceForContainer(Container container) throws Exception { + if (!container.getTurnIndexShouldBeCounted()) { + story.error("TURNS_SINCE() for target (" + container.getName() + " - on " + container.getDebugMetadata() + + ") unknown."); + } + + int index = 0; + + if (patch != null && patch.getTurnIndex(container) != null) { + index = patch.getTurnIndex(container); + return currentTurnIndex - index; + } + + String containerPathStr = container.getPath().toString(); + + if (turnIndices.containsKey(containerPathStr)) { + index = turnIndices.get(containerPathStr); + return currentTurnIndex - index; + } else { + return -1; + } + } + + public Pointer getDivertedPointer() { + return divertedPointer; + } + + public void setDivertedPointer(Pointer p) { + divertedPointer.assign(p); + } + + public boolean isDidSafeExit() { + return didSafeExit; + } + + public void setDidSafeExit(boolean didSafeExit) { + this.didSafeExit = didSafeExit; + } + + void restoreAfterPatch() { + // VariablesState was being borrowed by the patched + // state, so restore it with our own callstack. + // _patch will be null normally, but if you're in the + // middle of a save, it may contain a _patch for save purpsoes. + variablesState.setCallStack(getCallStack()); + variablesState.setPatch(patch); // usually null + } + + void applyAnyPatch() { + if (patch == null) return; + + variablesState.applyPatch(); + + for (Entry pathToCount : patch.getVisitCounts().entrySet()) + applyCountChanges(pathToCount.getKey(), pathToCount.getValue(), true); + + for (Entry pathToIndex : patch.getTurnIndices().entrySet()) + applyCountChanges(pathToIndex.getKey(), pathToIndex.getValue(), false); + + patch = null; + } + + void applyCountChanges(Container container, int newCount, boolean isVisit) { + HashMap counts = isVisit ? visitCounts : turnIndices; + + counts.put(container.getPath().toString(), newCount); + } + + void writeJson(SimpleJson.Writer writer) throws Exception { + writer.writeObjectStart(); + + // Flows + writer.writePropertyStart("flows"); + writer.writeObjectStart(); + + // Multi-flow + if (namedFlows != null) { + for (Entry namedFlow : namedFlows.entrySet()) { + final Flow flow = namedFlow.getValue(); + + writer.writeProperty(namedFlow.getKey(), new InnerWriter() { + + @Override + public void write(Writer w) throws Exception { + flow.writeJson(w); + } + }); + } + } + + // Single flow + else { + writer.writeProperty(currentFlow.name, new InnerWriter() { + @Override + public void write(Writer w) throws Exception { + currentFlow.writeJson(w); + } + }); + } + + writer.writeObjectEnd(); + writer.writePropertyEnd(); // end of flows + + writer.writeProperty("currentFlowName", currentFlow.name); + + writer.writeProperty("variablesState", new InnerWriter() { + @Override + public void write(Writer w) throws Exception { + variablesState.writeJson(w); + } + }); + + writer.writeProperty("evalStack", new InnerWriter() { + @Override + public void write(Writer w) throws Exception { + Json.writeListRuntimeObjs(w, evaluationStack); + } + }); + + if (!divertedPointer.isNull()) + writer.writeProperty( + "currentDivertTarget", divertedPointer.getPath().getComponentsString()); + + writer.writeProperty("visitCounts", new InnerWriter() { + @Override + public void write(Writer w) throws Exception { + Json.writeIntDictionary(w, visitCounts); + } + }); + + writer.writeProperty("turnIndices", new InnerWriter() { + @Override + public void write(Writer w) throws Exception { + Json.writeIntDictionary(w, turnIndices); + } + }); + + writer.writeProperty("turnIdx", currentTurnIndex); + writer.writeProperty("storySeed", storySeed); + writer.writeProperty("previousRandom", previousRandom); + + writer.writeProperty("inkSaveVersion", kInkSaveStateVersion); + + // Not using this right now, but could do in future. + writer.writeProperty("inkFormatVersion", Story.inkVersionCurrent); + + writer.writeObjectEnd(); + } + + @SuppressWarnings("unchecked") + void loadJsonObj(HashMap jObject) throws Exception { + Object jSaveVersion = jObject.get("inkSaveVersion"); + + if (jSaveVersion == null) { + throw new Exception("ink save format incorrect, can't load."); + } else if ((int) jSaveVersion < kMinCompatibleLoadVersion) { + throw new Exception("Ink save format isn't compatible with the current version (saw '" + jSaveVersion + + "', but minimum is " + kMinCompatibleLoadVersion + "), so can't load."); + } + + // Flows: Always exists in latest format (even if there's just one default) + // but this dictionary doesn't exist in prev format + Object flowsObj = jObject.get("flows"); + if (flowsObj != null) { + HashMap flowsObjDict = (HashMap) flowsObj; + + // Single default flow + if (flowsObjDict.size() == 1) namedFlows = null; + + // Multi-flow, need to create flows dict + else if (namedFlows == null) namedFlows = new HashMap<>(); + + // Multi-flow, already have a flows dict + else namedFlows.clear(); + + // Load up each flow (there may only be one) + for (Entry namedFlowObj : flowsObjDict.entrySet()) { + String name = namedFlowObj.getKey(); + HashMap flowObj = (HashMap) namedFlowObj.getValue(); + + // Load up this flow using JSON data + Flow flow = new Flow(name, story, flowObj); + + if (flowsObjDict.size() == 1) { + currentFlow = new Flow(name, story, flowObj); + } else { + namedFlows.put(name, flow); + } + } + + if (namedFlows != null && namedFlows.size() > 1) { + String currFlowName = (String) jObject.get("currentFlowName"); + currentFlow = namedFlows.get(currFlowName); + } + } + + // Old format: individually load up callstack, output stream, choices in + // current/default flow + else { + namedFlows = null; + currentFlow.name = kDefaultFlowName; + currentFlow.callStack.setJsonToken((HashMap) jObject.get("callstackThreads"), story); + currentFlow.outputStream = Json.jArrayToRuntimeObjList((List) jObject.get("outputStream")); + currentFlow.currentChoices = Json.jArrayToRuntimeObjList((List) jObject.get("currentChoices")); + + Object jChoiceThreadsObj = jObject.get("choiceThreads"); + + currentFlow.loadFlowChoiceThreads((HashMap) jChoiceThreadsObj, story); + } + + outputStreamDirty(); + aliveFlowNamesDirty = true; + + variablesState.setJsonToken((HashMap) jObject.get("variablesState")); + variablesState.setCallStack(currentFlow.callStack); + + evaluationStack = Json.jArrayToRuntimeObjList((List) jObject.get("evalStack")); + + Object currentDivertTargetPath = jObject.get("currentDivertTarget"); + if (currentDivertTargetPath != null) { + Path divertPath = new Path(currentDivertTargetPath.toString()); + divertedPointer.assign(story.pointerAtPath(divertPath)); + } + + visitCounts = Json.jObjectToIntHashMap((HashMap) jObject.get("visitCounts")); + turnIndices = Json.jObjectToIntHashMap((HashMap) jObject.get("turnIndices")); + + currentTurnIndex = (int) jObject.get("turnIdx"); + storySeed = (int) jObject.get("storySeed"); + + // Not optional, but bug in inkjs means it's actually missing in inkjs saves + Object previousRandomObj = jObject.get("previousRandom"); + if (previousRandomObj != null) { + previousRandom = (int) previousRandomObj; + } else { + previousRandom = 0; + } + } } diff --git a/src/main/java/com/bladecoder/ink/runtime/StringExt.java b/src/main/java/com/bladecoder/ink/runtime/StringExt.java index 2701571..1539ec3 100644 --- a/src/main/java/com/bladecoder/ink/runtime/StringExt.java +++ b/src/main/java/com/bladecoder/ink/runtime/StringExt.java @@ -1,20 +1,18 @@ -package com.bladecoder.ink.runtime; - -import java.util.List; - -public class StringExt { - public static String join(String separator, List RTObjects) throws Exception { - StringBuilder sb = new StringBuilder(); - boolean isFirst = true; - - for (T o : RTObjects) { - if (!isFirst) - sb.append(separator); - - sb.append(o.toString()); - isFirst = false; - } - return sb.toString(); - } - -} +package com.bladecoder.ink.runtime; + +import java.util.List; + +public class StringExt { + public static String join(String separator, List RTObjects) throws Exception { + StringBuilder sb = new StringBuilder(); + boolean isFirst = true; + + for (T o : RTObjects) { + if (!isFirst) sb.append(separator); + + sb.append(o.toString()); + isFirst = false; + } + return sb.toString(); + } +} diff --git a/src/main/java/com/bladecoder/ink/runtime/StringValue.java b/src/main/java/com/bladecoder/ink/runtime/StringValue.java index 4684fe1..7c7c14f 100644 --- a/src/main/java/com/bladecoder/ink/runtime/StringValue.java +++ b/src/main/java/com/bladecoder/ink/runtime/StringValue.java @@ -1,86 +1,84 @@ -package com.bladecoder.ink.runtime; - -public class StringValue extends Value { - private boolean isInlineWhitespace; - - private boolean isNewline; - - public StringValue() { - this(""); - } - - public StringValue(String str) { - super(str); - // Classify whitespace status - setIsNewline("\n".equals(getValue())); - - setIsInlineWhitespace(true); - for (char c : getValue().toCharArray()) { - if (c != ' ' && c != '\t') { - setIsInlineWhitespace(false); - break; - } - - } - } - - @Override - public AbstractValue cast(ValueType newType) throws Exception { - if (newType == getValueType()) { - return this; - } - - if (newType == ValueType.Int) { - try { - int parsedInt = Integer.parseInt(getValue()); - - return new IntValue(parsedInt); - } catch (NumberFormatException e) { - return null; - } - } - - if (newType == ValueType.Float) { - try { - float parsedFloat = Float.parseFloat(getValue()); - - return new FloatValue(parsedFloat); - } catch (NumberFormatException e) { - return null; - } - } - - throw BadCastException (newType); - } - - public boolean isInlineWhitespace() { - return isInlineWhitespace; - } - - public boolean isNewline() { - return isNewline; - } - - public boolean isNonWhitespace() { - return !isNewline() && !isInlineWhitespace(); - } - - @Override - public boolean isTruthy() { - return getValue().length() > 0; - } - - @Override - public ValueType getValueType() { - return ValueType.String; - } - - public void setIsInlineWhitespace(boolean value) { - isInlineWhitespace = value; - } - - public void setIsNewline(boolean value) { - isNewline = value; - } - -} +package com.bladecoder.ink.runtime; + +public class StringValue extends Value { + private boolean isInlineWhitespace; + + private boolean isNewline; + + public StringValue() { + this(""); + } + + public StringValue(String str) { + super(str); + // Classify whitespace status + setIsNewline("\n".equals(getValue())); + + setIsInlineWhitespace(true); + for (char c : getValue().toCharArray()) { + if (c != ' ' && c != '\t') { + setIsInlineWhitespace(false); + break; + } + } + } + + @Override + public AbstractValue cast(ValueType newType) throws Exception { + if (newType == getValueType()) { + return this; + } + + if (newType == ValueType.Int) { + try { + int parsedInt = Integer.parseInt(getValue()); + + return new IntValue(parsedInt); + } catch (NumberFormatException e) { + return null; + } + } + + if (newType == ValueType.Float) { + try { + float parsedFloat = Float.parseFloat(getValue()); + + return new FloatValue(parsedFloat); + } catch (NumberFormatException e) { + return null; + } + } + + throw BadCastException(newType); + } + + public boolean isInlineWhitespace() { + return isInlineWhitespace; + } + + public boolean isNewline() { + return isNewline; + } + + public boolean isNonWhitespace() { + return !isNewline() && !isInlineWhitespace(); + } + + @Override + public boolean isTruthy() { + return getValue().length() > 0; + } + + @Override + public ValueType getValueType() { + return ValueType.String; + } + + public void setIsInlineWhitespace(boolean value) { + isInlineWhitespace = value; + } + + public void setIsNewline(boolean value) { + isNewline = value; + } +} diff --git a/src/main/java/com/bladecoder/ink/runtime/Tag.java b/src/main/java/com/bladecoder/ink/runtime/Tag.java index 77c37d4..cac0806 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; - - public String getText() { - return text; - } - - public Tag(String tagText) { - this.text = tagText; - } - - public String toString() { - return "# " + text; - } -} +package com.bladecoder.ink.runtime; + +public class Tag extends RTObject { + private String text; + + public String getText() { + return text; + } + + public Tag(String tagText) { + this.text = tagText; + } + + public String toString() { + return "# " + text; + } +} diff --git a/src/main/java/com/bladecoder/ink/runtime/Value.java b/src/main/java/com/bladecoder/ink/runtime/Value.java index 13a68d0..9aa8ec6 100644 --- a/src/main/java/com/bladecoder/ink/runtime/Value.java +++ b/src/main/java/com/bladecoder/ink/runtime/Value.java @@ -1,27 +1,27 @@ -package com.bladecoder.ink.runtime; - -public abstract class Value extends AbstractValue { - protected T value; - - public T getValue() { - return value; - } - - public void setValue(T value) { - this.value = value; - } - - @Override - public Object getValueObject() { - return (Object) value; - } - - public Value(T val) { - value = val; - } - - @Override - public String toString() { - return value.toString(); - } -} +package com.bladecoder.ink.runtime; + +public abstract class Value extends AbstractValue { + protected T value; + + public T getValue() { + return value; + } + + public void setValue(T value) { + this.value = value; + } + + @Override + public Object getValueObject() { + return value; + } + + public Value(T val) { + value = val; + } + + @Override + public String toString() { + return value.toString(); + } +} diff --git a/src/main/java/com/bladecoder/ink/runtime/ValueType.java b/src/main/java/com/bladecoder/ink/runtime/ValueType.java index 8f84c7f..05070cb 100644 --- a/src/main/java/com/bladecoder/ink/runtime/ValueType.java +++ b/src/main/java/com/bladecoder/ink/runtime/ValueType.java @@ -1,13 +1,18 @@ -package com.bladecoder.ink.runtime; - -public enum ValueType { - // Order is significant for type coersion. - // If types aren't directly compatible for an operation, - // they're coerced to the same type, downward. - // Higher value types "infect" an operation. - // (This may not be the most sensible thing to do, but it's worked so far!) - // Used in coersion - Int, Float, List, String, - // Not used for coersion described above - DivertTarget, VariablePointer -} +package com.bladecoder.ink.runtime; + +public enum ValueType { + // Order is significant for type coersion. + // If types aren't directly compatible for an operation, + // they're coerced to the same type, downward. + // Higher value types "infect" an operation. + // (This may not be the most sensible thing to do, but it's worked so far!) + // Used in coersion + Bool, + Int, + Float, + List, + String, + // Not used for coersion described above + DivertTarget, + VariablePointer +} diff --git a/src/main/java/com/bladecoder/ink/runtime/VariableAssignment.java b/src/main/java/com/bladecoder/ink/runtime/VariableAssignment.java index 10f944a..7f21e13 100644 --- a/src/main/java/com/bladecoder/ink/runtime/VariableAssignment.java +++ b/src/main/java/com/bladecoder/ink/runtime/VariableAssignment.java @@ -1,51 +1,49 @@ -package com.bladecoder.ink.runtime; - -// The value to be assigned is popped off the evaluation stack, so no need to keep it here -public class VariableAssignment extends RTObject { - private boolean isGlobal; - - private boolean isNewDeclaration; - - private String variableName = new String(); - - // Require default constructor for serialisation - public VariableAssignment() throws Exception { - this(null, false); - } - - public VariableAssignment(String variableName, boolean isNewDeclaration) throws Exception { - this.setVariableName(variableName); - this.setIsNewDeclaration(isNewDeclaration); - } - - public String getVariableName() { - return variableName; - } - - public boolean isGlobal() { - return isGlobal; - } - - public boolean isNewDeclaration() { - return isNewDeclaration; - } - - public void setIsGlobal(boolean value) { - isGlobal = value; - } - - public void setIsNewDeclaration(boolean value) { - isNewDeclaration = value; - } - - public void setVariableName(String value) { - variableName = value; - } - - @Override - public String toString() { - return "VarAssign to " + getVariableName(); - - } - -} +package com.bladecoder.ink.runtime; + +// The value to be assigned is popped off the evaluation stack, so no need to keep it here +public class VariableAssignment extends RTObject { + private boolean isGlobal; + + private boolean isNewDeclaration; + + private String variableName = new String(); + + // Require default constructor for serialisation + public VariableAssignment() throws Exception { + this(null, false); + } + + public VariableAssignment(String variableName, boolean isNewDeclaration) throws Exception { + this.setVariableName(variableName); + this.setIsNewDeclaration(isNewDeclaration); + } + + public String getVariableName() { + return variableName; + } + + public boolean isGlobal() { + return isGlobal; + } + + public boolean isNewDeclaration() { + return isNewDeclaration; + } + + public void setIsGlobal(boolean value) { + isGlobal = value; + } + + public void setIsNewDeclaration(boolean value) { + isNewDeclaration = value; + } + + public void setVariableName(String value) { + variableName = value; + } + + @Override + public String toString() { + return "VarAssign to " + getVariableName(); + } +} diff --git a/src/main/java/com/bladecoder/ink/runtime/VariablePointerValue.java b/src/main/java/com/bladecoder/ink/runtime/VariablePointerValue.java index b9a535e..35bb8bf 100644 --- a/src/main/java/com/bladecoder/ink/runtime/VariablePointerValue.java +++ b/src/main/java/com/bladecoder/ink/runtime/VariablePointerValue.java @@ -1,70 +1,68 @@ -package com.bladecoder.ink.runtime; - -// TODO: Think: Erm, I get that this contains a string, but should -// we really derive from Value? That seems a bit misleading to me. -class VariablePointerValue extends Value { - // Where the variable is located - // -1 = default, unknown, yet to be determined - // 0 = in global scope - // 1+ = callstack element index + 1 (so that the first doesn't conflict with - // special global scope) - private int contextIndex; - - public VariablePointerValue() { - this(null); - } - - public VariablePointerValue(String variableName) { - this(variableName, -1); - } - - public VariablePointerValue(String variableName, int contextIndex) { - super(variableName); - this.setContextIndex(contextIndex); - } - - @Override - public AbstractValue cast(ValueType newType) throws Exception { - if (newType == getValueType()) - return this; - - throw BadCastException (newType); - } - - @Override - RTObject copy() { - return new VariablePointerValue(getVariableName(), getContextIndex()); - } - - public int getContextIndex() { - return contextIndex; - } - - @Override - public ValueType getValueType() { - return ValueType.VariablePointer; - } - - public String getVariableName() { - return this.getValue(); - } - - @Override - public boolean isTruthy() throws Exception { - throw new Exception("Shouldn't be checking the truthiness of a variable pointer"); - } - - public void setContextIndex(int value) { - contextIndex = value; - } - - public void setvariableName(String value) { - this.setValue(value); - } - - @Override - public String toString() { - return "VariablePointerValue(" + getVariableName() + ")"; - } - -} +package com.bladecoder.ink.runtime; + +// TODO: Think: Erm, I get that this contains a string, but should +// we really derive from Value? That seems a bit misleading to me. +class VariablePointerValue extends Value { + // Where the variable is located + // -1 = default, unknown, yet to be determined + // 0 = in global scope + // 1+ = callstack element index + 1 (so that the first doesn't conflict with + // special global scope) + private int contextIndex; + + public VariablePointerValue() { + this(null); + } + + public VariablePointerValue(String variableName) { + this(variableName, -1); + } + + public VariablePointerValue(String variableName, int contextIndex) { + super(variableName); + this.setContextIndex(contextIndex); + } + + @Override + public AbstractValue cast(ValueType newType) throws Exception { + if (newType == getValueType()) return this; + + throw BadCastException(newType); + } + + @Override + RTObject copy() { + return new VariablePointerValue(getVariableName(), getContextIndex()); + } + + public int getContextIndex() { + return contextIndex; + } + + @Override + public ValueType getValueType() { + return ValueType.VariablePointer; + } + + public String getVariableName() { + return this.getValue(); + } + + @Override + public boolean isTruthy() throws Exception { + throw new Exception("Shouldn't be checking the truthiness of a variable pointer"); + } + + public void setContextIndex(int value) { + contextIndex = value; + } + + public void setvariableName(String value) { + this.setValue(value); + } + + @Override + public String toString() { + return "VariablePointerValue(" + getVariableName() + ")"; + } +} diff --git a/src/main/java/com/bladecoder/ink/runtime/VariableReference.java b/src/main/java/com/bladecoder/ink/runtime/VariableReference.java index 19dccee..46f38ad 100644 --- a/src/main/java/com/bladecoder/ink/runtime/VariableReference.java +++ b/src/main/java/com/bladecoder/ink/runtime/VariableReference.java @@ -1,67 +1,61 @@ -package com.bladecoder.ink.runtime; - -public class VariableReference extends RTObject { - // Normal named variable - private String name = new String(); - - // Variable reference is actually a path for a visit (read) count - private Path pathForCount; - - // Require default constructor for serialisation - public VariableReference() { - } - - public VariableReference(String name) { - this.setName(name); - } - - public Container getContainerForCount() throws Exception { - return this.resolvePath(getPathForCount()).getContainer(); - } - - public String getName() { - return name; - } - - public Path getPathForCount() { - return pathForCount; - } - - public String getPathStringForCount() throws Exception { - if (getPathForCount() == null) - return null; - - return compactPathString(getPathForCount()); - } - - public void setName(String value) { - name = value; - } - - public void setPathForCount(Path value) { - pathForCount = value; - } - - public void setPathStringForCount(String value) throws Exception { - if (value == null) - setPathForCount(null); - else - setPathForCount(new Path(value)); - } - - @Override - public String toString() { - try { - if (getName() != null) { - return String.format("var(%s)", getName()); - } else { - String pathStr = getPathStringForCount(); - return String.format("read_count(%s)", pathStr); - } - } catch (Exception e) { - throw new RuntimeException(e); - } - - } - -} +package com.bladecoder.ink.runtime; + +public class VariableReference extends RTObject { + // Normal named variable + private String name = new String(); + + // Variable reference is actually a path for a visit (read) count + private Path pathForCount; + + // Require default constructor for serialisation + public VariableReference() {} + + public VariableReference(String name) { + this.setName(name); + } + + public Container getContainerForCount() throws Exception { + return this.resolvePath(getPathForCount()).getContainer(); + } + + public String getName() { + return name; + } + + public Path getPathForCount() { + return pathForCount; + } + + public String getPathStringForCount() throws Exception { + if (getPathForCount() == null) return null; + + return compactPathString(getPathForCount()); + } + + public void setName(String value) { + name = value; + } + + public void setPathForCount(Path value) { + pathForCount = value; + } + + public void setPathStringForCount(String value) throws Exception { + if (value == null) setPathForCount(null); + else setPathForCount(new Path(value)); + } + + @Override + public String toString() { + try { + if (getName() != null) { + return String.format("var(%s)", getName()); + } else { + String pathStr = getPathStringForCount(); + return String.format("read_count(%s)", pathStr); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/bladecoder/ink/runtime/VariablesState.java b/src/main/java/com/bladecoder/ink/runtime/VariablesState.java index fb5f088..d58af59 100644 --- a/src/main/java/com/bladecoder/ink/runtime/VariablesState.java +++ b/src/main/java/com/bladecoder/ink/runtime/VariablesState.java @@ -1,411 +1,419 @@ -package com.bladecoder.ink.runtime; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Map.Entry; - -/** - * Encompasses all the global variables in an ink Story, and allows binding of a - * VariableChanged event so that that game code can be notified whenever the - * global variables change. - */ -public class VariablesState implements Iterable { - - public static interface VariableChanged { - void variableStateDidChangeEvent(String variableName, RTObject newValue) throws Exception; - } - - private boolean batchObservingVariableChanges; - - // Used for accessing temporary variables - private CallStack callStack; - - private HashSet changedVariablesForBatchObs; - - private HashMap globalVariables; - private HashMap defaultGlobalVariables; - - private VariableChanged variableChangedEvent; - - private ListDefinitionsOrigin listDefsOrigin; - - private StatePatch patch; - - VariablesState(CallStack callStack, ListDefinitionsOrigin listDefsOrigin) { - globalVariables = new HashMap<>(); - this.callStack = callStack; - - this.listDefsOrigin = listDefsOrigin; - } - - CallStack getCallStack() { - return callStack; - } - - void setCallStack(CallStack callStack) { - this.callStack = callStack; - } - - public void assign(VariableAssignment varAss, RTObject value) throws Exception { - String name = varAss.getVariableName(); - int contextIndex = -1; - // Are we assigning to a global variable? - boolean setGlobal = false; - if (varAss.isNewDeclaration()) { - setGlobal = varAss.isGlobal(); - } else { - setGlobal = globalVariableExistsWithName(name); - } - - // Constructing new variable pointer reference - if (varAss.isNewDeclaration()) { - VariablePointerValue varPointer = value instanceof VariablePointerValue ? (VariablePointerValue) value - : (VariablePointerValue) null; - if (varPointer != null) { - VariablePointerValue fullyResolvedVariablePointer = resolveVariablePointer(varPointer); - value = fullyResolvedVariablePointer; - } - - } else { - // Assign to existing variable pointer? - // Then assign to the variable that the pointer is pointing to by - // name. - // De-reference variable reference to point to - VariablePointerValue existingPointer = null; - do { - existingPointer = getRawVariableWithName(name, contextIndex) instanceof VariablePointerValue - ? (VariablePointerValue) getRawVariableWithName(name, contextIndex) - : (VariablePointerValue) null; - if (existingPointer != null) { - name = existingPointer.getVariableName(); - contextIndex = existingPointer.getContextIndex(); - setGlobal = (contextIndex == 0); - } - - } while (existingPointer != null); - } - if (setGlobal) { - setGlobal(name, value); - } else { - callStack.setTemporaryVariable(name, value, varAss.isNewDeclaration(), contextIndex); - } - } - - ListDefinitionsOrigin getLists() { - return listDefsOrigin; - } - - void applyPatch() { - for (Entry namedVar : getPatch().getGlobals().entrySet()) { - globalVariables.put(namedVar.getKey(), namedVar.getValue()); - } - - if (changedVariablesForBatchObs != null) { - for (String name : getPatch().getChangedVariables()) - changedVariablesForBatchObs.add(name); - } - - setPatch(null); - } - - void setJsonToken(HashMap jToken) throws Exception { - globalVariables.clear(); - - for (Entry varVal : defaultGlobalVariables.entrySet()) { - Object loadedToken = jToken.get(varVal.getKey()); - - if (loadedToken != null) { - globalVariables.put(varVal.getKey(), Json.jTokenToRuntimeObject(loadedToken)); - } else { - globalVariables.put(varVal.getKey(), varVal.getValue()); - } - } - } - - /// - /// When saving out JSON state, we can skip saving global values that - /// remain equal to the initial values that were declared in ink. - /// This makes the save file (potentially) much smaller assuming that - /// at least a portion of the globals haven't changed. However, it - /// can also take marginally longer to save in the case that the - /// majority HAVE changed, since it has to compare all globals. - /// It may also be useful to turn this off for testing worst case - /// save timing. - /// - public static boolean dontSaveDefaultValues = true; - - void writeJson(SimpleJson.Writer writer) throws Exception { - writer.writeObjectStart(); - for (Entry keyVal : globalVariables.entrySet()) { - String name = keyVal.getKey(); - RTObject val = keyVal.getValue(); - - if (dontSaveDefaultValues) { - // Don't write out values that are the same as the default global values - RTObject defaultVal = defaultGlobalVariables.get(name); - if (defaultVal != null) { - if (runtimeObjectsEqual(val, defaultVal)) - continue; - } - } - - writer.writePropertyStart(name); - Json.writeRuntimeObject(writer, val); - writer.writePropertyEnd(); - } - writer.writeObjectEnd(); - } - - boolean runtimeObjectsEqual(RTObject obj1, RTObject obj2) throws Exception { - if (obj1.getClass() != obj2.getClass()) - return false; - - // Other Value type (using proper Equals: list, string, divert path) - if (obj1 instanceof Value && obj2 instanceof Value) { - Value val1 = (Value) obj1; - Value val2 = (Value) obj2; - - return val1.getValueObject().equals(val2.getValueObject()); - } - - throw new Exception("FastRoughDefinitelyEquals: Unsupported runtime object type: " + obj1.getClass()); - } - - RTObject tryGetDefaultVariableValue(String name) { - RTObject val = defaultGlobalVariables.get(name); - - return val; - } - - public Object get(String variableName) { - RTObject varContents = (getPatch() != null ? getPatch().getGlobal(variableName) : null); - - if (varContents != null) - return ((Value) varContents).getValueObject(); - - // Search main dictionary first. - // If it's not found, it might be because the story content has changed, - // and the original default value hasn't be instantiated. - // Should really warn somehow, but it's difficult to see how...! - if ((varContents = globalVariables.get(variableName)) != null) { - return ((Value) varContents).getValue(); - } else if ((varContents = defaultGlobalVariables.get(variableName)) != null) { - return ((Value) varContents).getValue(); - } else - return null; - } - - public boolean getbatchObservingVariableChanges() { - return batchObservingVariableChanges; - } - - // Make copy of the variable pointer so we're not using the value direct - // from - // the runtime. Temporary must be local to the current scope. - // 0 if named variable is global - // 1+ if named variable is a temporary in a particular call stack element - int getContextIndexOfVariableNamed(String varName) { - if (globalVariableExistsWithName(varName)) - return 0; - - return callStack.getCurrentElementIndex(); - } - - RTObject getRawVariableWithName(String name, int contextIndex) throws Exception { - RTObject varValue = null; - // 0 context = global - if (contextIndex == 0 || contextIndex == -1) { - if (patch != null && patch.getGlobal(name) != null) - return patch.getGlobal(name); - - varValue = globalVariables.get(name); - if (varValue != null) { - return varValue; - } - - // Getting variables can actually happen during globals set up since you can do - // VAR x = A_LIST_ITEM - // So _defaultGlobalVariables may be null. - // We need to do this check though in case a new global is added, so we need to - // revert to the default globals dictionary since an initial value hasn't yet - // been set. - if (defaultGlobalVariables != null && defaultGlobalVariables.containsKey(name)) { - return defaultGlobalVariables.get(name); - } - - ListValue listItemValue = listDefsOrigin.findSingleItemListWithName(name); - if (listItemValue != null) - return listItemValue; - } - - // Temporary - varValue = callStack.getTemporaryVariableWithName(name, contextIndex); - - return varValue; - } - - void snapshotDefaultGlobals() { - defaultGlobalVariables = new HashMap<>(globalVariables); - } - - public RTObject getVariableWithName(String name) throws Exception { - return getVariableWithName(name, -1); - } - - RTObject getVariableWithName(String name, int contextIndex) throws Exception { - RTObject varValue = getRawVariableWithName(name, contextIndex); - // Get value from pointer? - VariablePointerValue varPointer = varValue instanceof VariablePointerValue ? (VariablePointerValue) varValue - : (VariablePointerValue) null; - if (varPointer != null) { - varValue = valueAtVariablePointer(varPointer); - } - - return varValue; - } - - /** - * Enumerator to allow iteration over all global variables by name. - */ - @Override - public Iterator iterator() { - return globalVariables.keySet().iterator(); - } - - // Given a variable pointer with just the name of the target known, resolve - // to a variable - // pointer that more specifically points to the exact instance: whether it's - // global, - // or the exact position of a temporary on the callstack. - VariablePointerValue resolveVariablePointer(VariablePointerValue varPointer) throws Exception { - int contextIndex = varPointer.getContextIndex(); - if (contextIndex == -1) - contextIndex = getContextIndexOfVariableNamed(varPointer.getVariableName()); - - RTObject valueOfVariablePointedTo = getRawVariableWithName(varPointer.getVariableName(), contextIndex); - // Extra layer of indirection: - // When accessing a pointer to a pointer (e.g. when calling nested or - // recursive functions that take a variable references, ensure we don't - // create - // a chain of indirection by just returning the final target. - VariablePointerValue doubleRedirectionPointer = valueOfVariablePointedTo instanceof VariablePointerValue - ? (VariablePointerValue) valueOfVariablePointedTo - : (VariablePointerValue) null; - if (doubleRedirectionPointer != null) { - return doubleRedirectionPointer; - } else { - return new VariablePointerValue(varPointer.getVariableName(), contextIndex); - } - } - - public void set(String variableName, Object value) throws Exception { - - // This is the main - if (!defaultGlobalVariables.containsKey(variableName)) { - throw new StoryException( - "Cannot assign to a variable (" + variableName + ") that hasn't been declared in the story"); - } - - AbstractValue val = AbstractValue.create(value); - if (val == null) { - if (value == null) { - throw new StoryException("Cannot pass null to VariableState"); - } else { - throw new StoryException("Invalid value passed to VariableState: " + value.toString()); - } - } - - setGlobal(variableName, val); - } - - public void setbatchObservingVariableChanges(boolean value) throws Exception { - batchObservingVariableChanges = value; - if (value) { - changedVariablesForBatchObs = new HashSet<>(); - } else { - if (changedVariablesForBatchObs != null) { - for (String variableName : changedVariablesForBatchObs) { - RTObject currentValue = globalVariables.get(variableName); - getVariableChangedEvent().variableStateDidChangeEvent(variableName, currentValue); - } - } - - changedVariablesForBatchObs = null; - } - } - - void retainListOriginsForAssignment(RTObject oldValue, RTObject newValue) { - ListValue oldList = null; - - if (oldValue instanceof ListValue) - oldList = (ListValue) oldValue; - - ListValue newList = null; - - if (newValue instanceof ListValue) - newList = (ListValue) newValue; - - if (oldList != null && newList != null && newList.value.size() == 0) - newList.value.setInitialOriginNames(oldList.value.getOriginNames()); - } - - void setGlobal(String variableName, RTObject value) throws Exception { - RTObject oldValue = null; - - if (patch != null) - oldValue = patch.getGlobal(variableName); - - if (oldValue == null) - oldValue = globalVariables.get(variableName); - - ListValue.retainListOriginsForAssignment(oldValue, value); - - if (patch != null) - patch.setGlobal(variableName, value); - else - globalVariables.put(variableName, value); - - if (getVariableChangedEvent() != null && !value.equals(oldValue)) { - - if (getbatchObservingVariableChanges()) { - if (patch != null) - patch.addChangedVariable(variableName); - else if (changedVariablesForBatchObs != null) - changedVariablesForBatchObs.add(variableName); - } else { - getVariableChangedEvent().variableStateDidChangeEvent(variableName, value); - } - } - - } - - public void setjsonToken(HashMap value) throws Exception { - globalVariables = Json.jObjectToHashMapRuntimeObjs(value); - } - - public RTObject valueAtVariablePointer(VariablePointerValue pointer) throws Exception { - return getVariableWithName(pointer.getVariableName(), pointer.getContextIndex()); - } - - public VariableChanged getVariableChangedEvent() { - return variableChangedEvent; - } - - public void setVariableChangedEvent(VariableChanged variableChangedEvent) { - this.variableChangedEvent = variableChangedEvent; - } - - boolean globalVariableExistsWithName(String name) { - return globalVariables.containsKey(name) - || (defaultGlobalVariables != null && defaultGlobalVariables.containsKey(name)); - } - - public StatePatch getPatch() { - return patch; - } - - public void setPatch(StatePatch patch) { - this.patch = patch; - } -} +package com.bladecoder.ink.runtime; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map.Entry; + +/** + * Encompasses all the global variables in an ink Story, and allows binding of a + * VariableChanged event so that that game code can be notified whenever the + * global variables change. + */ +public class VariablesState implements Iterable { + + public interface VariableChanged { + void variableStateDidChangeEvent(String variableName, RTObject newValue) throws Exception; + } + + private boolean batchObservingVariableChanges; + + // Used for accessing temporary variables + private CallStack callStack; + + private HashSet changedVariablesForBatchObs; + + private HashMap globalVariables; + private HashMap defaultGlobalVariables; + + private VariableChanged variableChangedEvent; + + private ListDefinitionsOrigin listDefsOrigin; + + private StatePatch patch; + + VariablesState(CallStack callStack, ListDefinitionsOrigin listDefsOrigin) { + globalVariables = new HashMap<>(); + this.callStack = callStack; + + this.listDefsOrigin = listDefsOrigin; + } + + void setCallStack(CallStack callStack) { + this.callStack = callStack; + } + + public void assign(VariableAssignment varAss, RTObject value) throws Exception { + String name = varAss.getVariableName(); + int contextIndex = -1; + // Are we assigning to a global variable? + boolean setGlobal = false; + if (varAss.isNewDeclaration()) { + setGlobal = varAss.isGlobal(); + } else { + setGlobal = globalVariableExistsWithName(name); + } + + // Constructing new variable pointer reference + if (varAss.isNewDeclaration()) { + VariablePointerValue varPointer = + value instanceof VariablePointerValue ? (VariablePointerValue) value : (VariablePointerValue) null; + if (varPointer != null) { + VariablePointerValue fullyResolvedVariablePointer = resolveVariablePointer(varPointer); + value = fullyResolvedVariablePointer; + } + + } else { + // Assign to existing variable pointer? + // Then assign to the variable that the pointer is pointing to by + // name. + // De-reference variable reference to point to + VariablePointerValue existingPointer; + do { + existingPointer = getRawVariableWithName(name, contextIndex) instanceof VariablePointerValue + ? (VariablePointerValue) getRawVariableWithName(name, contextIndex) + : (VariablePointerValue) null; + if (existingPointer != null) { + name = existingPointer.getVariableName(); + contextIndex = existingPointer.getContextIndex(); + setGlobal = (contextIndex == 0); + } + + } while (existingPointer != null); + } + if (setGlobal) { + setGlobal(name, value); + } else { + callStack.setTemporaryVariable(name, value, varAss.isNewDeclaration(), contextIndex); + } + } + + void applyPatch() { + for (Entry namedVar : getPatch().getGlobals().entrySet()) { + globalVariables.put(namedVar.getKey(), namedVar.getValue()); + } + + if (changedVariablesForBatchObs != null) { + for (String name : getPatch().getChangedVariables()) changedVariablesForBatchObs.add(name); + } + + setPatch(null); + } + + void setJsonToken(HashMap jToken) throws Exception { + globalVariables.clear(); + + for (Entry varVal : defaultGlobalVariables.entrySet()) { + Object loadedToken = jToken.get(varVal.getKey()); + + if (loadedToken != null) { + globalVariables.put(varVal.getKey(), Json.jTokenToRuntimeObject(loadedToken)); + } else { + globalVariables.put(varVal.getKey(), varVal.getValue()); + } + } + } + + /// + /// When saving out JSON state, we can skip saving global values that + /// remain equal to the initial values that were declared in ink. + /// This makes the save file (potentially) much smaller assuming that + /// at least a portion of the globals haven't changed. However, it + /// can also take marginally longer to save in the case that the + /// majority HAVE changed, since it has to compare all globals. + /// It may also be useful to turn this off for testing worst case + /// save timing. + /// + public static boolean dontSaveDefaultValues = true; + + void writeJson(SimpleJson.Writer writer) throws Exception { + writer.writeObjectStart(); + for (Entry keyVal : globalVariables.entrySet()) { + String name = keyVal.getKey(); + RTObject val = keyVal.getValue(); + + if (dontSaveDefaultValues) { + // Don't write out values that are the same as the default global values + RTObject defaultVal = defaultGlobalVariables != null ? defaultGlobalVariables.get(name) : null; + if (defaultVal != null) { + if (runtimeObjectsEqual(val, defaultVal)) continue; + } + } + + writer.writePropertyStart(name); + Json.writeRuntimeObject(writer, val); + writer.writePropertyEnd(); + } + writer.writeObjectEnd(); + } + + boolean runtimeObjectsEqual(RTObject obj1, RTObject obj2) throws Exception { + if (obj1.getClass() != obj2.getClass()) return false; + + // Perform equality on int/float/bool manually to avoid boxing + if (obj1 instanceof BoolValue) { + BoolValue boolVal = (BoolValue) obj1; + return boolVal.value == ((BoolValue) obj2).value; + } + + if (obj1 instanceof IntValue) { + IntValue intVal = (IntValue) obj1; + return intVal.value == ((IntValue) obj2).value; + } + + if (obj1 instanceof FloatValue) { + FloatValue floatVal = (FloatValue) obj1; + return floatVal.value == ((FloatValue) obj2).value; + } + + // Other Value type (using proper Equals: list, string, divert path) + if (obj1 instanceof Value) { + Value val1 = (Value) obj1; + Value val2 = (Value) obj2; + + return val1.getValueObject().equals(val2.getValueObject()); + } + + throw new Exception("FastRoughDefinitelyEquals: Unsupported runtime object type: " + obj1.getClass()); + } + + RTObject tryGetDefaultVariableValue(String name) { + RTObject val = defaultGlobalVariables.get(name); + + return val; + } + + public Object get(String variableName) { + RTObject varContents = (getPatch() != null ? getPatch().getGlobal(variableName) : null); + + if (varContents != null) return ((Value) varContents).getValueObject(); + + // Search main dictionary first. + // If it's not found, it might be because the story content has changed, + // and the original default value hasn't be instantiated. + // Should really warn somehow, but it's difficult to see how...! + if ((varContents = globalVariables.get(variableName)) != null) { + return ((Value) varContents).getValue(); + } else if ((varContents = defaultGlobalVariables.get(variableName)) != null) { + return ((Value) varContents).getValue(); + } else return null; + } + + public void startVariableObservation() { + batchObservingVariableChanges = true; + changedVariablesForBatchObs = new HashSet<>(); + } + + public HashMap completeVariableObservation() { + batchObservingVariableChanges = false; + + HashMap changedVars = new HashMap<>(); + if (changedVariablesForBatchObs != null) { + for (String variableName : changedVariablesForBatchObs) { + RTObject currentValue = globalVariables.get(variableName); + changedVars.put(variableName, currentValue); + } + } + + // Patch may still be active - e.g. if we were in the middle of a background save + if (patch != null) { + for (String variableName : patch.getChangedVariables()) { + RTObject patchedVal = patch.getGlobal(variableName); + + if (patchedVal != null) { + changedVars.put(variableName, patchedVal); + } + } + } + + changedVariablesForBatchObs = null; + return changedVars; + } + + public void notifyObservers(HashMap changedVars) throws Exception { + for (Entry varToVal : changedVars.entrySet()) { + variableChangedEvent.variableStateDidChangeEvent(varToVal.getKey(), varToVal.getValue()); + } + } + + // Make copy of the variable pointer so we're not using the value direct + // from + // the runtime. Temporary must be local to the current scope. + // 0 if named variable is global + // 1+ if named variable is a temporary in a particular call stack element + int getContextIndexOfVariableNamed(String varName) { + if (globalVariableExistsWithName(varName)) return 0; + + return callStack.getCurrentElementIndex(); + } + + RTObject getRawVariableWithName(String name, int contextIndex) { + RTObject varValue = null; + // 0 context = global + if (contextIndex == 0 || contextIndex == -1) { + if (patch != null && patch.getGlobal(name) != null) return patch.getGlobal(name); + + varValue = globalVariables.get(name); + if (varValue != null) { + return varValue; + } + + // Getting variables can actually happen during globals set up since you can do + // VAR x = A_LIST_ITEM + // So _defaultGlobalVariables may be null. + // We need to do this check though in case a new global is added, so we need to + // revert to the default globals dictionary since an initial value hasn't yet + // been set. + if (defaultGlobalVariables != null && defaultGlobalVariables.containsKey(name)) { + return defaultGlobalVariables.get(name); + } + + ListValue listItemValue = listDefsOrigin.findSingleItemListWithName(name); + if (listItemValue != null) return listItemValue; + } + + // Temporary + varValue = callStack.getTemporaryVariableWithName(name, contextIndex); + + return varValue; + } + + void snapshotDefaultGlobals() { + defaultGlobalVariables = new HashMap<>(globalVariables); + } + + public RTObject getVariableWithName(String name) throws Exception { + return getVariableWithName(name, -1); + } + + RTObject getVariableWithName(String name, int contextIndex) throws Exception { + RTObject varValue = getRawVariableWithName(name, contextIndex); + // Get value from pointer? + VariablePointerValue varPointer = varValue instanceof VariablePointerValue + ? (VariablePointerValue) varValue + : (VariablePointerValue) null; + if (varPointer != null) { + varValue = valueAtVariablePointer(varPointer); + } + + return varValue; + } + + /** + * Enumerator to allow iteration over all global variables by name. + */ + @Override + public Iterator iterator() { + return globalVariables.keySet().iterator(); + } + + // Given a variable pointer with just the name of the target known, resolve + // to a variable + // pointer that more specifically points to the exact instance: whether it's + // global, + // or the exact position of a temporary on the callstack. + VariablePointerValue resolveVariablePointer(VariablePointerValue varPointer) throws Exception { + int contextIndex = varPointer.getContextIndex(); + if (contextIndex == -1) contextIndex = getContextIndexOfVariableNamed(varPointer.getVariableName()); + + RTObject valueOfVariablePointedTo = getRawVariableWithName(varPointer.getVariableName(), contextIndex); + // Extra layer of indirection: + // When accessing a pointer to a pointer (e.g. when calling nested or + // recursive functions that take a variable references, ensure we don't + // create + // a chain of indirection by just returning the final target. + VariablePointerValue doubleRedirectionPointer = valueOfVariablePointedTo instanceof VariablePointerValue + ? (VariablePointerValue) valueOfVariablePointedTo + : (VariablePointerValue) null; + if (doubleRedirectionPointer != null) { + return doubleRedirectionPointer; + } else { + return new VariablePointerValue(varPointer.getVariableName(), contextIndex); + } + } + + public void set(String variableName, Object value) throws Exception { + + // This is the main + if (!defaultGlobalVariables.containsKey(variableName)) { + throw new StoryException( + "Cannot assign to a variable (" + variableName + ") that hasn't been declared in the story"); + } + + AbstractValue val = AbstractValue.create(value); + if (val == null) { + if (value == null) { + throw new Exception("Cannot pass null to VariableState"); + } else { + throw new Exception("Invalid value passed to VariableState: " + value.toString()); + } + } + + setGlobal(variableName, val); + } + + void retainListOriginsForAssignment(RTObject oldValue, RTObject newValue) { + ListValue oldList = null; + + if (oldValue instanceof ListValue) oldList = (ListValue) oldValue; + + ListValue newList = null; + + if (newValue instanceof ListValue) newList = (ListValue) newValue; + + if (oldList != null && newList != null && newList.value.size() == 0) + newList.value.setInitialOriginNames(oldList.value.getOriginNames()); + } + + void setGlobal(String variableName, RTObject value) throws Exception { + RTObject oldValue = null; + + if (patch != null) oldValue = patch.getGlobal(variableName); + + if (oldValue == null) oldValue = globalVariables.get(variableName); + + ListValue.retainListOriginsForAssignment(oldValue, value); + + if (patch != null) patch.setGlobal(variableName, value); + else globalVariables.put(variableName, value); + + if (getVariableChangedEvent() != null && !value.equals(oldValue)) { + + if (batchObservingVariableChanges) { + if (patch != null) patch.addChangedVariable(variableName); + else if (changedVariablesForBatchObs != null) changedVariablesForBatchObs.add(variableName); + } else { + getVariableChangedEvent().variableStateDidChangeEvent(variableName, value); + } + } + } + + public void setjsonToken(HashMap value) throws Exception { + globalVariables = Json.jObjectToHashMapRuntimeObjs(value); + } + + public RTObject valueAtVariablePointer(VariablePointerValue pointer) throws Exception { + return getVariableWithName(pointer.getVariableName(), pointer.getContextIndex()); + } + + public VariableChanged getVariableChangedEvent() { + return variableChangedEvent; + } + + public void setVariableChangedEvent(VariableChanged variableChangedEvent) { + this.variableChangedEvent = variableChangedEvent; + } + + boolean globalVariableExistsWithName(String name) { + return globalVariables.containsKey(name) + || (defaultGlobalVariables != null && defaultGlobalVariables.containsKey(name)); + } + + StatePatch getPatch() { + return patch; + } + + void setPatch(StatePatch patch) { + this.patch = patch; + } +} diff --git a/src/main/java/com/bladecoder/ink/runtime/Void.java b/src/main/java/com/bladecoder/ink/runtime/Void.java index ffa0c40..bdf660c 100644 --- a/src/main/java/com/bladecoder/ink/runtime/Void.java +++ b/src/main/java/com/bladecoder/ink/runtime/Void.java @@ -1,8 +1,5 @@ -package com.bladecoder.ink.runtime; - -import com.bladecoder.ink.runtime.RTObject; - -public class Void extends RTObject { - public Void() { - } -} +package com.bladecoder.ink.runtime; + +public class Void extends RTObject { + public Void() {} +} diff --git a/src/test/java/com/bladecoder/ink/runtime/test/BasicTextSpecTest.java b/src/test/java/com/bladecoder/ink/runtime/test/BasicTextSpecTest.java index c885ec8..982015e 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/BasicTextSpecTest.java +++ b/src/test/java/com/bladecoder/ink/runtime/test/BasicTextSpecTest.java @@ -1,43 +1,41 @@ package com.bladecoder.ink.runtime.test; +import com.bladecoder.ink.runtime.Story; import java.util.ArrayList; import java.util.List; - import org.junit.Assert; import org.junit.Test; -import com.bladecoder.ink.runtime.Story; - public class BasicTextSpecTest { - - /** - * The more simple ink file, one line of text. - */ - @Test - public void oneline() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/basictext/oneline.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("Line.", text.get(0)); - } - - /** - * Two lines of text. - */ - @Test - public void twolines() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/basictext/twolines.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(2, text.size()); - Assert.assertEquals("Line.", text.get(0)); - Assert.assertEquals("Other line.", text.get(1)); - } + + /** + * The more simple ink file, one line of text. + */ + @Test + public void oneline() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/basictext/oneline.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("Line.", text.get(0)); + } + + /** + * Two lines of text. + */ + @Test + public void twolines() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/basictext/twolines.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(2, text.size()); + Assert.assertEquals("Line.", text.get(0)); + Assert.assertEquals("Other line.", text.get(1)); + } } diff --git a/src/test/java/com/bladecoder/ink/runtime/test/ChoiceSpecTest.java b/src/test/java/com/bladecoder/ink/runtime/test/ChoiceSpecTest.java index 86fa9ba..b364721 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/ChoiceSpecTest.java +++ b/src/test/java/com/bladecoder/ink/runtime/test/ChoiceSpecTest.java @@ -1,327 +1,333 @@ package com.bladecoder.ink.runtime.test; +import com.bladecoder.ink.runtime.Story; import java.util.ArrayList; import java.util.Arrays; import java.util.List; - import org.junit.Assert; import org.junit.Test; -import com.bladecoder.ink.runtime.Story; - public class ChoiceSpecTest { - @Test - public void noChoice() throws Exception { - List errors = new ArrayList(); + @Test + public void noChoice() throws Exception { + List errors = new ArrayList(); - List text = TestUtils.runStory("inkfiles/choices/no-choice-text.ink.json", null, errors); + List text = TestUtils.runStory("inkfiles/choices/no-choice-text.ink.json", null, errors); - Assert.assertEquals(0, errors.size()); - Assert.assertEquals("Hello world!\nHello back!\n", TestUtils.joinText(text)); - } + Assert.assertEquals(0, errors.size()); + Assert.assertEquals("Hello world!\nHello back!\n", TestUtils.joinText(text)); + } - @Test - public void one() throws Exception { - List errors = new ArrayList(); + @Test + public void one() throws Exception { + List errors = new ArrayList(); - List text = TestUtils.runStory("inkfiles/choices/one.ink.json", null, errors); + List text = TestUtils.runStory("inkfiles/choices/one.ink.json", null, errors); - Assert.assertEquals(0, errors.size()); - Assert.assertEquals("Hello world!\nHello back!\nHello back!\n", TestUtils.joinText(text)); - } + Assert.assertEquals(0, errors.size()); + Assert.assertEquals("Hello world!\nHello back!\nHello back!\n", TestUtils.joinText(text)); + } - @Test - public void multiChoice() throws Exception { - List errors = new ArrayList(); + @Test + public void multiChoice() throws Exception { + List errors = new ArrayList(); - List text = TestUtils.runStory("inkfiles/choices/multi-choice.ink.json", Arrays.asList(0), errors); + List text = TestUtils.runStory("inkfiles/choices/multi-choice.ink.json", Arrays.asList(0), errors); - Assert.assertEquals(0, errors.size()); - Assert.assertEquals("Hello, world!\nHello back!\nGoodbye\nHello back!\nNice to hear from you\n", - TestUtils.joinText(text)); + Assert.assertEquals(0, errors.size()); + Assert.assertEquals( + "Hello, world!\nHello back!\nGoodbye\nHello back!\nNice to hear from you\n", TestUtils.joinText(text)); - // Select second choice - text = TestUtils.runStory("inkfiles/choices/multi-choice.ink.json", Arrays.asList(1), errors); + // Select second choice + text = TestUtils.runStory("inkfiles/choices/multi-choice.ink.json", Arrays.asList(1), errors); - Assert.assertEquals(0, errors.size()); - Assert.assertEquals("Hello, world!\nHello back!\nGoodbye\nGoodbye\nSee you later\n", TestUtils.joinText(text)); - } + Assert.assertEquals(0, errors.size()); + Assert.assertEquals("Hello, world!\nHello back!\nGoodbye\nGoodbye\nSee you later\n", TestUtils.joinText(text)); + } - /** - * "- demarcate end of text for parent container" - */ - @Test - public void singleChoice1() throws Exception { - List text = new ArrayList(); + /** + * "- demarcate end of text for parent container" + */ + @Test + public void singleChoice1() throws Exception { + List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/choices/single-choice.ink.json"); - Story story = new Story(json); + String json = TestUtils.getJsonString("inkfiles/choices/single-choice.ink.json"); + Story story = new Story(json); - TestUtils.nextAll(story, text); + TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("Hello, world!", text.get(0)); - } + Assert.assertEquals(1, text.size()); + Assert.assertEquals("Hello, world!", text.get(0)); + } - /** - * "- continue processing with the choice text when a choice is selected" - */ - @Test - public void singleChoice2() throws Exception { - List text = new ArrayList(); + /** + * "- continue processing with the choice text when a choice is selected" + */ + @Test + public void singleChoice2() throws Exception { + List text = new ArrayList(); - String json = TestUtils.getJsonString("inkfiles/choices/single-choice.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - story.chooseChoiceIndex(0); + String json = TestUtils.getJsonString("inkfiles/choices/single-choice.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + story.chooseChoiceIndex(0); - text.clear(); - TestUtils.nextAll(story, text); + text.clear(); + TestUtils.nextAll(story, text); - Assert.assertEquals(2, text.size()); - Assert.assertEquals("Hello back!", text.get(0)); - Assert.assertEquals("Nice to hear from you", text.get(1)); - } + Assert.assertEquals(2, text.size()); + Assert.assertEquals("Hello back!", text.get(0)); + Assert.assertEquals("Nice to hear from you", text.get(1)); + } - /** - * "- be suppressed in the text flow using the [] syntax" - */ - @Test - public void suppressChoice() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/choices/suppress-choice.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals("Hello back!", story.getCurrentChoices().get(0).getText()); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - - Assert.assertEquals(1, text.size()); - Assert.assertEquals("Nice to hear from you.", text.get(0)); - } - - /** - * "- be suppressed in the text flow using the [] syntax" - */ - @Test - public void mixedChoice() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/choices/mixed-choice.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals("Hello back!", story.getCurrentChoices().get(0).getText()); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(2, text.size()); - - Assert.assertEquals("Hello right back to you!", text.get(0)); - Assert.assertEquals("Nice to hear from you.", text.get(1)); - } - - /** - * "- disappear when used if they are a once-only choice" - */ - @Test - public void varyingChoice() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/choices/varying-choice.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(2, story.getCurrentChoices().size()); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, story.getCurrentChoices().size()); - - Assert.assertEquals("The man with the briefcase?", story.getCurrentChoices().get(0).getText()); - } - - /** - * "- not disappear when used if they are a sticky choices" - */ - @Test - public void stickyChoice() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/choices/sticky-choice.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(2, story.getCurrentChoices().size()); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(2, story.getCurrentChoices().size()); - } - - /** - * "- not be shown if it is a fallback choice and there are non-fallback - * choices available" - */ - @Test - public void fallbackChoice() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/choices/fallback-choice.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(2, story.getCurrentChoices().size()); - } - - /** - * "- not be shown if it is a fallback choice and there are non-fallback - * choices available" - */ - @Test - public void fallbackChoice2() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/choices/fallback-choice.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(2, story.getCurrentChoices().size()); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - - Assert.assertEquals(true, TestUtils.isEnded(story)); - } - - /** - * "- not be visible if their conditions evaluate to 0" - */ - @Test - public void conditionalChoice() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/choices/conditional-choice.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - - Assert.assertEquals(4, story.getCurrentChoices().size()); - } - - /** - * "- handle labels on choices and evaluate in expressions (example 1)" - */ - @Test - public void labelFlow() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/choices/label-flow.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(2, story.getCurrentChoices().size()); - Assert.assertEquals("\'Having a nice day?\'", story.getCurrentChoices().get(0).getText()); - } - - /** - * "- handle labels on choices and evaluate in expressions (example 2)" - */ - @Test - public void labelFlow2() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/choices/label-flow.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - story.chooseChoiceIndex(1); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(2, story.getCurrentChoices().size()); - Assert.assertEquals("Shove him aside", story.getCurrentChoices().get(1).getText()); - } - - /** - * "- allow label references out of scope using the full path id" - */ - @Test - public void labelScope() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/choices/label-scope.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, story.getCurrentChoices().size()); - Assert.assertEquals("Found gatherpoint", story.getCurrentChoices().get(0).getText()); - } - - /** - * "- fail label references that are out of scope" - * - * NOTE: Label is found in ref. impl. and in blade-ink. It must fail? - */ - @Test - public void labelScopeError() throws Exception { -// List text = new ArrayList(); -// -// String json = TestUtils.getJsonString("inkfiles/choices/label-scope-error.ink.json"); -// Story story = new Story(json); -// -// try { -// TestUtils.nextAll(story, text); -// story.chooseChoiceIndex(0); -// Assert.fail(); -// } catch (Exception e) { -// -// } - - } - - /** - * "- be used up if they are once-only and a divert goes through them" - */ - @Test - public void divertChoice() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/choices/divert-choice.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(2, story.getCurrentChoices().size()); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(2, text.size()); - Assert.assertEquals("You pull a face, and the soldier comes at you! You shove the guard to one side, but he comes back swinging.", text.get(0)); - - Assert.assertEquals(1, story.getCurrentChoices().size()); - Assert.assertEquals("Grapple and fight", story.getCurrentChoices().get(0).getText()); - } + /** + * "- be suppressed in the text flow using the [] syntax" + */ + @Test + public void suppressChoice() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/choices/suppress-choice.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals("Hello back!", story.getCurrentChoices().get(0).getText()); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + + Assert.assertEquals(1, text.size()); + Assert.assertEquals("Nice to hear from you.", text.get(0)); + } + + /** + * "- be suppressed in the text flow using the [] syntax" + */ + @Test + public void mixedChoice() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/choices/mixed-choice.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals("Hello back!", story.getCurrentChoices().get(0).getText()); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(2, text.size()); + + Assert.assertEquals("Hello right back to you!", text.get(0)); + Assert.assertEquals("Nice to hear from you.", text.get(1)); + } + + /** + * "- disappear when used if they are a once-only choice" + */ + @Test + public void varyingChoice() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/choices/varying-choice.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(2, story.getCurrentChoices().size()); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, story.getCurrentChoices().size()); + + Assert.assertEquals( + "The man with the briefcase?", story.getCurrentChoices().get(0).getText()); + } + + /** + * "- not disappear when used if they are a sticky choices" + */ + @Test + public void stickyChoice() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/choices/sticky-choice.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(2, story.getCurrentChoices().size()); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(2, story.getCurrentChoices().size()); + } + + /** + * "- not be shown if it is a fallback choice and there are non-fallback + * choices available" + */ + @Test + public void fallbackChoice() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/choices/fallback-choice.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(2, story.getCurrentChoices().size()); + } + + /** + * "- not be shown if it is a fallback choice and there are non-fallback + * choices available" + */ + @Test + public void fallbackChoice2() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/choices/fallback-choice.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(2, story.getCurrentChoices().size()); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + + Assert.assertEquals(true, TestUtils.isEnded(story)); + } + + /** + * "- not be visible if their conditions evaluate to 0" + */ + @Test + public void conditionalChoice() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/choices/conditional-choice.ink.json"); + Story story = new Story(json); + + System.out.println(story.buildStringOfHierarchy()); + + TestUtils.nextAll(story, text); + + Assert.assertEquals(4, story.getCurrentChoices().size()); + } + + /** + * "- handle labels on choices and evaluate in expressions (example 1)" + */ + @Test + public void labelFlow() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/choices/label-flow.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(2, story.getCurrentChoices().size()); + Assert.assertEquals( + "\'Having a nice day?\'", story.getCurrentChoices().get(0).getText()); + } + + /** + * "- handle labels on choices and evaluate in expressions (example 2)" + */ + @Test + public void labelFlow2() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/choices/label-flow.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + story.chooseChoiceIndex(1); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(2, story.getCurrentChoices().size()); + Assert.assertEquals("Shove him aside", story.getCurrentChoices().get(1).getText()); + } + + /** + * "- allow label references out of scope using the full path id" + */ + @Test + public void labelScope() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/choices/label-scope.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, story.getCurrentChoices().size()); + Assert.assertEquals( + "Found gatherpoint", story.getCurrentChoices().get(0).getText()); + } + + /** + * "- fail label references that are out of scope" + * + * NOTE: Label is found in ref. impl. and in blade-ink. It must fail? + */ + @Test + public void labelScopeError() throws Exception { + // List text = new ArrayList(); + // + // String json = TestUtils.getJsonString("inkfiles/choices/label-scope-error.ink.json"); + // Story story = new Story(json); + // + // try { + // TestUtils.nextAll(story, text); + // story.chooseChoiceIndex(0); + // Assert.fail(); + // } catch (Exception e) { + // + // } + + } + + /** + * "- be used up if they are once-only and a divert goes through them" + */ + @Test + public void divertChoice() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/choices/divert-choice.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(2, story.getCurrentChoices().size()); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(2, text.size()); + Assert.assertEquals( + "You pull a face, and the soldier comes at you! You shove the guard to one side, but he comes back swinging.", + text.get(0)); + + Assert.assertEquals(1, story.getCurrentChoices().size()); + Assert.assertEquals( + "Grapple and fight", story.getCurrentChoices().get(0).getText()); + } } diff --git a/src/test/java/com/bladecoder/ink/runtime/test/ConditionalSpecTest.java b/src/test/java/com/bladecoder/ink/runtime/test/ConditionalSpecTest.java index fa05376..1d301b9 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/ConditionalSpecTest.java +++ b/src/test/java/com/bladecoder/ink/runtime/test/ConditionalSpecTest.java @@ -1,494 +1,494 @@ package com.bladecoder.ink.runtime.test; +import com.bladecoder.ink.runtime.Story; import java.util.ArrayList; import java.util.List; - import org.junit.Assert; import org.junit.Test; -import com.bladecoder.ink.runtime.Story; - public class ConditionalSpecTest { - /** - * "- evaluate the statements if the condition evaluates to true" - */ - @Test - public void ifTrue() throws Exception { - List text = new ArrayList<>(); - - String json = TestUtils.getJsonString("inkfiles/conditional/iftrue.ink.json"); - Story story = new Story(json); + /** + * "- evaluate the statements if the condition evaluates to true" + */ + @Test + public void ifTrue() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/conditional/iftrue.ink.json"); + Story story = new Story(json); + + System.out.println(story.buildStringOfHierarchy()); + + TestUtils.nextAll(story, text); + + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The value is 1.", text.get(0)); + } + + /** + * "- not evaluate the statement if the condition evaluates to false" + */ + @Test + public void ifFalse() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/conditional/iffalse.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); - TestUtils.nextAll(story, text); - - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The value is 1.", text.get(0)); - } - - /** - * "- not evaluate the statement if the condition evaluates to false" - */ - @Test - public void ifFalse() throws Exception { - List text = new ArrayList<>(); - - String json = TestUtils.getJsonString("inkfiles/conditional/iffalse.ink.json"); - Story story = new Story(json); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The value is 3.", text.get(0)); + } - TestUtils.nextAll(story, text); - - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The value is 3.", text.get(0)); - } - - /** - * "- evaluate an else statement if it exists and no other condition evaluates - * to true" - */ - @Test - public void ifElse() throws Exception { - List text = new ArrayList<>(); - - String json = TestUtils.getJsonString("inkfiles/conditional/ifelse.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The value is 1.", text.get(0)); - } - - /** - * "- evaluate an extended else statement if it exists and no other condition - * evaluates to true" - */ - @Test - public void ifElseExt() throws Exception { - List text = new ArrayList<>(); - - String json = TestUtils.getJsonString("inkfiles/conditional/ifelse-ext.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The value is -1.", text.get(0)); - } - - /** - * "- evaluate an extended else statement with text and divert at the end" - */ - @Test - public void ifElseExtText1() throws Exception { - List text = new ArrayList<>(); - - String json = TestUtils.getJsonString("inkfiles/conditional/ifelse-ext-text1.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - - Assert.assertEquals(1, text.size()); - Assert.assertEquals("This is text 1.", text.get(0)); - - Assert.assertEquals(1, story.getCurrentChoices().size()); - story.chooseChoiceIndex(0); - TestUtils.nextAll(story, text); - Assert.assertEquals(2, text.size()); - Assert.assertEquals("This is the end.", text.get(1)); - } - - /** - * "- evaluate an extended else statement with text and divert at the end" - */ - @Test - public void ifElseExtText2() throws Exception { - List text = new ArrayList<>(); - - String json = TestUtils.getJsonString("inkfiles/conditional/ifelse-ext-text2.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - - Assert.assertEquals(1, text.size()); - Assert.assertEquals("This is text 2.", text.get(0)); - - Assert.assertEquals(1, story.getCurrentChoices().size()); - story.chooseChoiceIndex(0); - TestUtils.nextAll(story, text); - Assert.assertEquals(2, text.size()); - Assert.assertEquals("This is the end.", text.get(1)); - } - - /** - * "- evaluate an extended else statement with text and divert at the end" - */ - @Test - public void ifElseExtText3() throws Exception { - List text = new ArrayList<>(); - - String json = TestUtils.getJsonString("inkfiles/conditional/ifelse-ext-text3.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - - Assert.assertEquals(1, text.size()); - Assert.assertEquals("This is text 3.", text.get(0)); - - Assert.assertEquals(1, story.getCurrentChoices().size()); - story.chooseChoiceIndex(0); - TestUtils.nextAll(story, text); - Assert.assertEquals(2, text.size()); - Assert.assertEquals("This is the end.", text.get(1)); - } - - /** - * "- work with conditional content which is not only logic (example 1)" - */ - @Test - public void condText1() throws Exception { - List text = new ArrayList<>(); - - String json = TestUtils.getJsonString("inkfiles/conditional/condtext.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(3, text.size()); - Assert.assertEquals("I stared at Monsieur Fogg. \"But surely you are not serious?\" I demanded.", text.get(1)); - } - - /** - * "- work with conditional content which is not only logic (example 2)" - */ - @Test - public void condText2() throws Exception { - List text = new ArrayList<>(); - - String json = TestUtils.getJsonString("inkfiles/conditional/condtext.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - story.chooseChoiceIndex(1); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(2, text.size()); - Assert.assertEquals("I stared at Monsieur Fogg. \"But there must be a reason for this trip,\" I observed.", - text.get(0)); - } - - /** - * "- work with options as conditional content (example 1)" - */ - @Test - public void condOpt1() throws Exception { - List text = new ArrayList<>(); - - String json = TestUtils.getJsonString("inkfiles/conditional/condopt.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, story.getCurrentChoices().size()); - } - - /** - * "- work with options as conditional content (example 2)" - */ - @Test - public void condOpt2() throws Exception { - List text = new ArrayList<>(); - - String json = TestUtils.getJsonString("inkfiles/conditional/condopt.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - story.chooseChoiceIndex(1); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(2, story.getCurrentChoices().size()); - } - - /** - * "- go through the alternatives and stick on last when the keyword is - * stopping" - */ - @Test - public void stopping() throws Exception { - List text = new ArrayList<>(); - - String json = TestUtils.getJsonString("inkfiles/conditional/stopping.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("I entered the casino.", text.get(0)); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("I entered the casino again.", text.get(0)); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("Once more, I went inside.", text.get(0)); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("Once more, I went inside.", text.get(0)); - story.chooseChoiceIndex(0); - } - - /** - * "- show each in turn and then cycle when the keyword is cycle" - */ - @Test - public void cycle() throws Exception { - List text = new ArrayList<>(); - - String json = TestUtils.getJsonString("inkfiles/conditional/cycle.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("I held my breath.", text.get(0)); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("I waited impatiently.", text.get(0)); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("I paused.", text.get(0)); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("I held my breath.", text.get(0)); - story.chooseChoiceIndex(0); - } - - /** - * "- show each, once, in turn, until all have been shown when the keyword is - * once" - */ - @Test - public void once() throws Exception { - List text = new ArrayList<>(); - - String json = TestUtils.getJsonString("inkfiles/conditional/once.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("Would my luck hold?", text.get(0)); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("Could I win the hand?", text.get(0)); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(0, text.size()); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(0, text.size()); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(0, text.size()); - } - - /** - * "- show one at random when the keyword is shuffle" - */ - @Test - public void shuffle() throws Exception { - List text = new ArrayList<>(); - - String json = TestUtils.getJsonString("inkfiles/conditional/shuffle.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - - // No check of the result, as that is random - } - - @Test - public void shuffleStopping() throws Exception { - List text = new ArrayList<>(); - - String json = TestUtils.getJsonString("inkfiles/conditional/shuffle_stopping.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("final", text.get(0)); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("final", text.get(0)); - } - - @Test - public void shuffleOnce() throws Exception { - List text = new ArrayList<>(); - - String json = TestUtils.getJsonString("inkfiles/conditional/shuffle_once.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(0, text.size()); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(0, text.size()); - } - - /** - * "- show multiple lines of texts from multiline list blocks" - */ - @Test - public void multiline() throws Exception { - List text = new ArrayList<>(); - - String json = TestUtils.getJsonString("inkfiles/conditional/multiline.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("At the table, I drew a card. Ace of Hearts.", text.get(0)); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(2, text.size()); - Assert.assertEquals("I drew a card. 2 of Diamonds.", text.get(0)); - Assert.assertEquals("\"Should I hit you again,\" the croupier asks.", text.get(1)); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(2, text.size()); - Assert.assertEquals("I drew a card. King of Spades.", text.get(0)); - Assert.assertEquals("\"You lose,\" he crowed.", text.get(1)); - } - - /** - * "- allow for embedded diverts" - */ - @Test - public void multilineDivert() throws Exception { - List text = new ArrayList<>(); - - String json = TestUtils.getJsonString("inkfiles/conditional/multiline-divert.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("At the table, I drew a card. Ace of Hearts.", text.get(0)); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(2, text.size()); - Assert.assertEquals("I drew a card. 2 of Diamonds.", text.get(0)); - Assert.assertEquals("\"Should I hit you again,\" the croupier asks.", text.get(1)); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(2, text.size()); - Assert.assertEquals("I drew a card. King of Spades.", text.get(0)); - Assert.assertEquals("\"You lose,\" he crowed.", text.get(1)); - } - - /** - * "- allow for embedded choices" - */ - @Test - public void multilineChoice() throws Exception { - List text = new ArrayList<>(); - - String json = TestUtils.getJsonString("inkfiles/conditional/multiline-choice.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("At the table, I drew a card. Ace of Hearts.", text.get(0)); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(2, story.getCurrentChoices().size()); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("I left the table.", text.get(0)); - } + /** + * "- evaluate an else statement if it exists and no other condition evaluates + * to true" + */ + @Test + public void ifElse() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/conditional/ifelse.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The value is 1.", text.get(0)); + } + + /** + * "- evaluate an extended else statement if it exists and no other condition + * evaluates to true" + */ + @Test + public void ifElseExt() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/conditional/ifelse-ext.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The value is -1.", text.get(0)); + } + + /** + * "- evaluate an extended else statement with text and divert at the end" + */ + @Test + public void ifElseExtText1() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/conditional/ifelse-ext-text1.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + + Assert.assertEquals(1, text.size()); + Assert.assertEquals("This is text 1.", text.get(0)); + + Assert.assertEquals(1, story.getCurrentChoices().size()); + story.chooseChoiceIndex(0); + TestUtils.nextAll(story, text); + Assert.assertEquals(2, text.size()); + Assert.assertEquals("This is the end.", text.get(1)); + } + + /** + * "- evaluate an extended else statement with text and divert at the end" + */ + @Test + public void ifElseExtText2() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/conditional/ifelse-ext-text2.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + + Assert.assertEquals(1, text.size()); + Assert.assertEquals("This is text 2.", text.get(0)); + + Assert.assertEquals(1, story.getCurrentChoices().size()); + story.chooseChoiceIndex(0); + TestUtils.nextAll(story, text); + Assert.assertEquals(2, text.size()); + Assert.assertEquals("This is the end.", text.get(1)); + } + + /** + * "- evaluate an extended else statement with text and divert at the end" + */ + @Test + public void ifElseExtText3() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/conditional/ifelse-ext-text3.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + + Assert.assertEquals(1, text.size()); + Assert.assertEquals("This is text 3.", text.get(0)); + + Assert.assertEquals(1, story.getCurrentChoices().size()); + story.chooseChoiceIndex(0); + TestUtils.nextAll(story, text); + Assert.assertEquals(2, text.size()); + Assert.assertEquals("This is the end.", text.get(1)); + } + + /** + * "- work with conditional content which is not only logic (example 1)" + */ + @Test + public void condText1() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/conditional/condtext.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(3, text.size()); + Assert.assertEquals("I stared at Monsieur Fogg. \"But surely you are not serious?\" I demanded.", text.get(1)); + } + + /** + * "- work with conditional content which is not only logic (example 2)" + */ + @Test + public void condText2() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/conditional/condtext.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + story.chooseChoiceIndex(1); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(2, text.size()); + Assert.assertEquals( + "I stared at Monsieur Fogg. \"But there must be a reason for this trip,\" I observed.", text.get(0)); + } + + /** + * "- work with options as conditional content (example 1)" + */ + @Test + public void condOpt1() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/conditional/condopt.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, story.getCurrentChoices().size()); + } + + /** + * "- work with options as conditional content (example 2)" + */ + @Test + public void condOpt2() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/conditional/condopt.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + story.chooseChoiceIndex(1); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(2, story.getCurrentChoices().size()); + } + + /** + * "- go through the alternatives and stick on last when the keyword is + * stopping" + */ + @Test + public void stopping() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/conditional/stopping.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("I entered the casino.", text.get(0)); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("I entered the casino again.", text.get(0)); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("Once more, I went inside.", text.get(0)); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("Once more, I went inside.", text.get(0)); + story.chooseChoiceIndex(0); + } + + /** + * "- show each in turn and then cycle when the keyword is cycle" + */ + @Test + public void cycle() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/conditional/cycle.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("I held my breath.", text.get(0)); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("I waited impatiently.", text.get(0)); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("I paused.", text.get(0)); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("I held my breath.", text.get(0)); + story.chooseChoiceIndex(0); + } + + /** + * "- show each, once, in turn, until all have been shown when the keyword is + * once" + */ + @Test + public void once() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/conditional/once.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("Would my luck hold?", text.get(0)); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("Could I win the hand?", text.get(0)); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(0, text.size()); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(0, text.size()); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(0, text.size()); + } + + /** + * "- show one at random when the keyword is shuffle" + */ + @Test + public void shuffle() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/conditional/shuffle.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + + // No check of the result, as that is random + } + + @Test + public void shuffleStopping() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/conditional/shuffle_stopping.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("final", text.get(0)); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("final", text.get(0)); + } + + @Test + public void shuffleOnce() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/conditional/shuffle_once.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(0, text.size()); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(0, text.size()); + } + + /** + * "- show multiple lines of texts from multiline list blocks" + */ + @Test + public void multiline() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/conditional/multiline.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("At the table, I drew a card. Ace of Hearts.", text.get(0)); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(2, text.size()); + Assert.assertEquals("I drew a card. 2 of Diamonds.", text.get(0)); + Assert.assertEquals("\"Should I hit you again,\" the croupier asks.", text.get(1)); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(2, text.size()); + Assert.assertEquals("I drew a card. King of Spades.", text.get(0)); + Assert.assertEquals("\"You lose,\" he crowed.", text.get(1)); + } + + /** + * "- allow for embedded diverts" + */ + @Test + public void multilineDivert() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/conditional/multiline-divert.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("At the table, I drew a card. Ace of Hearts.", text.get(0)); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(2, text.size()); + Assert.assertEquals("I drew a card. 2 of Diamonds.", text.get(0)); + Assert.assertEquals("\"Should I hit you again,\" the croupier asks.", text.get(1)); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(2, text.size()); + Assert.assertEquals("I drew a card. King of Spades.", text.get(0)); + Assert.assertEquals("\"You lose,\" he crowed.", text.get(1)); + } + + /** + * "- allow for embedded choices" + */ + @Test + public void multilineChoice() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/conditional/multiline-choice.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("At the table, I drew a card. Ace of Hearts.", text.get(0)); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(2, story.getCurrentChoices().size()); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("I left the table.", text.get(0)); + } } diff --git a/src/test/java/com/bladecoder/ink/runtime/test/DivertSpec.java b/src/test/java/com/bladecoder/ink/runtime/test/DivertSpec.java index 61073e2..61df85f 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/DivertSpec.java +++ b/src/test/java/com/bladecoder/ink/runtime/test/DivertSpec.java @@ -1,109 +1,106 @@ package com.bladecoder.ink.runtime.test; +import com.bladecoder.ink.runtime.Story; import java.util.ArrayList; import java.util.List; - import org.junit.Assert; import org.junit.Test; -import com.bladecoder.ink.runtime.Story; - public class DivertSpec { - - /** - * "- divert text from one knot/stitch to another" - */ - @Test - public void simpleDivert() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/divert/simple-divert.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - - Assert.assertEquals(2, text.size()); - Assert.assertEquals("We arrived into London at 9.45pm exactly.", text.get(0)); - Assert.assertEquals("We hurried home to Savile Row as fast as we could.", text.get(1)); - } - - /** - * "- divert from one line of text to new content invisibly" - */ - @Test - public void invisibleDivert() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/divert/invisible-divert.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - - Assert.assertEquals(1, text.size()); - Assert.assertEquals("We hurried home to Savile Row as fast as we could.", text.get(0)); - } - - /** - * "- branch directly from choices" - */ - @Test - public void divertOnChoice() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/divert/divert-on-choice.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - - Assert.assertEquals(1, text.size()); - Assert.assertEquals("You open the gate, and step out onto the path.", text.get(0)); - } - - /** - * "- be usable to branch and join text seamlessly (example 1)" - */ - @Test - public void complexBranching1() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/divert/complex-branching.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - - Assert.assertEquals(2, text.size()); - Assert.assertEquals("\"There is not a moment to lose!\" I declared.", text.get(0)); - Assert.assertEquals("We hurried home to Savile Row as fast as we could.", text.get(1)); - } - - /** - * "- be usable to branch and join text seamlessly (example 2)" - */ - @Test - public void complexBranching2() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/divert/complex-branching.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - story.chooseChoiceIndex(1); - - text.clear(); - TestUtils.nextAll(story, text); - - Assert.assertEquals(3, text.size()); - Assert.assertEquals("\"Monsieur, let us savour this moment!\" I declared.", text.get(0)); - Assert.assertEquals("My master clouted me firmly around the head and dragged me out of the door.", text.get(1)); - Assert.assertEquals("He insisted that we hurried home to Savile Row as fast as we could.", text.get(2)); - } - + + /** + * "- divert text from one knot/stitch to another" + */ + @Test + public void simpleDivert() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/divert/simple-divert.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + + Assert.assertEquals(2, text.size()); + Assert.assertEquals("We arrived into London at 9.45pm exactly.", text.get(0)); + Assert.assertEquals("We hurried home to Savile Row as fast as we could.", text.get(1)); + } + + /** + * "- divert from one line of text to new content invisibly" + */ + @Test + public void invisibleDivert() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/divert/invisible-divert.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + + Assert.assertEquals(1, text.size()); + Assert.assertEquals("We hurried home to Savile Row as fast as we could.", text.get(0)); + } + + /** + * "- branch directly from choices" + */ + @Test + public void divertOnChoice() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/divert/divert-on-choice.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + + Assert.assertEquals(1, text.size()); + Assert.assertEquals("You open the gate, and step out onto the path.", text.get(0)); + } + + /** + * "- be usable to branch and join text seamlessly (example 1)" + */ + @Test + public void complexBranching1() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/divert/complex-branching.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + + Assert.assertEquals(2, text.size()); + Assert.assertEquals("\"There is not a moment to lose!\" I declared.", text.get(0)); + Assert.assertEquals("We hurried home to Savile Row as fast as we could.", text.get(1)); + } + + /** + * "- be usable to branch and join text seamlessly (example 2)" + */ + @Test + public void complexBranching2() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/divert/complex-branching.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + story.chooseChoiceIndex(1); + + text.clear(); + TestUtils.nextAll(story, text); + + Assert.assertEquals(3, text.size()); + Assert.assertEquals("\"Monsieur, let us savour this moment!\" I declared.", text.get(0)); + Assert.assertEquals("My master clouted me firmly around the head and dragged me out of the door.", text.get(1)); + Assert.assertEquals("He insisted that we hurried home to Savile Row as fast as we could.", text.get(2)); + } } diff --git a/src/test/java/com/bladecoder/ink/runtime/test/FunctionSpecTest.java b/src/test/java/com/bladecoder/ink/runtime/test/FunctionSpecTest.java index 7f5d942..c6c6811 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/FunctionSpecTest.java +++ b/src/test/java/com/bladecoder/ink/runtime/test/FunctionSpecTest.java @@ -1,162 +1,160 @@ -package com.bladecoder.ink.runtime.test; - -import java.util.ArrayList; -import java.util.List; - -import org.junit.Assert; -import org.junit.Test; - -import com.bladecoder.ink.runtime.Story; - -public class FunctionSpecTest { - - /** - * "- return a value from a function in a variable expression" - */ - @Test - public void funcBasic() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/function/func-basic.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The value of x is 4.4.", text.get(0)); - } - - /** - * "- return a value from a function with no parameters" - */ - @Test - public void funcNone() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/function/func-none.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The value of x is 3.8.", text.get(0)); - } - - /** - * "- handle conditionals in the function" - */ - @Test - public void funcInline() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/function/func-inline.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The value of x is 4.4.", text.get(0)); - } - - /** - * "- be able to set a variable as a command" - */ - @Test - public void setVarFunc() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/function/setvar-func.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The value is 6.", text.get(0)); - } - - /** - * "- handle conditionals and setting of variables (test 1)" - */ - @Test - public void complexFunc1() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/function/complex-func1.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The values are 6 and 10.", text.get(0)); - } - - /** - * "- handle conditionals and setting of variables (test 2)" - */ - @Test - public void complexFunc2() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/function/complex-func2.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The values are -1 and 0 and 1.", text.get(0)); - } - - /** - * "- handle conditionals and setting of variables (test 3)" - */ - @Test - public void complexFunc3() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/function/complex-func3.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - - Assert.assertEquals(1, text.size()); - Assert.assertEquals( - "\"I will pay you 120 reales if you get the goods to their destination. The goods will take up 20 cargo spaces.\"", - text.get(0)); - } - - /** - * "- random function" - */ - @Test - public void rnd() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/function/rnd-func.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - - Assert.assertEquals(4, text.size()); - Assert.assertEquals("Rolling dice 1: 4.", text.get(0)); - Assert.assertEquals("Rolling dice 2: 1.", text.get(1)); - Assert.assertEquals("Rolling dice 3: 1.", text.get(2)); - Assert.assertEquals("Rolling dice 4: 5.", text.get(3)); - } - - /** - * "- TestEvaluatingFunctionVariableStateBug" - */ - @Test - public void evaluatingFunctionVariableStateBug() throws Exception { - - String json = TestUtils.getJsonString("inkfiles/function/evaluating-function-variablestate-bug.ink.json"); - Story story = new Story(json); - - Assert.assertEquals("Start\n", story.Continue()); - Assert.assertEquals("In tunnel.\n", story.Continue()); - - Object funcResult = story.evaluateFunction("function_to_evaluate"); - Assert.assertEquals("RIGHT", (String)funcResult); - - Assert.assertEquals("End\n", story.Continue()); - } -} +package com.bladecoder.ink.runtime.test; + +import com.bladecoder.ink.runtime.Story; +import java.util.ArrayList; +import java.util.List; +import org.junit.Assert; +import org.junit.Test; + +public class FunctionSpecTest { + + /** + * "- return a value from a function in a variable expression" + */ + @Test + public void funcBasic() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/function/func-basic.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The value of x is 4.4.", text.get(0)); + } + + /** + * "- return a value from a function with no parameters" + */ + @Test + public void funcNone() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/function/func-none.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The value of x is 3.8.", text.get(0)); + } + + /** + * "- handle conditionals in the function" + */ + @Test + public void funcInline() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/function/func-inline.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The value of x is 4.4.", text.get(0)); + } + + /** + * "- be able to set a variable as a command" + */ + @Test + public void setVarFunc() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/function/setvar-func.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The value is 6.", text.get(0)); + } + + /** + * "- handle conditionals and setting of variables (test 1)" + */ + @Test + public void complexFunc1() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/function/complex-func1.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The values are 6 and 10.", text.get(0)); + } + + /** + * "- handle conditionals and setting of variables (test 2)" + */ + @Test + public void complexFunc2() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/function/complex-func2.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The values are -1 and 0 and 1.", text.get(0)); + } + + /** + * "- handle conditionals and setting of variables (test 3)" + */ + @Test + public void complexFunc3() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/function/complex-func3.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + + Assert.assertEquals(1, text.size()); + Assert.assertEquals( + "\"I will pay you 120 reales if you get the goods to their destination. The goods will take up 20 cargo spaces.\"", + text.get(0)); + } + + /** + * "- random function" + */ + @Test + public void rnd() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/function/rnd-func.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + + Assert.assertEquals(4, text.size()); + Assert.assertEquals("Rolling dice 1: 4.", text.get(0)); + Assert.assertEquals("Rolling dice 2: 1.", text.get(1)); + Assert.assertEquals("Rolling dice 3: 1.", text.get(2)); + Assert.assertEquals("Rolling dice 4: 5.", text.get(3)); + } + + /** + * "- TestEvaluatingFunctionVariableStateBug" + */ + @Test + public void evaluatingFunctionVariableStateBug() throws Exception { + + String json = TestUtils.getJsonString("inkfiles/function/evaluating-function-variablestate-bug.ink.json"); + Story story = new Story(json); + + Assert.assertEquals("Start\n", story.Continue()); + Assert.assertEquals("In tunnel.\n", story.Continue()); + + Object funcResult = story.evaluateFunction("function_to_evaluate"); + Assert.assertEquals("RIGHT", (String) funcResult); + + Assert.assertEquals("End\n", story.Continue()); + } +} diff --git a/src/test/java/com/bladecoder/ink/runtime/test/GatherSpecTest.java b/src/test/java/com/bladecoder/ink/runtime/test/GatherSpecTest.java index fe80c61..b412d4b 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/GatherSpecTest.java +++ b/src/test/java/com/bladecoder/ink/runtime/test/GatherSpecTest.java @@ -1,172 +1,174 @@ package com.bladecoder.ink.runtime.test; +import com.bladecoder.ink.runtime.Story; import java.util.ArrayList; import java.util.List; - import org.junit.Assert; import org.junit.Test; -import com.bladecoder.ink.runtime.Story; - public class GatherSpecTest { - - /** - * "- gather the flow back together again" - */ - @Test - public void gatherBasic() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/gather/gather-basic.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - story.chooseChoiceIndex(1); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(3, text.size()); - Assert.assertEquals("\"Nothing, Monsieur!\" I replied.", text.get(0)); - Assert.assertEquals("\"Very good, then.\"", text.get(1)); - Assert.assertEquals("With that Monsieur Fogg left the room.", text.get(2)); - } - - /** - * "- form chains of content with multiple gathers" - */ - @Test - public void gatherChain() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/gather/gather-chain.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(3, story.getCurrentChoices().size()); - story.chooseChoiceIndex(1); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("I did not pause for breath but kept on running. The road could not be much further! Mackie would have the engine running, and then I'd be safe.", text.get(0)); - Assert.assertEquals(2, story.getCurrentChoices().size()); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(2, text.size()); - Assert.assertEquals("I reached the road and looked about. And would you believe it?", text.get(0)); - Assert.assertEquals("The road was empty. Mackie was nowhere to be seen.", text.get(1)); - } - - /** - * "- allow nested options to pop out to a higher level gather" - */ - @Test - public void nestedFlow() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/gather/nested-flow.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - story.chooseChoiceIndex(2); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(2, text.size()); - Assert.assertEquals("\"Myself!\"", text.get(0)); - Assert.assertEquals("Mrs. Christie lowered her manuscript a moment. The rest of the writing group sat, open-mouthed.", text.get(1)); - } - - /** - * "- gather the flow back together again from arbitrarily deep options - */ - @Test - public void deepNesting() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/gather/deep-nesting.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(2, text.size()); - Assert.assertEquals("\"...Tell us a tale Captain!\"", text.get(0)); - Assert.assertEquals("To a man, the crew began to yawn.", text.get(1)); - } - - /** - * "- offer a compact way to weave and blend text and options (Example 1)" - */ - @Test - public void complexFlow1() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/gather/complex-flow.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - story.chooseChoiceIndex(1); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals("... but I said nothing and we passed the day in silence.", text.get(0)); - } - - /** - * "- offer a compact way to weave and blend text and options (Example 2)" - */ - @Test - public void complexFlow2() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/gather/complex-flow.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(3, text.size()); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(2, text.size()); - story.chooseChoiceIndex(1); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(2, text.size()); - story.chooseChoiceIndex(1); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(3, text.size()); - } + + /** + * "- gather the flow back together again" + */ + @Test + public void gatherBasic() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/gather/gather-basic.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + story.chooseChoiceIndex(1); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(3, text.size()); + Assert.assertEquals("\"Nothing, Monsieur!\" I replied.", text.get(0)); + Assert.assertEquals("\"Very good, then.\"", text.get(1)); + Assert.assertEquals("With that Monsieur Fogg left the room.", text.get(2)); + } + + /** + * "- form chains of content with multiple gathers" + */ + @Test + public void gatherChain() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/gather/gather-chain.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(3, story.getCurrentChoices().size()); + story.chooseChoiceIndex(1); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals( + "I did not pause for breath but kept on running. The road could not be much further! Mackie would have the engine running, and then I'd be safe.", + text.get(0)); + Assert.assertEquals(2, story.getCurrentChoices().size()); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(2, text.size()); + Assert.assertEquals("I reached the road and looked about. And would you believe it?", text.get(0)); + Assert.assertEquals("The road was empty. Mackie was nowhere to be seen.", text.get(1)); + } + + /** + * "- allow nested options to pop out to a higher level gather" + */ + @Test + public void nestedFlow() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/gather/nested-flow.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + story.chooseChoiceIndex(2); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(2, text.size()); + Assert.assertEquals("\"Myself!\"", text.get(0)); + Assert.assertEquals( + "Mrs. Christie lowered her manuscript a moment. The rest of the writing group sat, open-mouthed.", + text.get(1)); + } + + /** + * "- gather the flow back together again from arbitrarily deep options + */ + @Test + public void deepNesting() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/gather/deep-nesting.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(2, text.size()); + Assert.assertEquals("\"...Tell us a tale Captain!\"", text.get(0)); + Assert.assertEquals("To a man, the crew began to yawn.", text.get(1)); + } + + /** + * "- offer a compact way to weave and blend text and options (Example 1)" + */ + @Test + public void complexFlow1() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/gather/complex-flow.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + story.chooseChoiceIndex(1); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals("... but I said nothing and we passed the day in silence.", text.get(0)); + } + + /** + * "- offer a compact way to weave and blend text and options (Example 2)" + */ + @Test + public void complexFlow2() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/gather/complex-flow.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(3, text.size()); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(2, text.size()); + story.chooseChoiceIndex(1); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(2, text.size()); + story.chooseChoiceIndex(1); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(3, text.size()); + } } diff --git a/src/test/java/com/bladecoder/ink/runtime/test/GlueSpecTest.java b/src/test/java/com/bladecoder/ink/runtime/test/GlueSpecTest.java index bc38578..6bf1702 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/GlueSpecTest.java +++ b/src/test/java/com/bladecoder/ink/runtime/test/GlueSpecTest.java @@ -1,90 +1,88 @@ package com.bladecoder.ink.runtime.test; +import com.bladecoder.ink.runtime.Story; import java.util.ArrayList; import java.util.List; - import org.junit.Assert; import org.junit.Test; -import com.bladecoder.ink.runtime.Story; - public class GlueSpecTest { - - /** - * "- bind text together across multiple lines of text" - */ - @Test - public void simpleGlue() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/glue/simple-glue.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("Some content with glue.", text.get(0)); - } - - /** - * "- bind text together across multiple lines of text" - */ - @Test - public void glueWithDivert() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/glue/glue-with-divert.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("We hurried home to Savile Row as fast as we could.", text.get(0)); - } - - /** - * "- bind text together across multiple lines of text" - */ - @Test - public void testLeftRightGlueMatching() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/glue/left-right-glue-matching.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(2, text.size()); - Assert.assertEquals("A line.", text.get(0)); - Assert.assertEquals("Another line.", text.get(1)); - } - - /** - * "- bind text together across multiple lines of text" - */ - @Test - public void testBugfix1() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/glue/testbugfix1.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(2, text.size()); - Assert.assertEquals("A", text.get(0)); - Assert.assertEquals("C", text.get(1)); - } - - /** - * "- bind text together across multiple lines of text" - */ - @Test - public void testBugfix2() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/glue/testbugfix2.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(2, text.size()); - // Assert.assertEquals("A ", text.get(0)); // TODO nextAll is trimming! - Assert.assertEquals("X", text.get(1)); - } + + /** + * "- bind text together across multiple lines of text" + */ + @Test + public void simpleGlue() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/glue/simple-glue.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("Some content with glue.", text.get(0)); + } + + /** + * "- bind text together across multiple lines of text" + */ + @Test + public void glueWithDivert() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/glue/glue-with-divert.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("We hurried home to Savile Row as fast as we could.", text.get(0)); + } + + /** + * "- bind text together across multiple lines of text" + */ + @Test + public void testLeftRightGlueMatching() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/glue/left-right-glue-matching.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(2, text.size()); + Assert.assertEquals("A line.", text.get(0)); + Assert.assertEquals("Another line.", text.get(1)); + } + + /** + * "- bind text together across multiple lines of text" + */ + @Test + public void testBugfix1() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/glue/testbugfix1.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(2, text.size()); + Assert.assertEquals("A", text.get(0)); + Assert.assertEquals("C", text.get(1)); + } + + /** + * "- bind text together across multiple lines of text" + */ + @Test + public void testBugfix2() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/glue/testbugfix2.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(2, text.size()); + // Assert.assertEquals("A ", text.get(0)); // TODO nextAll is trimming! + Assert.assertEquals("X", text.get(1)); + } } diff --git a/src/test/java/com/bladecoder/ink/runtime/test/KnotSpecTest.java b/src/test/java/com/bladecoder/ink/runtime/test/KnotSpecTest.java index e1c9194..282a79e 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/KnotSpecTest.java +++ b/src/test/java/com/bladecoder/ink/runtime/test/KnotSpecTest.java @@ -1,173 +1,171 @@ package com.bladecoder.ink.runtime.test; +import com.bladecoder.ink.runtime.Story; import java.util.ArrayList; import java.util.List; - import org.junit.Assert; import org.junit.Test; -import com.bladecoder.ink.runtime.Story; - public class KnotSpecTest { - /** - * "A single line of plain text in an ink file" - */ - @Test - public void testSingleLine() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/knot/single-line.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("Hello, world!", text.get(0)); - } - - /** - * "Multiple lines of plain text in an ink file" - */ - @Test - public void testMultiLine() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/knot/multi-line.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(3, text.size()); - Assert.assertEquals("Hello, world!", text.get(0)); - Assert.assertEquals("Hello?", text.get(1)); - Assert.assertEquals("Hello, are you there?", text.get(2)); - } - - /** - * "- strip empty lines of output" - */ - @Test - public void stripEmptyLines() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/knot/strip-empty-lines.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(3, text.size()); - Assert.assertEquals("Hello, world!", text.get(0)); - Assert.assertEquals("Hello?", text.get(1)); - Assert.assertEquals("Hello, are you there?", text.get(2)); - } - - /** - * "- handle string parameters in a divert" - */ - @Test - public void paramStrings() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/knot/param-strings.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - story.chooseChoiceIndex(2); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("\"I accuse myself!\" Poirot declared.", text.get(0)); - } - - /** - * "- handle passing integer as parameters in a divert" - */ - @Test - public void paramInts() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/knot/param-ints.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("You give 2 dollars.", text.get(0)); - } - - /** - * "- handle passing floats as parameters in a divert" - * - * FIXME: INKLECATE BUG? - */ - @Test - public void paramFloats() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/knot/param-floats.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("You give 2.5 dollars.", text.get(0)); - } - - /** - * "- handle passing variables as parameters in a divert" - */ - @Test - public void paramVars() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/knot/param-vars.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("You give 2 dollars.", text.get(0)); - } - - /** - * "- handle passing multiple values as parameters in a divert" - */ - @Test - public void paramMulti() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/knot/param-multi.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("You give 1 or 2 dollars. Hmm.", text.get(0)); - } - - /** - * "- should support recursive calls with parameters on a knot" - */ - @Test - public void paramRecurse() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/knot/param-recurse.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(2, text.size()); - Assert.assertEquals("\"The result is 120!\" you announce.", text.get(0)); - } + /** + * "A single line of plain text in an ink file" + */ + @Test + public void testSingleLine() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/knot/single-line.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("Hello, world!", text.get(0)); + } + + /** + * "Multiple lines of plain text in an ink file" + */ + @Test + public void testMultiLine() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/knot/multi-line.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(3, text.size()); + Assert.assertEquals("Hello, world!", text.get(0)); + Assert.assertEquals("Hello?", text.get(1)); + Assert.assertEquals("Hello, are you there?", text.get(2)); + } + + /** + * "- strip empty lines of output" + */ + @Test + public void stripEmptyLines() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/knot/strip-empty-lines.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(3, text.size()); + Assert.assertEquals("Hello, world!", text.get(0)); + Assert.assertEquals("Hello?", text.get(1)); + Assert.assertEquals("Hello, are you there?", text.get(2)); + } + + /** + * "- handle string parameters in a divert" + */ + @Test + public void paramStrings() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/knot/param-strings.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + story.chooseChoiceIndex(2); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("\"I accuse myself!\" Poirot declared.", text.get(0)); + } + + /** + * "- handle passing integer as parameters in a divert" + */ + @Test + public void paramInts() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/knot/param-ints.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("You give 2 dollars.", text.get(0)); + } + + /** + * "- handle passing floats as parameters in a divert" + * + * FIXME: INKLECATE BUG? + */ + @Test + public void paramFloats() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/knot/param-floats.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("You give 2.5 dollars.", text.get(0)); + } + + /** + * "- handle passing variables as parameters in a divert" + */ + @Test + public void paramVars() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/knot/param-vars.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("You give 2 dollars.", text.get(0)); + } + + /** + * "- handle passing multiple values as parameters in a divert" + */ + @Test + public void paramMulti() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/knot/param-multi.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("You give 1 or 2 dollars. Hmm.", text.get(0)); + } + + /** + * "- should support recursive calls with parameters on a knot" + */ + @Test + public void paramRecurse() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/knot/param-recurse.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(2, text.size()); + Assert.assertEquals("\"The result is 120!\" you announce.", text.get(0)); + } } diff --git a/src/test/java/com/bladecoder/ink/runtime/test/ListSpecTest.java b/src/test/java/com/bladecoder/ink/runtime/test/ListSpecTest.java index 2bc901a..fcdfe40 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/ListSpecTest.java +++ b/src/test/java/com/bladecoder/ink/runtime/test/ListSpecTest.java @@ -1,126 +1,143 @@ 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 ListSpecTest { - /** - * "- testListBasicOperations" - */ - @Test - public void testListBasicOperations() throws Exception { + /** + * "- testListBasicOperations" + */ + @Test + public void testListBasicOperations() throws Exception { + + String json = TestUtils.getJsonString("inkfiles/lists/basic-operations.ink.json"); + Story story = new Story(json); + + Assert.assertEquals("b, d\na, b, c, e\nb, c\nfalse\ntrue\ntrue\n", story.continueMaximally()); + } + + /** + * "- TestListMixedItems" + */ + @Test + public void testListMixedItems() throws Exception { + + String json = TestUtils.getJsonString("inkfiles/lists/list-mixed-items.ink.json"); + Story story = new Story(json); - String json = TestUtils.getJsonString("inkfiles/lists/basic-operations.ink.json"); - Story story = new Story(json); + Assert.assertEquals("a, y, c\n", story.continueMaximally()); + } - Assert.assertEquals("b, d\na, b, c, e\nb, c\n0\n1\n1\n", story.continueMaximally()); - } + /** + * "- TestMoreListOperations" + */ + @Test + public void testMoreListOperations() throws Exception { - /** - * "- TestListMixedItems" - */ - @Test - public void testListMixedItems() throws Exception { + String json = TestUtils.getJsonString("inkfiles/lists/more-list-operations.ink.json"); + Story story = new Story(json); - String json = TestUtils.getJsonString("inkfiles/lists/list-mixed-items.ink.json"); - Story story = new Story(json); + Assert.assertEquals("1\nl\nn\nl, m\nn\n", story.continueMaximally()); + } - Assert.assertEquals("a, y, c\n", story.continueMaximally()); - } + /** + * "- TestEmptyListOrigin" + */ + @Test + public void testEmptyListOrigin() throws Exception { - /** - * "- TestMoreListOperations" - */ - @Test - public void testMoreListOperations() throws Exception { + String json = TestUtils.getJsonString("inkfiles/lists/empty-list-origin.ink.json"); + Story story = new Story(json); - String json = TestUtils.getJsonString("inkfiles/lists/more-list-operations.ink.json"); - Story story = new Story(json); + Assert.assertEquals("a, b\n", story.continueMaximally()); + } - Assert.assertEquals("1\nl\nn\nl, m\nn\n", story.continueMaximally()); - } + /** + * "- TestListSaveLoad" + */ + @Test + public void testListSaveLoad() throws Exception { - /** - * "- TestEmptyListOrigin" - */ - @Test - public void testEmptyListOrigin() throws Exception { + String json = TestUtils.getJsonString("inkfiles/lists/list-save-load.ink.json"); + Story story = new Story(json); - String json = TestUtils.getJsonString("inkfiles/lists/empty-list-origin.ink.json"); - Story story = new Story(json); + Assert.assertEquals("a, x, c\n", story.continueMaximally()); - Assert.assertEquals("a, b\n", story.continueMaximally()); - } + String savedState = story.getState().toJson(); - /** - * "- TestListSaveLoad" - */ - @Test - public void testListSaveLoad() throws Exception { + // Compile new version of the story + story = new Story(json); - String json = TestUtils.getJsonString("inkfiles/lists/list-save-load.ink.json"); - Story story = new Story(json); + // Load saved game + story.getState().loadJson(savedState); - Assert.assertEquals("a, x, c\n", story.continueMaximally()); + story.choosePathString("elsewhere"); + // FIXME: This is the test from the C# impl. Is it correct? + // Assert.assertEquals("a, x, c, z\n", story.continueMaximally()); - String savedState = story.getState().toJson(); + Assert.assertEquals("z\n", story.continueMaximally()); + } - // Compile new version of the story - story = new Story(json); + /** + * "- TestEmptyListOriginAfterAssignment" + */ + @Test + public void testEmptyListOriginAfterAssignment() throws Exception { - // Load saved game - story.getState().loadJson(savedState); + String json = TestUtils.getJsonString("inkfiles/lists/empty-list-origin-after-assignment.ink.json"); + Story story = new Story(json); - story.choosePathString("elsewhere"); - // FIXME: This is the test from the C# impl. Is it correct? -// Assert.assertEquals("a, x, c, z\n", story.continueMaximally()); + Assert.assertEquals("a, b, c\n", story.continueMaximally()); + } - Assert.assertEquals("z\n", story.continueMaximally()); - } + @Test + public void testListRange() throws Exception { - /** - * "- TestEmptyListOriginAfterAssignment" - */ - @Test - public void testEmptyListOriginAfterAssignment() throws Exception { + String json = TestUtils.getJsonString("inkfiles/lists/list-range.ink.json"); + Story story = new Story(json); - String json = TestUtils.getJsonString("inkfiles/lists/empty-list-origin-after-assignment.ink.json"); - Story story = new Story(json); + Assert.assertEquals( + "Pound, Pizza, Euro, Pasta, Dollar, Curry, Paella\nEuro, Pasta, Dollar, Curry\nTwo, Three, Four, Five, Six\nPizza, Pasta\n", + story.continueMaximally()); + } - Assert.assertEquals("a, b, c\n", story.continueMaximally()); - } + @Test + public void testBugAddingElement() throws Exception { - // @Test - public void testListRange() throws Exception { + String json = TestUtils.getJsonString("inkfiles/lists/bug-adding-element.ink.json"); + Story story = new Story(json); - String json = TestUtils.getJsonString("inkfiles/lists/list-range.ink.json"); - Story story = new Story(json); + String s = story.continueMaximally(); + Assert.assertEquals("", s); - Assert.assertEquals( - "Pound, Pizza, Euro, Pasta, Dollar, Curry, Paella\nEuro, Pasta, Dollar, Curry\nTwo, Three, Four, Five, Six\nPizza, Pasta\n", - story.continueMaximally()); - } + story.chooseChoiceIndex(0); + s = story.continueMaximally(); + Assert.assertEquals("a\n", s); - @Test - public void testBugAddingElement() throws Exception { + story.chooseChoiceIndex(1); + s = story.continueMaximally(); + Assert.assertEquals("OK\n", s); + } - String json = TestUtils.getJsonString("inkfiles/lists/bug-adding-element.ink.json"); - Story story = new Story(json); + @Test + public void testMoreListOperations2() throws Exception { - String s = story.continueMaximally(); - Assert.assertEquals("", s); + String json = TestUtils.getJsonString("inkfiles/lists/more-list-operations2.ink.json"); + Story story = new Story(json); - story.chooseChoiceIndex(0); - s = story.continueMaximally(); - Assert.assertEquals("a\n", s); + Assert.assertEquals( + "a1, b1, c1\na1\na1, b2\ncount:2\nmax:c2\nmin:a1\ntrue\ntrue\nfalse\nempty\na2\na2, b2, c2\nrange:a1, b2\na1\nsubtract:a1, c1\nrandom:a1\nlistinc:b1\n", + story.continueMaximally()); + } - story.chooseChoiceIndex(1); - s = story.continueMaximally(); - Assert.assertEquals("OK\n", s); + @Test + public void testListAllBug() throws Exception { - } + String json = TestUtils.getJsonString("inkfiles/lists/list-all.ink.json"); + Story story = new Story(json); + Assert.assertEquals("A, B\n", story.continueMaximally()); + } } diff --git a/src/test/java/com/bladecoder/ink/runtime/test/MiscTest.java b/src/test/java/com/bladecoder/ink/runtime/test/MiscTest.java index 0403905..4a2414c 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/MiscTest.java +++ b/src/test/java/com/bladecoder/ink/runtime/test/MiscTest.java @@ -1,31 +1,30 @@ 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 MiscTest { - /** - * Issue: https://github.com/bladecoder/blade-ink/issues/15 - */ - @Test - public void issue15() throws Exception { - String json = TestUtils.getJsonString("inkfiles/misc/issue15.ink.json"); - Story story = new Story(json); + /** + * Issue: https://github.com/bladecoder/blade-ink/issues/15 + */ + @Test + public void issue15() throws Exception { + String json = TestUtils.getJsonString("inkfiles/misc/issue15.ink.json"); + Story story = new Story(json); - Assert.assertEquals("This is a test\n", story.Continue()); + Assert.assertEquals("This is a test\n", story.Continue()); - while (story.canContinue()) { -// System.out.println(story.buildStringOfHierarchy()); - String line = story.Continue(); + while (story.canContinue()) { + // System.out.println(story.buildStringOfHierarchy()); + String line = story.Continue(); - if (line.startsWith("SET_X:")) { - story.getVariablesState().set("x", 100); - } else { - Assert.assertEquals("X is set\n", line); - } - } - } + if (line.startsWith("SET_X:")) { + story.getVariablesState().set("x", 100); + } else { + Assert.assertEquals("X is set\n", line); + } + } + } } diff --git a/src/test/java/com/bladecoder/ink/runtime/test/MultiFlowSpecTest.java b/src/test/java/com/bladecoder/ink/runtime/test/MultiFlowSpecTest.java new file mode 100644 index 0000000..087959e --- /dev/null +++ b/src/test/java/com/bladecoder/ink/runtime/test/MultiFlowSpecTest.java @@ -0,0 +1,89 @@ +package com.bladecoder.ink.runtime.test; + +import com.bladecoder.ink.runtime.Story; +import org.junit.Assert; +import org.junit.Test; + +public class MultiFlowSpecTest { + + @Test + public void basics() throws Exception { + + String json = TestUtils.getJsonString("inkfiles/runtime/multiflow-basics.ink.json"); + Story story = new Story(json); + + story.switchFlow("First"); + story.choosePathString("knot1"); + Assert.assertEquals("knot 1 line 1\n", story.Continue()); + + story.switchFlow("Second"); + story.choosePathString("knot2"); + Assert.assertEquals("knot 2 line 1\n", story.Continue()); + + story.switchFlow("First"); + Assert.assertEquals("knot 1 line 2\n", story.Continue()); + + story.switchFlow("Second"); + Assert.assertEquals("knot 2 line 2\n", story.Continue()); + } + + @Test + public void testMultiFlowSaveLoadThreads() throws Exception { + + String json = TestUtils.getJsonString("inkfiles/runtime/multiflow-saveloadthreads.ink.json"); + Story story = new Story(json); + + // Default flow + Assert.assertEquals("Default line 1\n", story.Continue()); + + story.switchFlow("Blue Flow"); + story.choosePathString("blue"); + Assert.assertEquals("Hello I'm blue\n", story.Continue()); + + story.switchFlow("Red Flow"); + story.choosePathString("red"); + Assert.assertEquals("Hello I'm red\n", story.Continue()); + + // Test existing state remains after switch (blue) + story.switchFlow("Blue Flow"); + Assert.assertEquals("Hello I'm blue\n", story.getCurrentText()); + Assert.assertEquals( + "Thread 1 blue choice", story.getCurrentChoices().get(0).getText()); + + // Test existing state remains after switch (red) + story.switchFlow("Red Flow"); + Assert.assertEquals("Hello I'm red\n", story.getCurrentText()); + Assert.assertEquals( + "Thread 1 red choice", story.getCurrentChoices().get(0).getText()); + + // Save/load test + String saved = story.getState().toJson(); + + // Test choice before reloading state before resetting + story.chooseChoiceIndex(0); + Assert.assertEquals("Thread 1 red choice\nAfter thread 1 choice (red)\n", story.continueMaximally()); + story.resetState(); + + // Load to pre-choice: still red, choose second choice + story.getState().loadJson(saved); + + story.chooseChoiceIndex(1); + Assert.assertEquals("Thread 2 red choice\nAfter thread 2 choice (red)\n", story.continueMaximally()); + + // Load: switch to blue, choose 1 + story.getState().loadJson(saved); + story.switchFlow("Blue Flow"); + story.chooseChoiceIndex(0); + Assert.assertEquals("Thread 1 blue choice\nAfter thread 1 choice (blue)\n", story.continueMaximally()); + + // Load: switch to blue, choose 2 + story.getState().loadJson(saved); + story.switchFlow("Blue Flow"); + story.chooseChoiceIndex(1); + Assert.assertEquals("Thread 2 blue choice\nAfter thread 2 choice (blue)\n", story.continueMaximally()); + + // Remove active blue flow, should revert back to global flow + story.removeFlow("Blue Flow"); + Assert.assertEquals("Default line 2\n", story.Continue()); + } +} diff --git a/src/test/java/com/bladecoder/ink/runtime/test/RuntimeSpecTest.java b/src/test/java/com/bladecoder/ink/runtime/test/RuntimeSpecTest.java index 6cdb0d2..d81d1d4 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/RuntimeSpecTest.java +++ b/src/test/java/com/bladecoder/ink/runtime/test/RuntimeSpecTest.java @@ -1,487 +1,484 @@ package com.bladecoder.ink.runtime.test; -import java.util.ArrayList; -import java.util.List; - -import org.junit.Assert; -import org.junit.Test; - import com.bladecoder.ink.runtime.Profiler; import com.bladecoder.ink.runtime.Story; import com.bladecoder.ink.runtime.Story.ExternalFunction; import com.bladecoder.ink.runtime.Story.ExternalFunction0; import com.bladecoder.ink.runtime.Story.ExternalFunction1; -import com.bladecoder.ink.runtime.Story.ExternalFunction3; import com.bladecoder.ink.runtime.Story.ExternalFunction2; +import com.bladecoder.ink.runtime.Story.ExternalFunction3; import com.bladecoder.ink.runtime.Story.VariableObserver; import com.bladecoder.ink.runtime.StoryException; +import java.util.ArrayList; +import java.util.List; +import org.junit.Assert; +import org.junit.Test; public class RuntimeSpecTest { - /** - * Test external function call. - */ - @Test - public void externalFunction() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/runtime/external-function-2-arg.ink.json"); - final Story story = new Story(json); - - story.bindExternalFunction("externalFunction", new ExternalFunction() { - - @Override - public Integer call(Object[] args) throws Exception { - int x = story.tryCoerce(args[0], Integer.class); - int y = story.tryCoerce(args[1], Integer.class); - return x - y; - } - }); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The value is -1.", text.get(0)); - } - - /** - * Test external function zero arguments call. - */ - @Test - public void externalFunctionZeroArguments() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/runtime/external-function-0-arg.ink.json"); - final Story story = new Story(json); - - story.bindExternalFunction("externalFunction", new ExternalFunction0() { - - @Override - protected String call() { - return "Hello world"; - } - }); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The value is Hello world.", text.get(0)); - } - - /** - * Test external function one argument call. - */ - @Test - public void externalFunctionOneArgument() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/runtime/external-function-1-arg.ink.json"); - final Story story = new Story(json); - - story.bindExternalFunction("externalFunction", new ExternalFunction1() { - - @Override - protected Boolean call(Integer arg) { - return arg != 1; - } - }); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The value is 0.", text.get(0)); - } - - /** - * Test external function one argument call. Overrides coerce method. - */ - @Test - public void externalFunctionOneArgumentCoerceOverride() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/runtime/external-function-1-arg.ink.json"); - final Story story = new Story(json); - - story.bindExternalFunction("externalFunction", new ExternalFunction1() { - - @Override - protected Boolean coerceArg(Object arg) throws Exception { - return story.tryCoerce(arg, Boolean.class); - } - - @Override - protected Boolean call(Boolean arg) { - return !arg; - } - }); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The value is 0.", text.get(0)); - } - - /** - * Test external function two arguments call. - */ - @Test - public void externalFunctionTwoArguments() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/runtime/external-function-2-arg.ink.json"); - final Story story = new Story(json); - - story.bindExternalFunction("externalFunction", new ExternalFunction2() { - - @Override - protected Integer call(Integer x, Float y) { - return (int) (x - y); - } - }); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The value is -1.", text.get(0)); - } - - /** - * Test external function two arguments call. Overrides coerce methods. - */ - @Test - public void externalFunctionTwoArgumentsCoerceOverride() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/runtime/external-function-2-arg.ink.json"); - final Story story = new Story(json); - - story.bindExternalFunction("externalFunction", new ExternalFunction2() { - - @Override - protected Integer coerceArg0(Object arg) throws Exception { - return story.tryCoerce(arg, Integer.class); - } - - @Override - protected Integer coerceArg1(Object arg) throws Exception { - return story.tryCoerce(arg, Integer.class); - } - - @Override - protected Integer call(Integer x, Integer y) { - return x - y; - } - }); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The value is -1.", text.get(0)); - } - - /** - * Test external function three arguments call. - */ - @Test - public void externalFunctionThreeArguments() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/runtime/external-function-3-arg.ink.json"); - final Story story = new Story(json); - - story.bindExternalFunction("externalFunction", new ExternalFunction3() { - - @Override - protected Integer call(Integer x, Integer y, Integer z) { - return x + y + z; - } - }); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The value is 6.", text.get(0)); - } - - /** - * Test external function three arguments call. Overrides coerce methods. - */ - @Test - public void externalFunctionThreeArgumentsCoerceOverride() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/runtime/external-function-3-arg.ink.json"); - final Story story = new Story(json); - - story.bindExternalFunction("externalFunction", new ExternalFunction3() { - - @Override - protected Integer coerceArg0(Object arg) throws Exception { - return story.tryCoerce(arg, Integer.class); - } - - @Override - protected Integer coerceArg1(Object arg) throws Exception { - return story.tryCoerce(arg, Integer.class); - } - - @Override - protected Integer coerceArg2(Object arg) throws Exception { - return story.tryCoerce(arg, Integer.class); - } - - @Override - protected Integer call(Integer x, Integer y, Integer z) { - return x + y + z; - } - }); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The value is 6.", text.get(0)); - } - - /** - * Test external function fallback. - */ - @Test - public void externalFunctionFallback() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/runtime/external-function-2-arg.ink.json"); - Story story = new Story(json); - - story.setAllowExternalFunctionFallbacks(true); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The value is 7.0.", text.get(0)); - } - - private static int variableObserversExceptedValue = 5; - - /** - * Test variable observers. - */ - @Test - public void variableObservers() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/runtime/variable-observers.ink.json"); - Story story = new Story(json); - - story.observeVariable("x", new VariableObserver() { - - @Override - public void call(String variableName, Object newValue) { - if (!"x".equals(variableName)) - Assert.fail(); - try { - if ((int) newValue != variableObserversExceptedValue) - Assert.fail(); - - variableObserversExceptedValue = 10; - } catch (Exception e) { - Assert.fail(); - } - } - }); - - TestUtils.nextAll(story, text); - story.chooseChoiceIndex(0); - TestUtils.nextAll(story, text); - } - - /** - * Test set/get variables from code. - */ - @Test - public void setAndGetVariable() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/runtime/set-get-variables.ink.json"); - Story story = new Story(json); + /** + * Test external function call. + */ + @Test + public void externalFunction() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/runtime/external-function-2-arg.ink.json"); + final Story story = new Story(json); + + story.bindExternalFunction("externalFunction", new ExternalFunction() { + + @Override + public Integer call(Object[] args) throws Exception { + int x = story.tryCoerce(args[0], Integer.class); + int y = story.tryCoerce(args[1], Integer.class); + return x - y; + } + }); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The value is -1.", text.get(0)); + } + + /** + * Test external function zero arguments call. + */ + @Test + public void externalFunctionZeroArguments() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/runtime/external-function-0-arg.ink.json"); + final Story story = new Story(json); + + story.bindExternalFunction("externalFunction", new ExternalFunction0() { + + @Override + protected String call() { + return "Hello world"; + } + }); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The value is Hello world.", text.get(0)); + } + + /** + * Test external function one argument call. + */ + @Test + public void externalFunctionOneArgument() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/runtime/external-function-1-arg.ink.json"); + final Story story = new Story(json); + + story.bindExternalFunction("externalFunction", new ExternalFunction1() { + + @Override + protected Boolean call(Integer arg) { + return arg != 1; + } + }); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The value is false.", text.get(0)); + } + + /** + * Test external function one argument call. Overrides coerce method. + */ + @Test + public void externalFunctionOneArgumentCoerceOverride() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/runtime/external-function-1-arg.ink.json"); + final Story story = new Story(json); + + story.bindExternalFunction("externalFunction", new ExternalFunction1() { + + @Override + protected Boolean coerceArg(Object arg) throws Exception { + return story.tryCoerce(arg, Boolean.class); + } + + @Override + protected Boolean call(Boolean arg) { + return !arg; + } + }); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The value is false.", text.get(0)); + } + + /** + * Test external function two arguments call. + */ + @Test + public void externalFunctionTwoArguments() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/runtime/external-function-2-arg.ink.json"); + final Story story = new Story(json); + + story.bindExternalFunction("externalFunction", new ExternalFunction2() { + + @Override + protected Integer call(Integer x, Float y) { + return (int) (x - y); + } + }); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The value is -1.", text.get(0)); + } + + /** + * Test external function two arguments call. Overrides coerce methods. + */ + @Test + public void externalFunctionTwoArgumentsCoerceOverride() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/runtime/external-function-2-arg.ink.json"); + final Story story = new Story(json); + + story.bindExternalFunction("externalFunction", new ExternalFunction2() { + + @Override + protected Integer coerceArg0(Object arg) throws Exception { + return story.tryCoerce(arg, Integer.class); + } + + @Override + protected Integer coerceArg1(Object arg) throws Exception { + return story.tryCoerce(arg, Integer.class); + } + + @Override + protected Integer call(Integer x, Integer y) { + return x - y; + } + }); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The value is -1.", text.get(0)); + } + + /** + * Test external function three arguments call. + */ + @Test + public void externalFunctionThreeArguments() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/runtime/external-function-3-arg.ink.json"); + final Story story = new Story(json); + + story.bindExternalFunction("externalFunction", new ExternalFunction3() { + + @Override + protected Integer call(Integer x, Integer y, Integer z) { + return x + y + z; + } + }); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The value is 6.", text.get(0)); + } + + /** + * Test external function three arguments call. Overrides coerce methods. + */ + @Test + public void externalFunctionThreeArgumentsCoerceOverride() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/runtime/external-function-3-arg.ink.json"); + final Story story = new Story(json); + + story.bindExternalFunction("externalFunction", new ExternalFunction3() { + + @Override + protected Integer coerceArg0(Object arg) throws Exception { + return story.tryCoerce(arg, Integer.class); + } + + @Override + protected Integer coerceArg1(Object arg) throws Exception { + return story.tryCoerce(arg, Integer.class); + } + + @Override + protected Integer coerceArg2(Object arg) throws Exception { + return story.tryCoerce(arg, Integer.class); + } + + @Override + protected Integer call(Integer x, Integer y, Integer z) { + return x + y + z; + } + }); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The value is 6.", text.get(0)); + } + + /** + * Test external function fallback. + */ + @Test + public void externalFunctionFallback() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/runtime/external-function-2-arg.ink.json"); + Story story = new Story(json); + + story.setAllowExternalFunctionFallbacks(true); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The value is 7.0.", text.get(0)); + } + + private static int variableObserversExpectedValue = 5; + + /** + * Test variable observers. + */ + @Test + public void variableObservers() throws Exception { + List text = new ArrayList<>(); + + String json = TestUtils.getJsonString("inkfiles/runtime/variable-observers.ink.json"); + Story story = new Story(json); + + story.observeVariable("x", new VariableObserver() { + + @Override + public void call(String variableName, Object newValue) { + if (!"x".equals(variableName)) Assert.fail(); + try { + if ((int) newValue != variableObserversExpectedValue) Assert.fail(); + + variableObserversExpectedValue = 10; + } catch (Exception e) { + Assert.fail(); + } + } + }); + + TestUtils.nextAll(story, text); + story.chooseChoiceIndex(0); + TestUtils.nextAll(story, text); + + Assert.assertEquals(10, variableObserversExpectedValue); + } + + /** + * Test set/get variables from code. + */ + @Test + public void setAndGetVariable() throws Exception { + List text = new ArrayList<>(); - TestUtils.nextAll(story, text); + String json = TestUtils.getJsonString("inkfiles/runtime/set-get-variables.ink.json"); + Story story = new Story(json); - Assert.assertEquals(10, (int) story.getVariablesState().get("x")); + TestUtils.nextAll(story, text); - story.getVariablesState().set("x", 15); + Assert.assertEquals(10, (int) story.getVariablesState().get("x")); - Assert.assertEquals(15, (int) story.getVariablesState().get("x")); + story.getVariablesState().set("x", 15); - story.chooseChoiceIndex(0); + Assert.assertEquals(15, (int) story.getVariablesState().get("x")); - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals("OK", text.get(0)); - } + story.chooseChoiceIndex(0); - /** - * Test non existant variable. - */ - @Test - public void testSetNonExistantVariable() throws Exception { - List text = new ArrayList(); + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals("OK", text.get(0)); + } - String json = TestUtils.getJsonString("inkfiles/runtime/set-get-variables.ink.json"); - Story story = new Story(json); + /** + * Test non existant variable. + */ + @Test + public void testSetNonExistantVariable() throws Exception { + List text = new ArrayList<>(); - TestUtils.nextAll(story, text); + String json = TestUtils.getJsonString("inkfiles/runtime/set-get-variables.ink.json"); + Story story = new Story(json); - try { - story.getVariablesState().set("y", "earth"); - Assert.fail("Setting non existant variable."); - } catch(StoryException e) { + TestUtils.nextAll(story, text); - } + try { + story.getVariablesState().set("y", "earth"); + Assert.fail("Setting non existant variable."); + } catch (StoryException e) { - Assert.assertEquals(10, (int) story.getVariablesState().get("x")); + } - story.getVariablesState().set("x", 15); + Assert.assertEquals(10, (int) story.getVariablesState().get("x")); - Assert.assertEquals(15, (int) story.getVariablesState().get("x")); + story.getVariablesState().set("x", 15); - story.chooseChoiceIndex(0); + Assert.assertEquals(15, (int) story.getVariablesState().get("x")); - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals("OK", text.get(0)); - } + story.chooseChoiceIndex(0); - /** - * Jump to knot from code. - */ - @Test - public void jumpKnot() throws Exception { - List text = new ArrayList(); + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals("OK", text.get(0)); + } - String json = TestUtils.getJsonString("inkfiles/runtime/jump-knot.ink.json"); - Story story = new Story(json); + /** + * Jump to knot from code. + */ + @Test + public void jumpKnot() throws Exception { + List text = new ArrayList<>(); - story.choosePathString("two"); - TestUtils.nextAll(story, text); - Assert.assertEquals("Two", text.get(0)); + String json = TestUtils.getJsonString("inkfiles/runtime/jump-knot.ink.json"); + Story story = new Story(json); - text.clear(); - story.choosePathString("three"); - TestUtils.nextAll(story, text); - Assert.assertEquals("Three", text.get(0)); + story.choosePathString("two"); + TestUtils.nextAll(story, text); + Assert.assertEquals("Two", text.get(0)); - text.clear(); - story.choosePathString("one"); - TestUtils.nextAll(story, text); - Assert.assertEquals("One", text.get(0)); + text.clear(); + story.choosePathString("three"); + TestUtils.nextAll(story, text); + Assert.assertEquals("Three", text.get(0)); - text.clear(); - story.choosePathString("two"); - TestUtils.nextAll(story, text); - Assert.assertEquals("Two", text.get(0)); - } + text.clear(); + story.choosePathString("one"); + TestUtils.nextAll(story, text); + Assert.assertEquals("One", text.get(0)); - /** - * Test the Profiler. - */ - @Test - public void profiler() throws Exception { - List text = new ArrayList(); + text.clear(); + story.choosePathString("two"); + TestUtils.nextAll(story, text); + Assert.assertEquals("Two", text.get(0)); + } - String json = TestUtils.getJsonString("inkfiles/runtime/jump-knot.ink.json"); - Story story = new Story(json); + /** + * Test the Profiler. + */ + @Test + public void profiler() throws Exception { + List text = new ArrayList<>(); - Profiler profiler = story.startProfiling(); + String json = TestUtils.getJsonString("inkfiles/runtime/jump-knot.ink.json"); + Story story = new Story(json); - story.choosePathString("two"); - TestUtils.nextAll(story, text); + Profiler profiler = story.startProfiling(); - story.choosePathString("three"); - TestUtils.nextAll(story, text); + story.choosePathString("two"); + TestUtils.nextAll(story, text); - story.choosePathString("one"); - TestUtils.nextAll(story, text); + story.choosePathString("three"); + TestUtils.nextAll(story, text); - story.choosePathString("two"); - TestUtils.nextAll(story, text); + story.choosePathString("one"); + TestUtils.nextAll(story, text); - String reportStr = profiler.report(); + story.choosePathString("two"); + TestUtils.nextAll(story, text); - story.endProfiling(); + String reportStr = profiler.report(); - System.out.println("PROFILER REPORT: " + reportStr); - } + story.endProfiling(); - /** - * Jump to stitch from code. - */ - @Test - public void jumpStitch() throws Exception { - List text = new ArrayList(); + System.out.println("PROFILER REPORT: " + reportStr); + } - String json = TestUtils.getJsonString("inkfiles/runtime/jump-stitch.ink.json"); - Story story = new Story(json); + /** + * Jump to stitch from code. + */ + @Test + public void jumpStitch() throws Exception { + List text = new ArrayList<>(); - story.choosePathString("two.sthree"); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("Two.3", text.get(0)); + String json = TestUtils.getJsonString("inkfiles/runtime/jump-stitch.ink.json"); + Story story = new Story(json); - text.clear(); - story.choosePathString("one.stwo"); - TestUtils.nextAll(story, text); - Assert.assertEquals("One.2", text.get(0)); + story.choosePathString("two.sthree"); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("Two.3", text.get(0)); - text.clear(); - story.choosePathString("one.sone"); - TestUtils.nextAll(story, text); - Assert.assertEquals("One.1", text.get(0)); + text.clear(); + story.choosePathString("one.stwo"); + TestUtils.nextAll(story, text); + Assert.assertEquals("One.2", text.get(0)); - text.clear(); - story.choosePathString("two.stwo"); - TestUtils.nextAll(story, text); - Assert.assertEquals("Two.2", text.get(0)); - } + text.clear(); + story.choosePathString("one.sone"); + TestUtils.nextAll(story, text); + Assert.assertEquals("One.1", text.get(0)); - /** - * Read the visit counts from code. - */ - @Test - public void readVisitCounts() throws Exception { - List text = new ArrayList(); + text.clear(); + story.choosePathString("two.stwo"); + TestUtils.nextAll(story, text); + Assert.assertEquals("Two.2", text.get(0)); + } - String json = TestUtils.getJsonString("inkfiles/runtime/read-visit-counts.ink.json"); - Story story = new Story(json); + /** + * Read the visit counts from code. The .ink file must be compiled with the '-c' + * flag in inklecate. + */ + @Test + public void readVisitCounts() throws Exception { + List text = new ArrayList<>(); - TestUtils.nextAll(story, text); - Assert.assertEquals(4, story.getState().visitCountAtPathString("two.s2")); - Assert.assertEquals(5, story.getState().visitCountAtPathString("two")); - } + String json = TestUtils.getJsonString("inkfiles/runtime/read-visit-counts.ink.json"); + Story story = new Story(json); - @Test - public void testLoadSave() throws Exception { - String json = TestUtils.getJsonString("inkfiles/runtime/load-save.ink.json"); - Story story = new Story(json); + TestUtils.nextAll(story, text); + Assert.assertEquals(4, story.getState().visitCountAtPathString("two.s2")); + Assert.assertEquals(5, story.getState().visitCountAtPathString("two")); + } - List text = new ArrayList(); + @Test + public void testLoadSave() throws Exception { + String json = TestUtils.getJsonString("inkfiles/runtime/load-save.ink.json"); + Story story = new Story(json); - TestUtils.nextAll(story, text); + List text = new ArrayList<>(); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("We arrived into London at 9.45pm exactly.", text.get(0)); + TestUtils.nextAll(story, text); -// String choicesText = getChoicesText(story); -// assertThat(choicesText, is( -// "0:\"There is not a moment to lose!\"\n1:\"Monsieur, let us savour this moment!\"\n2:We hurried home\n")); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("We arrived into London at 9.45pm exactly.", text.get(0)); - // save the game state - String saveString = story.getState().toJson(); + // String choicesText = getChoicesText(story); + // assertThat(choicesText, is( + // "0:\"There is not a moment to lose!\"\n1:\"Monsieur, let us savour this moment!\"\n2:We hurried home\n")); - // recreate game and load state - story = new Story(json); - story.getState().loadJson(saveString); + // save the game state + String saveString = story.getState().toJson(); - story.chooseChoiceIndex(0); + // recreate game and load state + story = new Story(json); + story.getState().loadJson(saveString); - TestUtils.nextAll(story, text); - Assert.assertEquals( - "\"There is not a moment to lose!\" I declared.", text.get(1)); - Assert.assertEquals("We hurried home to Savile Row as fast as we could.", text.get(2)); + story.chooseChoiceIndex(0); - // check that we are at the end - Assert.assertEquals(false, story.canContinue()); - Assert.assertEquals(0, story.getCurrentChoices().size()); - } + TestUtils.nextAll(story, text); + Assert.assertEquals("\"There is not a moment to lose!\" I declared.", text.get(1)); + Assert.assertEquals("We hurried home to Savile Row as fast as we could.", text.get(2)); + // check that we are at the end + Assert.assertEquals(false, story.canContinue()); + Assert.assertEquals(0, story.getCurrentChoices().size()); + } } diff --git a/src/test/java/com/bladecoder/ink/runtime/test/StitchSpecTest.java b/src/test/java/com/bladecoder/ink/runtime/test/StitchSpecTest.java index 7db0ad6..62815f2 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/StitchSpecTest.java +++ b/src/test/java/com/bladecoder/ink/runtime/test/StitchSpecTest.java @@ -1,88 +1,86 @@ package com.bladecoder.ink.runtime.test; +import com.bladecoder.ink.runtime.Story; import java.util.ArrayList; import java.util.List; - import org.junit.Assert; import org.junit.Test; -import com.bladecoder.ink.runtime.Story; - public class StitchSpecTest { - - /** - * "- be automatically started with if there is no content in a knot" - */ - @Test - public void autoStitch() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/stitch/auto-stitch.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("I settled my master.", text.get(0)); - } - - /** - * "- be automatically diverted to if there is no other content in a knot" - */ - @Test - public void autoStitch2() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/stitch/auto-stitch.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("I settled my master.", text.get(0)); - } - - /** - * "- not be diverted to if the knot has content" - */ - @Test - public void manualStitch() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/stitch/manual-stitch.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("How shall we travel?", text.get(0)); - story.chooseChoiceIndex(1); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("I put myself in third.", text.get(0)); - } - - /** - * "- be usable locally without the full name" - */ - @Test - public void manualStitch2() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/stitch/manual-stitch.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("How shall we travel?", text.get(0)); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("I settled my master.", text.get(0)); - } + + /** + * "- be automatically started with if there is no content in a knot" + */ + @Test + public void autoStitch() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/stitch/auto-stitch.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("I settled my master.", text.get(0)); + } + + /** + * "- be automatically diverted to if there is no other content in a knot" + */ + @Test + public void autoStitch2() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/stitch/auto-stitch.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("I settled my master.", text.get(0)); + } + + /** + * "- not be diverted to if the knot has content" + */ + @Test + public void manualStitch() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/stitch/manual-stitch.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("How shall we travel?", text.get(0)); + story.chooseChoiceIndex(1); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("I put myself in third.", text.get(0)); + } + + /** + * "- be usable locally without the full name" + */ + @Test + public void manualStitch2() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/stitch/manual-stitch.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("How shall we travel?", text.get(0)); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("I settled my master.", text.get(0)); + } } 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..904feba 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,114 @@ 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()); + } + + @Test + public void testTagsInChoice() throws Exception { + + String json = TestUtils.getJsonString("inkfiles/tags/tagsInChoice.ink.json"); + Story story = new Story(json); + + 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()); + + story.chooseChoiceIndex(0); + + Assert.assertEquals("one three", story.Continue()); + Assert.assertArrayEquals( + new String[] {"one", "three"}, story.getCurrentTags().toArray()); + } + + @Test + public void testTagsInChoiceDynamicContent() throws Exception { - String json = TestUtils.getJsonString("inkfiles/tags/tags.ink.json"); - Story story = new Story(json); + String json = TestUtils.getJsonString("inkfiles/tags/tagsInChoiceDynamic.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" }; + story.Continue(); + Assert.assertEquals(0, story.getCurrentTags().size()); + Assert.assertEquals(3, story.getCurrentChoices().size()); + Assert.assertArrayEquals( + new String[] {"tag Name"}, + story.getCurrentChoices().get(0).getTags().toArray()); + Assert.assertArrayEquals( + new String[] {"tag 1 Name 2 3 4"}, + story.getCurrentChoices().get(1).getTags().toArray()); + Assert.assertArrayEquals( + new String[] {"Name tag 1 2 3 4"}, + story.getCurrentChoices().get(2).getTags().toArray()); + } - Assert.assertArrayEquals(globalTags, story.getGlobalTags().toArray()); + @Test + public void testTagsDynamicContent() throws Exception { - Assert.assertEquals("This is the content\n", story.Continue()); + String json = TestUtils.getJsonString("inkfiles/tags/tagsDynamicContent.ink.json"); + Story story = new Story(json); - Assert.assertArrayEquals(globalTags, story.getCurrentTags().toArray()); + Assert.assertEquals("tag\n", story.Continue()); + Assert.assertArrayEquals( + new String[] {"pic8red.jpg"}, story.getCurrentTags().toArray()); + } - Assert.assertArrayEquals(knotTags, story.tagsForContentAtPath("knot").toArray()); - Assert.assertArrayEquals(stitchTags, story.tagsForContentAtPath("knot.stitch").toArray()); + @Test + public void testTagsInLines() 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/tagsInLines.ink.json"); + Story story = new Story(json); + Assert.assertEquals("í\n", story.Continue()); + Assert.assertEquals("a\n", story.Continue()); + } } diff --git a/src/test/java/com/bladecoder/ink/runtime/test/TestUtils.java b/src/test/java/com/bladecoder/ink/runtime/test/TestUtils.java index cf47a6b..1da2a19 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/TestUtils.java +++ b/src/test/java/com/bladecoder/ink/runtime/test/TestUtils.java @@ -2,119 +2,112 @@ import static org.junit.Assert.fail; +import com.bladecoder.ink.runtime.Choice; +import com.bladecoder.ink.runtime.Story; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; -import com.bladecoder.ink.runtime.Choice; -import com.bladecoder.ink.runtime.Story; -import com.bladecoder.ink.runtime.StoryException; - public class TestUtils { - public static final String getJsonString(String filename) throws IOException { - - InputStream systemResourceAsStream = ClassLoader.getSystemResourceAsStream(filename); - - BufferedReader br = new BufferedReader(new InputStreamReader(systemResourceAsStream, "UTF-8")); - - try { - StringBuilder sb = new StringBuilder(); - String line = br.readLine(); - - // Replace the BOM mark - if (line != null) - line = line.replace('\uFEFF', ' '); - - while (line != null) { - sb.append(line); - sb.append("\n"); - line = br.readLine(); - } - return sb.toString(); - } finally { - br.close(); - } - } - - public static final List runStory(String filename, List choiceList, List errors) - throws Exception { - // 1) Load story - String json = getJsonString(filename); - -// System.out.println(json); - - Story story = new Story(json); - - List text = new ArrayList<>(); - -// System.out.println(story.BuildStringOfHierarchy()); - - int choiceListIndex = 0; - - while (story.canContinue() || story.getCurrentChoices().size() > 0) { - // 2) Game content, line by line - while (story.canContinue()) { - String line = story.Continue(); - System.out.print(line); - text.add(line); - } - - if (story.hasError()) { - for (String errorMsg : story.getCurrentErrors()) { - System.out.println(errorMsg); - errors.add(errorMsg); - } - } - - // 3) Display story.currentChoices list, allow player to choose one - if (story.getCurrentChoices().size() > 0) { - - for (Choice c : story.getCurrentChoices()) { - System.out.println(c.getText()); - text.add(c.getText() + "\n"); - } - - if (choiceList == null || choiceListIndex >= choiceList.size()) - story.chooseChoiceIndex((int) (Math.random() * story.getCurrentChoices().size())); - else { - story.chooseChoiceIndex(choiceList.get(choiceListIndex)); - choiceListIndex++; - } - } - } - - return text; - } - - public static final String joinText(List text) { - StringBuilder sb = new StringBuilder(); - - for (String s : text) { - sb.append(s); - } - - return sb.toString(); - } - - public static final boolean isEnded(Story story) { - return !story.canContinue() && story.getCurrentChoices().size() == 0; - } - - public static final void nextAll(Story story, List text) throws StoryException, Exception { - while (story.canContinue()) { - String line = story.Continue(); - System.out.print(line); - - if (!line.trim().isEmpty()) - text.add(line.trim()); - } - - if (story.hasError()) { - fail(TestUtils.joinText(story.getCurrentErrors())); - } - } + public static String getJsonString(String filename) throws IOException { + + InputStream systemResourceAsStream = ClassLoader.getSystemResourceAsStream(filename); + + assert systemResourceAsStream != null; + try (BufferedReader br = + new BufferedReader(new InputStreamReader(systemResourceAsStream, StandardCharsets.UTF_8))) { + StringBuilder sb = new StringBuilder(); + String line = br.readLine(); + + while (line != null) { + sb.append(line); + sb.append("\n"); + line = br.readLine(); + } + return sb.toString(); + } + } + + public static List runStory(String filename, List choiceList, List errors) + throws Exception { + // 1) Load story + String json = getJsonString(filename); + + Story story = new Story(json); + + List text = new ArrayList<>(); + + // System.out.println(story.BuildStringOfHierarchy()); + + int choiceListIndex = 0; + + while (story.canContinue() || !story.getCurrentChoices().isEmpty()) { + // System.out.println(story.buildStringOfHierarchy()); + + // 2) Game content, line by line + while (story.canContinue()) { + String line = story.Continue(); + System.out.print(line); + text.add(line); + } + + if (story.hasError()) { + for (String errorMsg : story.getCurrentErrors()) { + System.out.println(errorMsg); + errors.add(errorMsg); + } + } + + // 3) Display story.currentChoices list, allow player to choose one + if (!story.getCurrentChoices().isEmpty()) { + + for (Choice c : story.getCurrentChoices()) { + System.out.println(c.getText()); + text.add(c.getText() + "\n"); + } + + if (choiceList == null || choiceListIndex >= choiceList.size()) + story.chooseChoiceIndex( + (int) (Math.random() * story.getCurrentChoices().size())); + else { + story.chooseChoiceIndex(choiceList.get(choiceListIndex)); + choiceListIndex++; + } + } + } + + return text; + } + + public static String joinText(List text) { + StringBuilder sb = new StringBuilder(); + + for (String s : text) { + sb.append(s); + } + + return sb.toString(); + } + + public static boolean isEnded(Story story) { + return !story.canContinue() && story.getCurrentChoices().isEmpty(); + } + + public static void nextAll(Story story, List text) throws Exception { + while (story.canContinue()) { + String line = story.Continue(); + System.out.print(line); + + if (!line.trim().isEmpty()) text.add(line.trim()); + } + + if (story.hasError()) { + fail(TestUtils.joinText(story.getCurrentErrors())); + } + } } diff --git a/src/test/java/com/bladecoder/ink/runtime/test/ThreadSpecTest.java b/src/test/java/com/bladecoder/ink/runtime/test/ThreadSpecTest.java index 2c3916a..5b8b8a7 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/ThreadSpecTest.java +++ b/src/test/java/com/bladecoder/ink/runtime/test/ThreadSpecTest.java @@ -1,79 +1,76 @@ -package com.bladecoder.ink.runtime.test; - -import org.junit.Assert; -import org.junit.Test; - -import com.bladecoder.ink.runtime.Story; - -public class ThreadSpecTest { - - /** - * "- Exception on threads to add additional choices (#5) - */ - @Test - public void testThread() throws Exception { - - String json = TestUtils.getJsonString("inkfiles/threads/thread-bug.ink.json"); - Story story = new Story(json); - - Assert.assertEquals("Here is some gold. Do you want it?\n", story.continueMaximally()); - - Assert.assertEquals(2, story.getCurrentChoices().size()); - - Assert.assertEquals("No", story.getCurrentChoices().get(0).getText()); - Assert.assertEquals("Yes", story.getCurrentChoices().get(1).getText()); - - story.chooseChoiceIndex(0); - - Assert.assertEquals("No\nTry again!\n", story.continueMaximally()); - - Assert.assertEquals(2, story.getCurrentChoices().size()); - - Assert.assertEquals("No", story.getCurrentChoices().get(0).getText()); - Assert.assertEquals("Yes", story.getCurrentChoices().get(1).getText()); - - story.chooseChoiceIndex(1); - - Assert.assertEquals("Yes\nYou win!\n", story.continueMaximally()); - } - - - /** - * "- Exception on threads to add additional choices (#5) - */ - @Test - public void testThreadBug() throws Exception { - - String json = TestUtils.getJsonString("inkfiles/threads/thread-bug.ink.json"); - Story story = new Story(json); - - Assert.assertEquals("Here is some gold. Do you want it?\n", story.continueMaximally()); - - Assert.assertEquals(2, story.getCurrentChoices().size()); - - Assert.assertEquals("No", story.getCurrentChoices().get(0).getText()); - Assert.assertEquals("Yes", story.getCurrentChoices().get(1).getText()); - - // Convert state to JSON then load it back in - String stateJson = story.getState().toJson(); - - story = new Story(json); - story.getState().loadJson(stateJson); - - // + Works correctly if choiceIdx is 1 ('Yes', defined in the knot) - // + Throws exception if choiceIdx is 0 ('No', defined in the thread) - story.chooseChoiceIndex(0); - - Assert.assertEquals("No\nTry again!\n", story.continueMaximally()); - - Assert.assertEquals(2, story.getCurrentChoices().size()); - - Assert.assertEquals("No", story.getCurrentChoices().get(0).getText()); - Assert.assertEquals("Yes", story.getCurrentChoices().get(1).getText()); - - story.chooseChoiceIndex(1); - - Assert.assertEquals("Yes\nYou win!\n", story.continueMaximally()); - } - -} +package com.bladecoder.ink.runtime.test; + +import com.bladecoder.ink.runtime.Story; +import org.junit.Assert; +import org.junit.Test; + +public class ThreadSpecTest { + + /** + * "- Exception on threads to add additional choices (#5) + */ + @Test + public void testThread() throws Exception { + + String json = TestUtils.getJsonString("inkfiles/threads/thread-bug.ink.json"); + Story story = new Story(json); + + Assert.assertEquals("Here is some gold. Do you want it?\n", story.continueMaximally()); + + Assert.assertEquals(2, story.getCurrentChoices().size()); + + Assert.assertEquals("No", story.getCurrentChoices().get(0).getText()); + Assert.assertEquals("Yes", story.getCurrentChoices().get(1).getText()); + + story.chooseChoiceIndex(0); + + Assert.assertEquals("No\nTry again!\n", story.continueMaximally()); + + Assert.assertEquals(2, story.getCurrentChoices().size()); + + Assert.assertEquals("No", story.getCurrentChoices().get(0).getText()); + Assert.assertEquals("Yes", story.getCurrentChoices().get(1).getText()); + + story.chooseChoiceIndex(1); + + Assert.assertEquals("Yes\nYou win!\n", story.continueMaximally()); + } + + /** + * "- Exception on threads to add additional choices (#5) + */ + @Test + public void testThreadBug() throws Exception { + + String json = TestUtils.getJsonString("inkfiles/threads/thread-bug.ink.json"); + Story story = new Story(json); + + Assert.assertEquals("Here is some gold. Do you want it?\n", story.continueMaximally()); + + Assert.assertEquals(2, story.getCurrentChoices().size()); + + Assert.assertEquals("No", story.getCurrentChoices().get(0).getText()); + Assert.assertEquals("Yes", story.getCurrentChoices().get(1).getText()); + + // Convert state to JSON then load it back in + String stateJson = story.getState().toJson(); + + story = new Story(json); + story.getState().loadJson(stateJson); + + // + Works correctly if choiceIdx is 1 ('Yes', defined in the knot) + // + Throws exception if choiceIdx is 0 ('No', defined in the thread) + story.chooseChoiceIndex(0); + + Assert.assertEquals("No\nTry again!\n", story.continueMaximally()); + + Assert.assertEquals(2, story.getCurrentChoices().size()); + + Assert.assertEquals("No", story.getCurrentChoices().get(0).getText()); + Assert.assertEquals("Yes", story.getCurrentChoices().get(1).getText()); + + story.chooseChoiceIndex(1); + + Assert.assertEquals("Yes\nYou win!\n", story.continueMaximally()); + } +} diff --git a/src/test/java/com/bladecoder/ink/runtime/test/TunnelSpecTest.java b/src/test/java/com/bladecoder/ink/runtime/test/TunnelSpecTest.java index 293607e..0584f62 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/TunnelSpecTest.java +++ b/src/test/java/com/bladecoder/ink/runtime/test/TunnelSpecTest.java @@ -1,22 +1,20 @@ 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 TunnelSpecTest { - /** - * "- Test for tunnel onwards divert override" - */ - @Test - public void testTunnelOnwardsDivertOverride() throws Exception { - - String json = TestUtils.getJsonString("inkfiles/tunnels/tunnel-onwards-divert-override.ink.json"); - Story story = new Story(json); + /** + * "- Test for tunnel onwards divert override" + */ + @Test + public void testTunnelOnwardsDivertOverride() throws Exception { - Assert.assertEquals("This is A\nNow in B.\n", story.continueMaximally()); - } + String json = TestUtils.getJsonString("inkfiles/tunnels/tunnel-onwards-divert-override.ink.json"); + Story story = new Story(json); + Assert.assertEquals("This is A\nNow in B.\n", story.continueMaximally()); + } } 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..8c79c50 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,81 @@ package com.bladecoder.ink.runtime.test; +import com.bladecoder.ink.runtime.Story; import java.util.ArrayList; import java.util.List; - import org.junit.Assert; import org.junit.Test; -import com.bladecoder.ink.runtime.Story; - 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/java/com/bladecoder/ink/runtime/test/VariableTextSpecTest.java b/src/test/java/com/bladecoder/ink/runtime/test/VariableTextSpecTest.java index 7793ee3..2346fd8 100644 --- a/src/test/java/com/bladecoder/ink/runtime/test/VariableTextSpecTest.java +++ b/src/test/java/com/bladecoder/ink/runtime/test/VariableTextSpecTest.java @@ -1,230 +1,230 @@ package com.bladecoder.ink.runtime.test; +import com.bladecoder.ink.runtime.Story; import java.util.ArrayList; import java.util.List; - import org.junit.Assert; import org.junit.Test; -import com.bladecoder.ink.runtime.Story; - public class VariableTextSpecTest { - /** - * "- step through each element and repeat the final element" - */ - @Test - public void sequence() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/variabletext/sequence.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The radio hissed into life. \"Three!\"", text.get(0)); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The radio hissed into life. \"Two!\"", text.get(0)); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The radio hissed into life. \"One!\"", text.get(0)); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The radio hissed into life. There was the white noise racket of an explosion.", text.get(0)); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The radio hissed into life. There was the white noise racket of an explosion.", text.get(0)); - } - - /** - * "- cycle through the element repeatedly" - */ - @Test - public void cycle() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/variabletext/cycle.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The radio hissed into life. \"Three!\"", text.get(0)); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The radio hissed into life. \"Two!\"", text.get(0)); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The radio hissed into life. \"One!\"", text.get(0)); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The radio hissed into life. \"Three!\"", text.get(0)); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The radio hissed into life. \"Two!\"", text.get(0)); - } - - /** - * "- step through each element and return no text once the list is exhausted" - */ - @Test - public void once() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/variabletext/once.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The radio hissed into life. \"Three!\"", text.get(0)); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The radio hissed into life. \"Two!\"", text.get(0)); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The radio hissed into life. \"One!\"", text.get(0)); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The radio hissed into life.", text.get(0)); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The radio hissed into life.", text.get(0)); - } - - - /** - * "- allow for empty text elements in the list" - */ - @Test - public void emptyElements() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/variabletext/empty-elements.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The radio hissed into life.", text.get(0)); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The radio hissed into life.", text.get(0)); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals(1, text.size()); - Assert.assertEquals("The radio hissed into life. \"One!\"", text.get(0)); - } - - /** - * "- be usable in a choice test" - */ - @Test - public void listInChoice() throws Exception { - List text = new ArrayList(); - - String json = TestUtils.getJsonString("inkfiles/variabletext/list-in-choice.ink.json"); - Story story = new Story(json); - - TestUtils.nextAll(story, text); - Assert.assertEquals("\"Hello, Master!\"", story.getCurrentChoices().get(0).getText()); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals("\"Hello, Monsieur!\"", story.getCurrentChoices().get(0).getText()); - story.chooseChoiceIndex(0); - - text.clear(); - TestUtils.nextAll(story, text); - Assert.assertEquals("\"Hello, you!\"", story.getCurrentChoices().get(0).getText()); - } - - /** - * "- return the text string in the sequence if the condition is a valid value" - * - * FIXME: "Value evaluated lists" not supported in C# ref. engine. - */ - @Test - public void one() throws Exception { -// List text = new ArrayList(); -// -// String json = TestUtils.getJsonString("inkfiles/variabletext/one.ink.json"); -// Story story = new Story(json); -// -// TestUtils.nextAll(story, text); -// Assert.assertEquals(1, text.size()); -// Assert.assertEquals("We needed to find one apple.", text.get(0)); - } - - /** - * "- return the text string in the sequence if the condition is a valid value" - * FIXME: "Value evaluated lists" not supported in C# ref. engine. - */ - @Test - public void minusOne() throws Exception { -// List text = new ArrayList(); -// -// String json = TestUtils.getJsonString("inkfiles/variabletext/minus-one.ink.json"); -// Story story = new Story(json); -// -// TestUtils.nextAll(story, text); -// Assert.assertEquals(1, text.size()); -// Assert.assertEquals("We needed to find nothing.", text.get(0)); - } - - /** - * "- return the text string in the sequence if the condition is a valid value" - * FIXME: "Value evaluated lists" not supported in C# ref. engine. - */ - @Test - public void ten() throws Exception { -// List text = new ArrayList(); -// -// String json = TestUtils.getJsonString("inkfiles/variabletext/ten.ink.json"); -// Story story = new Story(json); -// -// TestUtils.nextAll(story, text); -// Assert.assertEquals(1, text.size()); -// Assert.assertEquals("We needed to find many oranges.", text.get(0)); - } - + /** + * "- step through each element and repeat the final element" + */ + @Test + public void sequence() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/variabletext/sequence.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The radio hissed into life. \"Three!\"", text.get(0)); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The radio hissed into life. \"Two!\"", text.get(0)); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The radio hissed into life. \"One!\"", text.get(0)); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals( + "The radio hissed into life. There was the white noise racket of an explosion.", text.get(0)); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals( + "The radio hissed into life. There was the white noise racket of an explosion.", text.get(0)); + } + + /** + * "- cycle through the element repeatedly" + */ + @Test + public void cycle() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/variabletext/cycle.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The radio hissed into life. \"Three!\"", text.get(0)); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The radio hissed into life. \"Two!\"", text.get(0)); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The radio hissed into life. \"One!\"", text.get(0)); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The radio hissed into life. \"Three!\"", text.get(0)); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The radio hissed into life. \"Two!\"", text.get(0)); + } + + /** + * "- step through each element and return no text once the list is exhausted" + */ + @Test + public void once() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/variabletext/once.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The radio hissed into life. \"Three!\"", text.get(0)); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The radio hissed into life. \"Two!\"", text.get(0)); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The radio hissed into life. \"One!\"", text.get(0)); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The radio hissed into life.", text.get(0)); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The radio hissed into life.", text.get(0)); + } + + /** + * "- allow for empty text elements in the list" + */ + @Test + public void emptyElements() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/variabletext/empty-elements.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The radio hissed into life.", text.get(0)); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The radio hissed into life.", text.get(0)); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals(1, text.size()); + Assert.assertEquals("The radio hissed into life. \"One!\"", text.get(0)); + } + + /** + * "- be usable in a choice test" + */ + @Test + public void listInChoice() throws Exception { + List text = new ArrayList(); + + String json = TestUtils.getJsonString("inkfiles/variabletext/list-in-choice.ink.json"); + Story story = new Story(json); + + TestUtils.nextAll(story, text); + Assert.assertEquals( + "\"Hello, Master!\"", story.getCurrentChoices().get(0).getText()); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals( + "\"Hello, Monsieur!\"", story.getCurrentChoices().get(0).getText()); + story.chooseChoiceIndex(0); + + text.clear(); + TestUtils.nextAll(story, text); + Assert.assertEquals("\"Hello, you!\"", story.getCurrentChoices().get(0).getText()); + } + + /** + * "- return the text string in the sequence if the condition is a valid value" + * + * FIXME: "Value evaluated lists" not supported in C# ref. engine. + */ + @Test + public void one() throws Exception { + // List text = new ArrayList(); + // + // String json = TestUtils.getJsonString("inkfiles/variabletext/one.ink.json"); + // Story story = new Story(json); + // + // TestUtils.nextAll(story, text); + // Assert.assertEquals(1, text.size()); + // Assert.assertEquals("We needed to find one apple.", text.get(0)); + } + + /** + * "- return the text string in the sequence if the condition is a valid value" + * FIXME: "Value evaluated lists" not supported in C# ref. engine. + */ + @Test + public void minusOne() throws Exception { + // List text = new ArrayList(); + // + // String json = TestUtils.getJsonString("inkfiles/variabletext/minus-one.ink.json"); + // Story story = new Story(json); + // + // TestUtils.nextAll(story, text); + // Assert.assertEquals(1, text.size()); + // Assert.assertEquals("We needed to find nothing.", text.get(0)); + } + + /** + * "- return the text string in the sequence if the condition is a valid value" + * FIXME: "Value evaluated lists" not supported in C# ref. engine. + */ + @Test + public void ten() throws Exception { + // List text = new ArrayList(); + // + // String json = TestUtils.getJsonString("inkfiles/variabletext/ten.ink.json"); + // Story story = new Story(json); + // + // TestUtils.nextAll(story, text); + // Assert.assertEquals(1, text.size()); + // Assert.assertEquals("We needed to find many oranges.", text.get(0)); + } } diff --git a/src/test/resources/inkfiles/basictext/oneline.ink.json b/src/test/resources/inkfiles/basictext/oneline.ink.json index 389a1e3..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",{"#f":3}],"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 92e380e..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",{"#f":3}],"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 1711675..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",{"#f":3}],"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 b4a62a0..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]}],{"#f":3}],"#f":3}],"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 656661f..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]}],{"#f":3}],"#f":3}],"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 1dc7089..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]}],{"#f":3}],"#f":3}],"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 d526480..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}]}],{"#f":3}],"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}]}],{"#f":3}],"#f":3}],"#f":3}],"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 b2000a4..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}]}],{"#f":3}],"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}]}],{"#f":3}],"#f":3}],"#f":3}],"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 cccbdcd..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",{"#f":3}],"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 ae1aca8..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",{"#f":3}],"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 adb83b2..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",{"#f":3}],"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 1e68659..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",{"#f":3}],"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 2c0e388..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",{"#f":3}],"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 4d25d07..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}]}],{"#f":3}],"#f":3}],"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 7c01b45..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",{"#f":3}],"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 bf3882c..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}]}],{"#f":3}],"#f":3}],"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 35974b0..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}]}],{"#f":3}],"hurry_outside":["^We hurried home to Savile Row ",{"->":"as_fast_as_we_could"},"\n",{"#f":3}],"dragged_outside":["^He insisted that we hurried home to Savile Row ",{"->":"as_fast_as_we_could"},"\n",{"#f":3}],"as_fast_as_we_could":["<>","^as fast as we could. ","end","\n",{"#f":3}],"#f":3}],"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 a24d3ef..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}]}],{"#f":3}],"paragraph_2":["^You open the gate, and step out onto the path. ","end","\n",{"#f":3}],"#f":3}],"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 83044bd..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",{"#f":3}],"#f":3}],"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 820fdc3..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",{"#f":3}],"#f":3}],"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 9e15aa1..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",{"temp=":"x","re":true},"ev",{"VAR?":"x"},5,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","ev",6,"/ev",{"temp=":"x","re":true},{"->":"derp.15"},null]}],"nop","\n","ev",{"VAR?":"x"},{"VAR?":"c"},"+","/ev",{"temp=":"y","re":true},{"#f":3}],"global decl":["ev",0,{"VAR=":"x"},3,{"VAR=":"y"},"/ev","end",null],"#f":3}],"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 e88c885..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",{"temp=":"x","re":true},["ev",{"VAR?":"x"},0,"==","/ev",{"->":".^.b","c":true},{"b":["\n","ev",0,"/ev",{"temp=":"y","re":true},{"->":"derp.11"},null]}],["ev",{"VAR?":"x"},0,">","/ev",{"->":".^.b","c":true},{"b":["\n","ev",{"VAR?":"x"},1,"-","/ev",{"temp=":"y","re":true},{"->":"derp.11"},null]}],[{"->":".^.b"},{"b":["\n","ev",{"VAR?":"x"},1,"+","/ev",{"temp=":"y","re":true},{"->":"derp.11"},null]}],"nop","\n",{"#f":3}],"global decl":["ev",0,{"VAR=":"x"},3,{"VAR=":"y"},1,{"VAR=":"z"},"/ev","end",null],"#f":3}],"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 eef2bc3..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",{"temp=":"mult","re":true},{"->":".^.^.^.6"},null]}],"nop","\n","ev",{"VAR?":"mult"},2,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","ev",1,"/ev",{"temp=":"roll","re":true},{"->":".^.^.^.14"},null]}],"nop","\n","ev",{"VAR?":"roll"},0,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","ev",3,"/ev",{"temp=":"mult","re":true},{"->":".^.^.^.22"},null]}],"nop","\n","ev",{"VAR?":"dst"},100,"*",100,"/","/ev",{"temp=":"deadline","re":true},"ev",1,{"VAR?":"dst"},"+",10,"*",{"VAR?":"mult"},"*","/ev",{"temp=":"fee","re":true},{"#f":3}],"global decl":["ev",20,{"VAR=":"weight"},0,{"VAR=":"roll"},1,{"VAR=":"mult"},5,{"VAR=":"dst"},0,{"VAR=":"deadline"},0,{"VAR=":"fee"},"/ev","end",null],"#f":3}],"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 28a1ecf..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","->->",{"#f":3}],"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",{"#f":3}],"zero_equals_":[{"temp=":"k"},"ev",0,{"f()":"do_nothing"},"pop","/ev","\n","ev",0,{"VAR?":"k"},"==","/ev","~ret",{"#f":3}],"do_nothing":[{"temp=":"k"},"ev",0,"/ev","~ret",{"#f":3}],"#f":3}],"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 0072d4c..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",{"temp=":"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",{"#f":3}],"global decl":["ev",0.0,{"VAR=":"x"},"/ev","end",null],"#f":3}],"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 9949dcb..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",{"#f":3}],"#f":3}],"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 e49e003..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",{"temp=":"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",{"#f":3}],"global decl":["ev",0.0,{"VAR=":"x"},"/ev","end",null],"#f":3}],"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 7712a7f..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",{"#f":3}],"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 c7f7e34..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",{"temp=":"x","re":true},{"#f":3}],"global decl":["ev",0.0,{"VAR=":"x"},"/ev","end",null],"#f":3}],"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 dd67d0d..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",{"temp=":"roll","re":true},{"->":"0.6"},null]}],"nop","\n","ev",{"VAR?":"roll"},0,"==","/ev",[{"->":".^.b","c":true},{"b":["\n","ev",1,"/ev",{"temp=":"roll","re":true},{"->":"0.14"},null]}],"nop","\n",["done",{"#n":"g-0"}],null],"done",{"global decl":["ev",0,{"VAR=":"roll"},"/ev","end",null],"#f":3}],"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 5a3d6a5..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",{"#f":3}],"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 12bf67a..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",{"#f":3}],"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 be101ac..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",{"#f":3}],"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 fd91773..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",{"#f":3}],"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 0f0bd28..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",{"#f":3}],"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 29887cf..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",{"#f":3}],"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 c2876d7..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"},{"#f":3}],"as_fast_as_we_could":["<>","^ as fast as we could.","\n","end",{"#f":3}],"#f":3}],"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 790d67e..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",{"#f":3}],"#f":3}],"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 7be0841..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",{"#f":3}],"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 82d6087..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",{"#f":3}],"#f":3}],"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 2d5177a..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",{"#f":3}],"#f":3}],"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 9e1d8c4..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",{"#f":3}],"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 d78e44b..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",{"#f":3}],"#f":3}],"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 c89ca91..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",{"#f":3}],"#f":3}],"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 c959b74..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",{"#f":3}],"global decl":["ev",1,{"VAR=":"x"},"str","^Hmm.","/str",{"VAR=":"y"},"/ev","end",null],"#f":3}],"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 7587491..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",{"#f":3}],"finished":[{"temp=":"total"},"^\"The result is ","ev",{"VAR?":"total"},"out","/ev","^!\" you announce.","\n","^Gauss stares at you in horror.","\n","end",{"#f":3}],"#f":3}],"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 8038766..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",{"#f":3}],"#f":3}],"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 d3cd579..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",{"#f":3}],"global decl":["ev",1,{"VAR=":"x"},2,{"VAR=":"y"},0,{"VAR=":"z"},"/ev","end",null],"#f":3}],"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 fcef608..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",{"#f":3}],"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 9e1d8c4..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",{"#f":3}],"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 7b59efb..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],"#f":3}],"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 74b4e7a..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"},{"#f":5}],"c-1":["ev",{"^->":"0.init.c-1.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.1.s"},[{"#n":"$r2"}],"\n",{"->":".^.^"},{"->":"0.g-0"},{"#f":5}],"c-2":["ev",{"^->":"0.init.c-2.$r2"},"/ev",{"temp=":"$r"},{"->":".^.^.2.s"},[{"#n":"$r2"}],"\n","end",{"->":"0.g-0"},{"#f":5}],"#f":5,"#n":"init"}],{"g-0":["done",{"#f":5}]}],"done",{"global decl":["ev",{"list":{},"origins":["gameState"]},{"VAR=":"gameState"},"/ev","end",null],"#f":1}],"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 3b54819..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",{"temp=":"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],"#f":3}],"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 79aaa20..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],"#f":3}],"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-all.ink b/src/test/resources/inkfiles/lists/list-all.ink new file mode 100644 index 0000000..5c83e3e --- /dev/null +++ b/src/test/resources/inkfiles/lists/list-all.ink @@ -0,0 +1,3 @@ +LIST a = A +LIST b = B +{LIST_ALL(A + B)} \ No newline at end of file diff --git a/src/test/resources/inkfiles/lists/list-all.ink.json b/src/test/resources/inkfiles/lists/list-all.ink.json new file mode 100644 index 0000000..2d1f34a --- /dev/null +++ b/src/test/resources/inkfiles/lists/list-all.ink.json @@ -0,0 +1,63 @@ +{ + "inkVersion": 21, + "root": [ + [ + "ev", + { + "VAR?": "A" + }, + { + "VAR?": "B" + }, + "+", + "LIST_ALL", + "out", + "/ev", + "\n", + [ + "done", + { + "#f": 5, + "#n": "g-0" + } + ], + null + ], + "done", + { + "global decl": [ + "ev", + { + "list": {}, + "origins": [ + "a" + ] + }, + { + "VAR=": "a" + }, + { + "list": {}, + "origins": [ + "b" + ] + }, + { + "VAR=": "b" + }, + "/ev", + "end", + null + ], + "#f": 1 + } + ], + "listDefs": { + "a": { + "A": 1 + }, + "b": { + "B": 1 + } + } +} \ 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 8003d5f..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],"#f":3}],"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 40f3c1e..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",{"temp=":"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],"#f":3}],"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 1a6f2d7..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",{"temp=":"t","re":true},"ev",{"VAR?":"t"},"out","/ev","\n",["done",{"#n":"g-0"}],null],"done",{"elsewhere":["ev",{"VAR?":"z"},"/ev",{"temp=":"t","re":true},"ev",{"VAR?":"t"},"out","/ev","\n","end",{"#f":3}],"global decl":["ev",{"list":{"l1.a":1,"l1.c":3}},{"VAR=":"l1"},{"list":{"l2.x":1}},{"VAR=":"l2"},{"list":{}},{"VAR=":"t"},"/ev","end",null],"#f":3}],"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 d4e1ab6..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",{"temp=":"t","re":true},"ev",{"VAR?":"t"},"out","/ev","\n","ev",{"VAR?":"t"},"LIST_ALL","/ev",{"temp=":"t","re":true},"\n","ev",{"VAR?":"t"},{"VAR?":"n"},"-",{"temp=":"t","re":true},"/ev","ev",{"VAR?":"t"},"out","/ev","\n","ev",{"VAR?":"t"},"LIST_INVERT","/ev",{"temp=":"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],"#f":3}],"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/lists/more-list-operations2.ink b/src/test/resources/inkfiles/lists/more-list-operations2.ink new file mode 100644 index 0000000..4c30ed0 --- /dev/null +++ b/src/test/resources/inkfiles/lists/more-list-operations2.ink @@ -0,0 +1,43 @@ +LIST list1 = (a1), b1, c1 +LIST list2 = a2, b2, c2 +LIST list3 = a3, b3, c3 +VAR vlist = () + +{LIST_ALL(list1)} +{list1} + +~list2 += a1 +~list2 += b2 + +{list2} +count:{LIST_COUNT(list2)} + +~list2 += c2 + +max:{LIST_MAX(list2)} +min:{LIST_MIN(list2)} + +// Equality +~temp t = list2 +{t == list2} +{t == (a1, b2, c2)} +{t != list2} + +//emptiness +{list3: not empty| empty} + +~vlist = (a2) +{ vlist } +{ LIST_ALL(vlist) } + +range:{ LIST_RANGE(list2, 1, 2)} +{ LIST_RANGE(list2, a1, a3)} + +subtract:{(a1,b1,c1) - (b1)} + +~ SEED_RANDOM(10) +random:{LIST_RANDOM(t)} + +listinc:{(a1) + 1} + + diff --git a/src/test/resources/inkfiles/lists/more-list-operations2.ink.json b/src/test/resources/inkfiles/lists/more-list-operations2.ink.json new file mode 100644 index 0000000..47a4075 --- /dev/null +++ b/src/test/resources/inkfiles/lists/more-list-operations2.ink.json @@ -0,0 +1 @@ +{"inkVersion":21,"root":[["ev",{"VAR?":"list1"},"LIST_ALL","out","/ev","\n","ev",{"VAR?":"list1"},"out","/ev","\n","ev",{"VAR?":"list2"},{"VAR?":"a1"},"+",{"VAR=":"list2","re":true},"/ev","ev",{"VAR?":"list2"},{"VAR?":"b2"},"+",{"VAR=":"list2","re":true},"/ev","ev",{"VAR?":"list2"},"out","/ev","\n","^count:","ev",{"VAR?":"list2"},"LIST_COUNT","out","/ev","\n","ev",{"VAR?":"list2"},{"VAR?":"c2"},"+",{"VAR=":"list2","re":true},"/ev","^max:","ev",{"VAR?":"list2"},"LIST_MAX","out","/ev","\n","^min:","ev",{"VAR?":"list2"},"LIST_MIN","out","/ev","\n","ev",{"VAR?":"list2"},"/ev",{"temp=":"t"},"ev",{"VAR?":"t"},{"VAR?":"list2"},"==","out","/ev","\n","ev",{"VAR?":"t"},{"list":{"list1.a1":1,"list2.b2":2,"list2.c2":3}},"==","out","/ev","\n","ev",{"VAR?":"t"},{"VAR?":"list2"},"!=","out","/ev","\n","ev",{"VAR?":"list3"},"/ev",[{"->":".^.b","c":true},{"b":["^ not empty",{"->":"0.85"},null]}],[{"->":".^.b"},{"b":["^ empty",{"->":"0.85"},null]}],"nop","\n","ev",{"list":{"list2.a2":1}},"/ev",{"VAR=":"vlist","re":true},"ev",{"VAR?":"vlist"},"out","/ev","\n","ev",{"VAR?":"vlist"},"LIST_ALL","out","/ev","\n","^range:","ev",{"VAR?":"list2"},1,2,"range","out","/ev","\n","ev",{"VAR?":"list2"},{"VAR?":"a1"},{"VAR?":"a3"},"range","out","/ev","\n","^subtract:","ev",{"list":{"list1.a1":1,"list1.b1":2,"list1.c1":3}},{"list":{"list1.b1":2}},"-","out","/ev","\n","ev",10,"srnd","pop","/ev","\n","^random:","ev",{"VAR?":"t"},"lrnd","out","/ev","\n","^listinc:","ev",{"list":{"list1.a1":1}},1,"+","out","/ev","\n",["done",{"#f":5,"#n":"g-0"}],null],"done",{"global decl":["ev",{"list":{"list1.a1":1}},{"VAR=":"list1"},{"list":{},"origins":["list2"]},{"VAR=":"list2"},{"list":{},"origins":["list3"]},{"VAR=":"list3"},{"list":{}},{"VAR=":"vlist"},"/ev","end",null],"#f":1}],"listDefs":{"list1":{"a1":1,"b1":2,"c1":3},"list2":{"a2":1,"b2":2,"c2":3},"list3":{"a3":1,"b3":2,"c3":3}}} \ 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 5fabf0e..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",{"#f":3}],"x_is_set":["^X is set","\n","end",{"#f":3}],"global decl":["ev","str","^","/str",{"VAR=":"x"},"/ev","end",null],"#f":3}],"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 85dc834..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","str","^","/str","/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 204f6f6..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",{"#f":3}],"#f":3}],"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 881073e..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",{"#f":3}],"two":["^Two ",{"->":"end"},"\n",{"#f":3}],"three":["^Three ",{"->":"end"},"\n",{"#f":3}],"end":["end",{"#f":3}],"#f":3}],"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 574fdb9..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",{"#f":3}],"stwo":["^One.2 ",{"->":"end"},"\n",{"#f":3}],"#f":3}],"two":[{"->":".^.sone"},{"sone":["^Two.1 ",{"->":"end"},"\n",{"#f":3}],"stwo":["^Two.2 ",{"->":"end"},"\n",{"#f":3}],"sthree":["^Two.3 ",{"->":"end"},"\n",{"#f":3}],"#f":3}],"end":["end",{"#f":3}],"#f":3}],"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 d095abc..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}]}],{"#f":3}],"hurry_outside":["^We hurried home to Savile Row ",{"->":"as_fast_as_we_could"},"\n",{"#f":3}],"dragged_outside":["^He insisted that we hurried home to Savile Row","\n",{"->":"as_fast_as_we_could"},{"#f":3}],"as_fast_as_we_could":["<>","^ as fast as we could.","\n","end",{"#f":3}],"#f":3}],"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 b/src/test/resources/inkfiles/runtime/multiflow-basics.ink new file mode 100644 index 0000000..4c71e11 --- /dev/null +++ b/src/test/resources/inkfiles/runtime/multiflow-basics.ink @@ -0,0 +1,9 @@ +=== knot1 +knot 1 line 1 +knot 1 line 2 +-> END + +=== knot2 +knot 2 line 1 +knot 2 line 2 +-> END \ 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 new file mode 100644 index 0000000..09d69db --- /dev/null +++ b/src/test/resources/inkfiles/runtime/multiflow-basics.ink.json @@ -0,0 +1 @@ +{"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 b/src/test/resources/inkfiles/runtime/multiflow-saveloadthreads.ink new file mode 100644 index 0000000..27dc6cf --- /dev/null +++ b/src/test/resources/inkfiles/runtime/multiflow-saveloadthreads.ink @@ -0,0 +1,30 @@ +Default line 1 +Default line 2 + +== red == +Hello I'm red +<- thread1("red") +<- thread2("red") +-> DONE + +== blue == +Hello I'm blue +<- thread1("blue") +<- thread2("blue") +-> DONE + +== thread1(name) == ++ Thread 1 {name} choice + -> thread1Choice(name) + +== thread2(name) == ++ Thread 2 {name} choice + -> thread2Choice(name) + +== thread1Choice(name) == +After thread 1 choice ({name}) +-> END + +== thread2Choice(name) == +After thread 2 choice ({name}) +-> END \ 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 new file mode 100644 index 0000000..1a11845 --- /dev/null +++ b/src/test/resources/inkfiles/runtime/multiflow-saveloadthreads.ink.json @@ -0,0 +1 @@ +{"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 291a852..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",{"#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":3}],"two":["end",{"s2":["ev",{"VAR?":"x"},1,"+","/ev",{"temp=":"x","re":true},{"->":"one"},{"#f":3}],"#f":3}],"global decl":["ev",0,{"VAR=":"x"},"/ev","end",null],"#f":3}],"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 a979c53..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"},{"#f":3}],"to_savile_row":["^to Savile Row","\n",{"->":"as_fast_as_we_could"},{"#f":3}],"as_fast_as_we_could":["<>","^ as fast as we could.","\n","end",{"#f":3}],"#f":3}],"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 05e228a..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",{"temp=":"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],"#f":3}],"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 3afd93f..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",{"temp=":"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",{"temp=":"x","re":true},"end",{"->":"0.g-0"},{"#f":5}],"g-0":["done",null]}],"done",{"global decl":["ev",0,{"VAR=":"x"},"/ev","end",null],"#f":3}],"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 6341a5c..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}]}],{"#f":3}],"in_third_class":["^I put myself in third.","\n","end",{"#f":3}],"#f":3}],"#f":3}],"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 87bc44c..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}]}],{"#f":3}],"in_third_class":["^I put myself in third.","\n","end",{"#f":3}],"#f":3}],"#f":3}],"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 cc8e44e..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",{"#f":3}],"#f":3}],"global decl":["ev",2,{"VAR=":"x"},"/ev","end",null],"#f":3}],"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/tagsInChoiceDynamic.ink b/src/test/resources/inkfiles/tags/tagsInChoiceDynamic.ink new file mode 100644 index 0000000..a5fcedc --- /dev/null +++ b/src/test/resources/inkfiles/tags/tagsInChoiceDynamic.ink @@ -0,0 +1,6 @@ +VAR name = "Name" +// Should add tag 'tag Name' to choice at runtime ++ [Choice #tag {name}] ++ [Choice2 #tag 1 {name} 2 3 4] ++ [Choice #{name} tag 1 2 3 4] +->END \ No newline at end of file diff --git a/src/test/resources/inkfiles/tags/tagsInChoiceDynamic.ink.json b/src/test/resources/inkfiles/tags/tagsInChoiceDynamic.ink.json new file mode 100644 index 0000000..612192d --- /dev/null +++ b/src/test/resources/inkfiles/tags/tagsInChoiceDynamic.ink.json @@ -0,0 +1 @@ +{"inkVersion":21,"root":[["ev","str","^Choice ","#","^tag ","ev",{"VAR?":"name"},"out","/ev","/#","/str","/ev",{"*":"0.c-0","flg":4},"ev","str","^Choice2 ","#","^tag 1 ","ev",{"VAR?":"name"},"out","/ev","^ 2 3 4","/#","/str","/ev",{"*":"0.c-1","flg":4},"ev","str","^Choice ","#","ev",{"VAR?":"name"},"out","/ev","^ tag 1 2 3 4","/#","/str","/ev",{"*":"0.c-2","flg":4},{"c-0":["\n",{"->":"0.g-0"},null],"c-1":["\n",{"->":"0.g-0"},null],"c-2":["\n","end",{"->":"0.g-0"},null],"g-0":["done",null]}],"done",{"global decl":["ev","str","^Name","/str",{"VAR=":"name"},"/ev","end",null]}],"listDefs":{}} \ No newline at end of file diff --git a/src/test/resources/inkfiles/tags/tagsInLines.ink.json b/src/test/resources/inkfiles/tags/tagsInLines.ink.json new file mode 100644 index 0000000..4e86011 --- /dev/null +++ b/src/test/resources/inkfiles/tags/tagsInLines.ink.json @@ -0,0 +1 @@ +{"inkVersion":21,"root":[["^í","\n","^a","\n",["done",{"#f":5,"#n":"g-0"}],null],"done",{"#f":1}],"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 010e0da..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],"#n":"top"}],null],{"#f":3}],"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]}],{"#f":3}],"#f":3}],"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 7e09fb6..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","->->",{"#f":3}],"B":["^Now in B.","\n","end",{"#f":3}],"#f":3}],"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 2044365..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}]}],{"#f":3}],"everybody_dies":["^Everybody dies.","\n","end",{"#f":3}],"global decl":["ev",{"^->":"everybody_dies"},{"VAR=":"current_epilogue"},"/ev","end",null],"#f":3}],"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 8bdc74b..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",{"#f":5,"#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],"#f":1}],"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 9fde585..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],"#f":3}],"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 f15f342..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",{"#f":5}]}],"done",{"global decl":["ev","str","^","/str",{"VAR=":"v"},"/ev","end",null],"#f":1}],"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 c7d15d4..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]}],{"#f":3}],"#f":3}],"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 ec6255d..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]}],{"#f":3}],"#f":3}],"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 e4f9928..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]}],{"#f":3}],"#f":3}],"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 1040d87..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","/ev","ev","du",0,"==","/ev",{"->":".^.s0","c":true},"ev","du",1,"==","/ev",{"->":".^.s1","c":true},"ev","du",2,"==","/ev",{"->":".^.s2","c":true},"pop","nop",{"s0":["pop","^\"Three!\"",{"->":".^.^.22"},null],"s1":["pop","^\"Two!\"",{"->":".^.^.22"},null],"s2":["pop","^\"One!\"",{"->":".^.^.22"},null],"#f":5}],"\n","ev","str","^Again","/str","/ev",{"*":".^.c-0","flg":4},{"c-0":["^ ",{"->":"test"},"\n",null]}],{"#f":3}],"#f":3}],"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 e96487e..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]}],{"#f":3}],"#f":3}],"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